Commit Graph

3 Commits

Author SHA1 Message Date
Amaury Levé 754011b5ee Add workspace-integrity guardrail to code-testing agents (#773)
The code-testing-generator/implementer agents could treat an unusual or
scaffolded workspace (e.g. a gutted repo with an injected synthetic module)
as corruption and "repair" it with git checkout/restore/reset/clean or rm,
restoring deleted tracked files and testing the wrong code.

- Replace generator Rule 5 ("Clean git first - stash changes") with an
  explicit "Treat the workspace as delivered" rule, and add a "Never mutate
  version control" rule. Output must be purely additive test files.
- Add a no-revert/no-clean invariant to the implementer's edit boundaries.
- Add a 'workspace integrity' eval to the code-testing-agent suite
  (eval.yaml + eval.vally.yaml). The fixture looks gutted: a metricsd project
  whose real core/io modules are committed at HEAD but deleted from the
  working tree, leaving only a synthetic 'synthstr' decoy. A git restore would
  resurrect the deleted sentinel files; graders fail if they reappear and
  require passing pytest tests for the module as delivered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 13:49:34 +00:00
dependabot[bot] faca87d669 Bump vite, @vitest/coverage-v8 and vitest (#731)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) to 8.0.16 and updates ancestor dependencies [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `vite` from 5.4.21 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

Updates `@vitest/coverage-v8` from 2.1.9 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/coverage-v8)

Updates `vitest` from 2.1.9 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: indirect
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.8
  dependency-type: direct:development
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:00:35 +00:00
Amaury Levé 812565f8d1 Add polyglot evals for code-testing-agent (Python Flask + TypeScript Vitest) (#709)
* Add polyglot evals for code-testing-agent (Python Flask + TypeScript Vitest)

Adds two non-.NET evaluation scenarios to the dotnet-test/code-testing-agent
eval to measure how well the agent generates idiomatic tests in Python and
TypeScript — the languages exercised by the msbench top5-* benchmarks.

New fixtures (no pre-existing tests, so passing pytest/vitest proves the agent
actually generated them):
- fixtures/python-flask-tasks/ — small Flask app with TaskService (injected
  clock), TaskRepository Protocol + InMemoryTaskRepository, and a /tasks
  blueprint with HTTP 201/200/400/404/409 surfaces. Validated locally with
  `python -m pip install -e ".[test]" && python -m pytest`.
- fixtures/typescript-vitest-cart/ — small shopping-cart library with an
  injectable DiscountPolicy seam (No/Percentage policies) and a Cart class
  with non-trivial merge / clamping semantics. Validated locally with
  `npm ci && npx vitest run`. package-lock.json committed so `npm ci` is
  reproducible in CI.

New scenarios (added to both eval.yaml and eval.vally.yaml):
- "Generate pytest tests for the Flask tasks API (Python polyglot)" — asserts
  that `python3 -m pip install -e '.[test]' && python3 -m pytest` exits 0
  and that at least one `tests/**/test_*.py` file was produced. Rubric
  rewards using Flask's `test_client()`, mocking `TaskRepository` for
  service-level tests, asserting HTTP status + JSON body, and covering the
  empty-title / >200-char / not-found / already-done error paths.
- "Generate Vitest tests for the shopping-cart library (TypeScript polyglot)"
  — asserts that `npm ci && npx vitest run` exits 0 and that at least one
  `tests/**/*.test.ts` file was produced. Rubric rewards mocking
  `DiscountPolicy` via `vi.fn()`, covering Cart merge semantics,
  `updateQuantity` zero-removes, `totals()` discount-clamping, and the
  `PercentageDiscountPolicy` constructor boundary.

Companion to #708 (polyglot examples + sub-agent generification); these
scenarios exercise the per-language guidance that PR adds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #709

- routes.py: guard `create_task` against non-dict JSON payloads (e.g. list)
  by returning 400 instead of letting `payload.get(...)` raise AttributeError
  and surface as a 500. This keeps the API behavior stable and makes it easier
  for generated tests to assert 400 on invalid input shapes.
- tsconfig.json: drop `vitest/globals` from `compilerOptions.types`. The
  fixture's vitest.config.ts sets `globals: false`, so allowing TypeScript
  to assume globals (describe/it/expect) only enables tests that compile but
  fail at runtime. Removing it keeps the fixture aligned with the non-global
  Vitest API the eval is exercising.

Both fixtures re-smoke-tested locally: vitest 1/1, pytest 2/2 (including a
new test confirming the non-dict body returns 400).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix skill activation for polyglot scenarios in PR #709

Both polyglot scenarios (pytest + Flask, Vitest + TS) scored 5.0/5
quality but reported `⚠️ NOT ACTIVATED` because the SDK's skill router
did not load `code-testing-agent` into the agent's context:
`activated: false, detectedSkills: []` in skillActivationIsolated /
skillActivationPlugin. The .NET ContosoUniversity scenario activates
fine because its prompt has stronger `project-wide, multi-file ... high
coverage` framing and SKILL.md already covers .NET keywords.

Fixes per `eng/skill-validator/src/docs/InvestigatingResults.md` section
"Skill not activated":

- SKILL.md description: add framework-specific keywords (pytest,
  Flask/Django, Vitest, Jest, Mocha, JUnit, Node libraries, API,
  package, project-wide, multi-file) so the router has explicit hooks
  for polyglot prompts. Trimmed verbose `DO NOT USE FOR` clauses to
  stay under the 1,024-char skill spec limit (now 1,019 chars).
- eval.yaml + eval.vally.yaml: rewrite the two polyglot prompts to
  mirror the .NET scenario's framing — add `project-wide, multi-file
  test generation task across the ... layers` and `achieve high
  coverage`, soften the library-prescriptive bullets (Mock(spec=...),
  vi.fn()) into capability-level requirements (`mocked or stubbed`,
  `mock or hand-written stub`). Rubric items unchanged — they remain
  flexible enough to grade either mock style.

Validated locally: `dotnet run --project eng/skill-validator/src --
check --plugin ./plugins/dotnet-test` => ✅ All checks passed
(23 skill(s), 11 agent(s), 1 plugin(s)).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #709 review + sharpen polyglot skill activation

**Activation fix for polyglot scenarios (Python + TypeScript)**

The previous attempt (e8e6feeb2) added pytest/Vitest framework names
to a long capability list, but both polyglot scenarios still reported
activated: false in skillActivationIsolated/skillActivationPlugin
across two consecutive /evaluate runs.

Rewrote the SKILL.md frontmatter description so trigger vocabulary
moves into the "Use when asked to ..." intent phrase and mirrors
the EXACT phrasing the polyglot prompts use (the .NET scenario
already activates because it says "scaffold a new test project",
which is in the description):

- Action triggers: "generate pytest tests", "generate Vitest tests"
  alongside "generate tests" / "write unit tests".
- Scaffolding triggers: "scaffold a new test project / test suite"
  (matches "scaffold a comprehensive pytest suite" and "scaffold
  a comprehensive Vitest suite").
- Surface nouns matching the polyglot fixtures: "web app",
  "REST API", "blueprint", "services, repositories, routes,
  and modules".
- Up front: list what the skill scaffolds across all languages
  (".NET test projects, pytest test suites, Vitest/Jest test
  suites, Go test files, JUnit test suites").

Trimmed the "DO NOT USE FOR" clause to keep the folded description
at 992 chars (under the 1,024 agentskills.io spec limit).

**Review comment fixes**

- fixtures/typescript-vitest-cart/tsconfig.json: drop
  types: ["node"] — fixture has no @types/node so tsc
  would fail with "Cannot find type definition file for 'node'".
- fixtures/typescript-vitest-cart/src/pricing.ts: rewrite the
  DiscountPolicy.computeDiscountCents @returns docstring.
  Was "Must not exceed the subtotal" (contradicted by
  Cart.totals() which clamps both negative and over-subtotal
  values). Now clearly states policies should target
  [0, subtotalCents] but Cart.totals() clamps out-of-range
  values back into the range, so contract and implementation match.
- fixtures/python-flask-tasks/src/tasks_api/models.py: the
  Task.created_at default now uses datetime.now(timezone.utc)
  via default_factory, matching TaskService's timezone-aware
  _now callable. Avoids mixing naive datetime.utcnow() with
  aware datetimes when Task is instantiated directly (e.g., in
  generated tests).
- fixtures/python-flask-tasks/src/tasks_api/routes.py: add an
  explicit isinstance(title, str) guard in create_task
  before delegating to TaskService.create. Previously a JSON
  title value that was a number/null/object would fall through
  to title.strip() and crash with AttributeError -> 500.
- fixtures/python-flask-tasks/src/tasks_api/service.py:
  defense-in-depth — TaskService.create now raises ValueError
  on non-string titles (instead of crashing) so non-Flask callers
  (and unit tests that exercise the service directly with bad
  inputs) get a clean validation error.

Validated locally:

- dotnet run --project eng/skill-validator/src -- check --plugin
  ./plugins/dotnet-test -> All checks passed (23 skill(s),
  11 agent(s), 1 plugin(s)).
- Python fixture: python -m compileall -q src clean.
- TS fixture: npm ci && npx tsc --noEmit clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify polyglot prompts to encourage skill activation

The Python pytest and TypeScript Vitest scenarios were marked NOT
ACTIVATED in evaluation runs even after rewriting the SKILL.md
description with polyglot trigger vocabulary. Investigation of
results.json showed the agent went straight to view/create without
ever invoking the `skill` tool.

Root cause: the polyglot prompts contained long bullet lists
pre-specifying exact test cases (Cart.add merge semantics, updateQuantity
zero-removes, PercentageDiscountPolicy boundary tests, TaskService
validation errors, etc.). This gave the model a complete plan and removed
any incentive to load a planning skill.

Compare with the .NET ContosoUniversity scenario (which does activate
the skill): a short open-ended prompt — "write comprehensive unit tests
across the controllers, services, and the data layer. Achieve high code
coverage." plus a real tool-config requirement (coverlet.collector for
Cobertura XML).

Mirror that style in both polyglot prompts:
- Drop the prescriptive bullet lists; keep a short framing intro and a
  single open-ended ask.
- Add a coverage tooling requirement (pytest-cov XML for Python,
  @vitest/coverage-v8 lcov for TypeScript) to give the skill real
  scaffolding value.
- Rubric and assertions unchanged — they still grade the actual quality
  of the generated suite.

Mirrors guidance in eng/skill-validator/src/docs/InvestigatingResults.md
section 5 ("Check the scenario itself has sufficient information that
the agent can reason that it needs the skill") and avoids the
"illegitimate fixes" called out there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Elaborate polyglot prompts and enforce coverage in assertions

The previous prompt rewrite got the TypeScript Vitest scenario
activating in isolated mode (2/3 runs) but the Python scenario still
NEVER activated. Inspecting the session events showed the agent went
straight to view+create on all 3 Python runs without invoking the
`skill` tool.

Theory: the Python prompt is too abstract (just "service layer, the
repository, and the Flask routes") compared to the .NET prompt which
enumerates Students/Courses/Departments/Instructors controllers. Make
the polyglot prompts feel as substantial as the .NET one by:

- Naming concrete symbols (TaskService with injected clock,
  InMemoryTaskRepository, /tasks, /tasks/<id>, /tasks/<id>/complete
  for Python; Cart.add/remove/updateQuantity/totals plus the two
  policy classes for TS) — mirrors the .NET prompt's named
  controllers list.
- Pointing at the exact mocking + injection strategy ("with the
  repository mocked and the clock injected", "with DiscountPolicy
  mocked").
- Pinning the coverage report to an explicit path (coverage.xml for
  Python, coverage/lcov.info for TS).

Also enforce the coverage tooling requirement at assertion time so
the prompt's "and the project should be configured so... coverage"
is a real deliverable — mirrors the .NET scenario's
`coverage.cobertura.xml` file_exists check:

- Python: added file_exists for fixtures/python-flask-tasks/coverage.xml
  (pytest with --cov=tasks_api --cov-report=xml in addopts writes
  coverage.xml from the directory pytest runs in).
- TS: added a `npx vitest run --coverage` run step plus file_exists
  for fixtures/typescript-vitest-cart/coverage/lcov.info.

Both changes are legitimate per
eng/skill-validator/src/docs/InvestigatingResults.md "improving the
skill vs gaming the eval" — they make the deliverable richer rather
than steering toward the skill or relaxing the rubric.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify SKILL.md routing and mirror coverage assertions in eval.vally.yaml

Per rubber-duck review of the activation strategy:

1. SKILL.md description was ambiguous about coverage — the previous
   wording said "DO NOT USE FOR coverage/CRAP analysis" but the
   polyglot prompts heavily mention coverage tooling. In plugin mode
   that could confuse the router into deferring to coverage-analysis
   or crap-score even when the user wants new tests written. Clarify:
   - In-scope: "configures coverage tooling (coverlet, pytest-cov,
     @vitest/coverage-v8) as part of test generation"
   - Out-of-scope: "analyzing existing coverage reports" (not just
     "coverage")
   Description re-tightened to 1003 chars (under the 1024 limit).

2. eval.vally.yaml previously asserted only that test files exist; the
   eval.yaml mirror added coverage XML / lcov.info file_exists
   assertions in the prior commit. Mirror those into the vally graders
   so both eval schemas enforce the same coverage deliverable:
   - Python: assert fixtures/python-flask-tasks/coverage.xml exists
   - TS: run npx vitest run --coverage --reporter=basic then assert
     fixtures/typescript-vitest-cart/coverage/lcov.info exists

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden polyglot fixtures so baseline cannot perfectly ace them

Both polyglot scenarios (Python Flask, TypeScript Vitest) activated the
code-testing-agent skill in isolated mode but not in plugin mode. Root
cause: the baseline (no skill loaded) was scoring 5/5, leaving the skill
no headroom to demonstrate value, so plugin-mode runs went straight to
view+write without invoking any skill.

Per InvestigatingResults.md section 8, the correct fix is to add genuine
complexity that benefits from a planning workflow rather than relaxing
rubrics or gaming activation. This commit grows both fixtures into
realistic, multi-seam libraries that genuinely reward layered test
planning.

Python (fixtures/python-flask-tasks/) additions:

- models.py: TaskPriority enum, Tag value-object with normalization /
  validation, due_at on Task, is_overdue(now) helper.
- queries.py (new): TaskQuery / TaskPage / apply_query separating
  filter / sort / pagination from the service. Sort by created_at,
  due_at, priority, or title with ascending / descending order; filter
  by status, tag, search substring, and overdue.
- repository.py: TaskRepository protocol gains delete(); added
  normalize_tags() helper that the service uses for tag dedup.
- repository_sqlite.py (new): SqliteTaskRepository — a second backend
  implementing the same Protocol using stdlib sqlite3 (no extra deps),
  with persisted tags via a task_tags table and ON DELETE CASCADE.
- service.py: configurable max_title_length, timezone-aware due_at
  validation, priority / due_at / tag mutations, reopen() and delete()
  state transitions, add_tags / remove_tag with normalization.
- routes.py: new endpoints — DELETE /tasks/<id>, POST /tasks/<id>/reopen,
  POST /tasks/<id>/tags, DELETE /tasks/<id>/tags/<name>. GET /tasks now
  parses status / tag / q / overdue / sort / order / limit / offset and
  returns a paged response with total / has_more.
- app.py: env / config-driven backend selection (TASKS_BACKEND =
  memory | sqlite, TASKS_DATABASE_URL, TASKS_MAX_TITLE_LENGTH).
- README.md: documents the new layout and the expected layered test
  approach.

TypeScript (fixtures/typescript-vitest-cart/) additions:

- pricing.ts: FixedAmountDiscountPolicy and CompositeDiscountPolicy
  with "sum" vs "chain" stacking modes (the latter compounds discounts
  on the remaining subtotal).
- tax.ts (new): TaxCalculator interface; NoTaxCalculator,
  RegionalTaxCalculator (per-region rate table with default fallback),
  AsyncTaxRateProvider async seam, AsyncTaxCalculator adapter.
- shipping.ts (new): ShippingCalculator interface;
  FreeShippingCalculator, FlatShippingCalculator (with optional
  free-over-threshold), WeightBasedShippingCalculator (bracket-based
  with overflow cost).
- inventory.ts (new): PriceFetcher and InventoryChecker async seams,
  InventoryError with productId / requested / available, refreshPrices()
  helper.
- cart.ts: fixed pricing pipeline subtotal → discount (clamped) → tax
  (on the discounted subtotal) → shipping; async checkout() that
  refreshes prices, checks inventory (first denial wins,
  InventoryError thrown), and supports an AsyncTaxRateProvider for
  this call.
- product.ts: optional weightGrams (used by shipping) and currency.
- index.ts: barrel re-exports for all new symbols.
- README.md: documents the new layout and the expected layered test
  approach.

eval.yaml + eval.vally.yaml:

- Updated both polyglot prompts to name the new modules and seams
  (queries.apply_query, SqliteTaskRepository, tag endpoints,
  CompositeDiscountPolicy stacking, AsyncTaxRateProvider,
  WeightBasedShippingCalculator, PriceFetcher / InventoryChecker,
  InventoryError). Style remains open-ended (.NET-style) — no bullet
  list of test cases.
- Replaced the small rubric with outcome-based rubric items that grade
  the new behaviors: pipeline composition (tax on discounted
  subtotal), CompositeDiscountPolicy stacking-mode differences, weight
  bracket boundary semantics, async checkout() with mocked
  collaborators including InventoryError shape, queries.apply_query
  pagination boundaries, SqliteTaskRepository integration tests
  against an in-memory connection, multi-status assertions across
  200/201/204/400/404/409, and constructor validation on the boundary
  classes.
- Bumped per-command timeouts from 5m to 10m to absorb the larger
  test suite.

No skill description / SKILL.md changes — the skill description is
already accurate for these scenarios (1003 / 1024 chars).

* Enforce hard 80% coverage floor on polyglot fixtures

After the previous harden commit (c78532bf7), the polyglot scenarios still
scored a 5.0/5 baseline with no plugin-mode activation. Root cause: even on
the larger fixtures the judge ticked every rubric item because the baseline
agent wrote tests that *touched* each named module — the rubric grades
"covered this concept", not "covered this code path", so a strong baseline
naturally clears it.

The previous commit hardened the SURFACE of the fixtures. This commit
hardens the SUCCESS BAR by baking coverage thresholds into the test
runners themselves, so a baseline that skips an entire module fails the
existing pytest / vitest run-command assertion outright (assertion exit
code != 0 → run fails → judge cannot give 5/5).

Python (fixtures/python-flask-tasks/pyproject.toml):

- Added `pytest-cov>=5.0,<7.0` to the [test] extra so coverage is
  pre-installed (the agent no longer has to add it manually).
- Set `addopts = "-q --cov=tasks_api --cov-branch --cov-report=xml
  --cov-report=term-missing --cov-fail-under=80"`. pytest now exits
  non-zero when total line + branch coverage on tasks_api is below 80%,
  which makes the existing `python -m pytest` assertion fail.

TypeScript (fixtures/typescript-vitest-cart/vitest.config.ts):

- Added a `coverage` block that pre-configures provider: 'v8',
  reporter: ['lcov', 'text'], include: ['src/**/*.ts'], and thresholds
  of lines / statements / functions ≥ 80, branches ≥ 70. The agent no
  longer has to wire up the coverage provider. `npx vitest run
  --coverage` exits non-zero when any threshold is unmet.

Prompts (eval.yaml + eval.vally.yaml):

- Trimmed the prompts to drop the "configure coverage if not already
  there" guidance — coverage tooling is now pre-installed and the floor
  is fixed. The prompt now explicitly tells the agent the threshold is
  enforced and that incidental coverage will not clear the bar, so the
  agent's planning workflow has a concrete cross-module target to plan
  against.

READMEs: documented the new behavior so the agent reading the README
sees the coverage floor up front.

Verified locally: with an empty tests/ directory, pytest exits 1 with
"FAIL Required test coverage of 80% not reached" and `vitest run
--coverage` exits 1 with "Coverage for lines (0%) does not meet global
threshold (80%)". The existing run-command assertions therefore fail for
incomplete suites without needing any new assertion / grader.

* Address PR #709 review: snapshot deep-copy, due_at sort, tests/ wording

- cart.ts: snapshot() now deep-copies the Product on each line in addition

  to the CartLine, so callers mutating the returned snapshot can no longer

  reach back into the cart's internal state via line.product. (Copilot 3344261946)

- queries.py: _sort_key for 'due_at' returns a (is_none, isoformat) tuple

  so we never compare across the None / not-None boundary, and never

  trigger 'cannot compare naive and aware datetimes' TypeErrors when

  a stray naive datetime sneaks through. (Copilot 3347273389)

- README + eval.yaml + eval.vally.yaml: reword the 'tests/ directory is

  intentionally empty' phrasing to 'no test files yet — only a .gitkeep

  marker' so the docs are strictly accurate about what's on disk.

  (Copilot 3344580026 / 3344580055 / 3344580067 / 3344580085 / 3344580104

  / 3344580136 / 3344580156)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 12:38:35 +00:00