Add functional eval scenarios and CI reporting - #362
Conversation
Preview:
|
149ec1e to
e869e02
Compare
5b9e609 to
9a8dad3
Compare
9a8dad3 to
5e8cffe
Compare
5e8cffe to
3250a45
Compare
3250a45 to
a9269d7
Compare
a9269d7 to
e8cc355
Compare
1a8ff13 to
856c92c
Compare
856c92c to
2db6506
Compare
2db6506 to
f3a8952
Compare
| // check-then-write implementation oversells the slot. | ||
| await verifier.check("concurrent-booking-never-oversells", async () => { | ||
| using api = await verifier.connect<DeskApi>("Desk"); | ||
| const capacity = 3; |
There was a problem hiding this comment.
The capacity contract accepts any positive integer, but unique bookings are only exercised through capacity 3. The capacity-8 fixture below sends one repeated booking ID, so an implementation that enforces Math.min(slot.capacity, 3) passes every check while rejecting the fourth valid booking on larger slots. Please exercise more than three unique bookings against a larger capacity.
| verifyAfterAccept: async verifier => { | ||
| await verifier.check("bookings-survive-code-reload", async () => { | ||
| using api = await verifier.connect<DeskApi>("Desk"); | ||
| const status = StatusSchema.parse(await api.slotStatus({ slotId: "seq" })); |
There was a problem hiding this comment.
This reload check only covers seq, so cancellation need not be durable. An implementation can remove cancel-1 from memory, persist cancel-3, and pass the pre-accept check; after reload cancel-1 reappears and the capacity-1 slot is oversold. Please verify the final cancel state after accepting as well.
| evidence: { duplicate, total, balances, settlement, analysis }, | ||
| }; | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Turn two ends without accepting and reloading these changes. A Gadget that durably stores expenses but keeps every budget in an instance-local Map passes all budget assertions, even though the task requires Gadget-owned storage that survives restart. Add post-accept verification for the budget rows created in this turn.
| const afterState = canonicalBalances(after); | ||
| return { | ||
| pass: accepted.ok && wrong.length === 0 && | ||
| JSON.stringify(beforeState) === JSON.stringify(afterState), |
There was a problem hiding this comment.
Equal balances do not prove that rejected inputs changed nothing. For example, the Gadget can insert the zero-amount or empty-split rows and return the expected errors; those rows contribute no balance, so this still passes while their IDs remain consumed. Retry the rejected IDs with valid data or otherwise observe that no rows were stored.
|
Submitted 4 actionable inline review comments. |
| results: packages/workshop-evals/.wrangler/evals/results.json | ||
| publish-check: true | ||
| check-name: Workshop evals | ||
| fail-on-failures: ${{ !inputs.reportOnly }} |
There was a problem hiding this comment.
With reportOnly true this disables the action gate entirely, while the eval step above is still continue-on-error. In vitest-evals an unenforced report is advisory (shouldFail is false), even when the JSON records a non-eval/global-setup failure or zero eval cases. Therefore report-only runs can finish green for infrastructure failures, although the input promises only to tolerate failed trials / skip score enforcement. Please preserve a hard-failure path for invalid or infrastructure-failed reports while suppressing only qualitative eval misses.
| const status = StatusSchema.parse(await api.slotStatus({ slotId: "race" })); | ||
| const { accepted, errors } = tally(results); | ||
| return { | ||
| pass: accepted === capacity && status.capacity === capacity && |
There was a problem hiding this comment.
This counts successful responses and stored holders independently, so they need not describe the same bookings. For example, race-0..2 can return success while the implementation stores person-7..9; the accepted/error counts and three unique personIds all pass, but successful callers have no reservations. Please compare status.personIds with the people corresponding to the successful result indexes.
| return { | ||
| pass: accepted === 1 && errors.DUPLICATE_BOOKING === 5 && | ||
| !crossSlot.ok && crossSlot.error === "DUPLICATE_BOOKING" && | ||
| status.capacity === 8 && status.bookedCount === 1 && otherStatus.bookedCount === 0, |
There was a problem hiding this comment.
status is captured before the cross-slot duplicate attempt, so this does not verify that the rejected call changed nothing in the original slot. A handler can update dupe-same from ana to ben, return DUPLICATE_BOOKING, and leave dupe-other empty; every assertion still passes. Re-read and validate the original slot after crossSlot.
| const analysis = analyzeSettlement(balances, settlement); | ||
| const total = balances.perPerson.reduce((sum, entry) => sum + entry.netCents, 0); | ||
| return { | ||
| pass: !duplicate.ok && duplicate.error === "DUPLICATE_ID" && |
There was a problem hiding this comment.
The turn-two categorized expenses are only observed through budgetStatus(). These assertions still pass if budget-1 and budget-2 are kept in a separate budget table and ignored by balances(), settlement(), and monthlyTotals(): the checked March balances remain exact and the global sum remains zero. That violates the unchanged LedgerApi contract for newly added expenses. Please assert the June total and the expected budget-ana/budget-ben balances (and settlement) after adding them.
|
Submitted 4 actionable inline review comments. |
86346b8 to
5029f5f
Compare
|
|
5029f5f to
6c1cf6e
Compare
|
|
6c1cf6e to
cf91ea7
Compare
|
| dateIso: "2026-06-09", | ||
| })); | ||
| const balances = BalancesSchema.parse(await api.balances()); | ||
| const settlement = SettlementSchema.parse(await api.settlement()); |
There was a problem hiding this comment.
The turn-two settlement() call is the final state access, so the eval does not verify that this read leaves the ledger unchanged. An implementation can compute valid transfers from the balances captured above, then delete the expense rows (or persist settlement payments) before returning; every assertion here still passes even though the existing ledger has been destroyed. Please re-read balances/monthly totals after settlement() and require the pre-call state to remain intact.
|
Submitted 1 actionable inline review comment. |
cf91ea7 to
9e17f74
Compare
|
| const defined = OkSchema.parse(await api.defineSlot({ | ||
| slotId: "seq", startIso: "2026-07-01T09:00:00Z", capacity: 2, | ||
| })); | ||
| const duplicate = OkSchema.parse(await api.defineSlot({ |
There was a problem hiding this comment.
P2 Duplicate slot IDs are only tested sequentially. A defineSlot() implementation that checks, awaits, then inserts can let two concurrent calls for the same slotId both return success, violating DUPLICATE_SLOT while passing this suite. Race two definitions with different capacities, require exactly one success and one DUPLICATE_SLOT, and verify the stored capacity matches the winner.
| const accepted = OkSchema.parse(await api.addExpense(valid)); | ||
| const before = BalancesSchema.parse(await api.balances()); | ||
| const rejections = { | ||
| DUPLICATE_ID: OkSchema.parse(await api.addExpense({ ...valid, amountCents: 999 })), |
There was a problem hiding this comment.
P2 Duplicate expense IDs are never submitted concurrently. Two requests can both pass an existence check before either insert completes, both return success, and double-count the expense while all current checks pass. Race two expenses with the same ID and verify one succeeds, one returns DUPLICATE_ID, and totals include it only once.
| dateIso: "2026-04-01", | ||
| }), | ||
| ]; | ||
| const march = MonthlySchema.parse(await api.monthlyTotals({ month: TURN_ONE_MONTH })); |
There was a problem hiding this comment.
P2 Every monthlyTotals() assertion requests March, even though this setup seeds an April expense. An implementation that ignores the month argument and always returns March therefore passes the full eval. Query April as well and require its 90_000 total and payer row.
|
Submitted 3 actionable inline review comments. |
9e17f74 to
18224d9
Compare
18224d9 to
5c6cf5a
Compare
|
| }; | ||
| }); | ||
|
|
||
| await verifier.check("settlement-clears-every-balance-and-stays-short", async () => { |
There was a problem hiding this comment.
P2 settlement() is only called after the fixtures have created multiple non-zero balances, and this check explicitly requires at least one transfer. An implementation that throws or returns a bogus payment when the ledger is empty/already settled therefore passes, even though the RPC must return payments that settle the current balances (which should be [] when nothing is owed). Exercise settlement() before adding expenses, or after constructing a zero-balance state, and require an empty transfer list.
| return { | ||
| pass: added.every(result => OkSchema.parse(result).ok) && | ||
| march.totalCents === 3_700 && paid("month-ana") === 2_500 && paid("month-ben") === 1_200, | ||
| evidence: march, |
There was a problem hiding this comment.
P2 monthlyTotals() is never queried for a valid month containing no expenses. An implementation that assumes an aggregate row exists and throws, or carries payer rows forward from another month, passes all current checks despite the contract requiring totals counting only that month. Query an unused month and require totalCents === 0 with an empty byPayer array.
|
Submitted 2 actionable inline review comments. |
5c6cf5a to
95bb0fa
Compare
|
LGTM! |
|
LGTM! |
This PR adds two more evals and a manual GitHub Actions workflow. The appointment eval checks booking limits, concurrent requests, duplicate IDs, cancellation, and persistence after the agent's changes are accepted. The expense eval uses two prompts in the same workspace to check cents-based splitting, balances, settlements, input errors, monthly totals, budgets, and whether the second code change preserves the first turn's data.
It also adds post-accept checks, so an eval can verify provisional Gadget code, accept the changes, reload the Gadget, and confirm that its behavior and stored data survive.
The workflow runs
pnpm evalsagainst local workerd, uploadsresults.json, and publishes one GitHub check. It accepts model and trial inputs, plus a report-only mode for collecting scores without requiring every task to pass.Wall time: 1.78 seconds
Wall time: 0.45 seconds