fix(#3968): measure commit claims at all three surfaces — ledger, verifier BLOCKER, porcelain HANDOFF (#4230)
* test(#3968): commit claims must be measured against git, never narrated * fix(#3968): measure commit claims at all three surfaces Emitted-Drift-Ack-Growth: gsd-executor.md — #3968 plan commit ledger and measured commits contract (held under the agent size cap) Emitted-Drift-Ack-Growth: verify-work.md — #3968 commit-claim reconciliation with the same rev-list instrument Emitted-Drift-Ack-Growth: pause-work.md — #3968 uncommitted_files from git status --porcelain * fix(#3968): retired slash syntax, allowlist line pin, git-compare test pin * fix(#3968): persist the ledger on disk and reconcile with the same rev-list instrument Emitted-Drift-Ack-Growth: gsd-executor.md — #3968 plan commit ledger and measured commits contract (held under the agent size cap) Emitted-Drift-Ack-Growth: verify-work.md — #3968 commit-claim reconciliation with the same rev-list instrument * fix(#3968): hold the gsd-executor size cap with a compact ledger contract Emitted-Drift-Ack-Growth: gsd-executor.md — #3968 plan commit ledger and measured commits contract (held under the agent size cap) * fix(#3968): allowlist pin and HALT regex track the final prose * chore(#3968): changeset for measured commit claims * chore(#3968): backfill changeset pr number * fix(#3968): quote the BASE expansion (SC2086) * ci: raise the test-lane budget 21 to 32 minutes (measured cost grew past the cap) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -65,7 +65,16 @@ const LANE_COSTS = [
|
||||
// figure — rounded up from the higher, cancelled-run observation, since a
|
||||
// cancelled run's own timestamp is still real elapsed time even though the
|
||||
// job never finished.
|
||||
measuredMinutes: 14,
|
||||
// #4070's method applied again: the 14-minute figure went stale after
|
||||
// #4207's run-tests temp-root regression suite landed — its rows spawn
|
||||
// real runner instances on the windows scoped lane (each booting the full
|
||||
// build), and windows shards 1-2 were CANCELLED at 99% of the 21-minute
|
||||
// cap on three consecutive runs (33722188315 and two reruns; ubuntu and
|
||||
// macos lanes green throughout, other PRs' windows lanes green — the
|
||||
// long pole is this lane's windows matrix alone). A cancelled run's own
|
||||
// timestamp is still real elapsed time: >=21 minutes, so 21 is the
|
||||
// honest floor and the budget moves 21 -> 32 (1.5x headroom).
|
||||
measuredMinutes: 21,
|
||||
// Sharded three ways as of #2952, so this is ONE shard's cost, not the
|
||||
// whole unit suite. Shard 1 is the long pole because the unsharded aux
|
||||
// suites (integration/security/install/slow) ride on it — #4070 fixed the
|
||||
@@ -242,7 +251,7 @@ test('mutation.yml mutate job timeout budgets (#4036)', async (t) => {
|
||||
|
||||
test('near-cap check CI_JOB_TIMEOUT_MINUTES literals match each job\'s own timeout-minutes (#4036)', async (t) => {
|
||||
const staticLanes = [
|
||||
{ workflowFile: 'test.yml', jobKey: 'test', envLiteral: '21' },
|
||||
{ workflowFile: 'test.yml', jobKey: 'test', envLiteral: '32' },
|
||||
{ workflowFile: 'test.yml', jobKey: 'test-full', envLiteral: '45' },
|
||||
{ workflowFile: 'install-smoke.yml', jobKey: 'smoke', envLiteral: '12' },
|
||||
];
|
||||
|
||||
@@ -103,7 +103,7 @@ const BARE_COMMAND_RE = new RegExp(
|
||||
// Each entry MUST carry a one-line reason; the test prints the allowlist on
|
||||
// failure so a reviewer can see exactly what is sanctioned.
|
||||
const PROSE_ALLOWLIST = [
|
||||
{ file: 'agents/gsd-executor.md', line: 795, reason: 'describes the SDK return envelope of `gsd-tools query commit`; not an instruction to run the bare word' },
|
||||
{ file: 'agents/gsd-executor.md', line: 819, reason: 'describes the SDK return envelope of `gsd-tools query commit`; not an instruction to run the bare word' },
|
||||
{ file: 'agents/gsd-phase-researcher.md', line: 33, reason: 'package-legitimacy provenance rule names the command as the source of an OK verdict; descriptive' },
|
||||
{ file: 'agents/gsd-roadmapper.md', line: 647, reason: 'parenthetical "e.g." naming SDK queries a user *could* run; not an agent instruction' },
|
||||
{ file: 'agents/gsd-intel-updater.md', line: 40, reason: 'cross-platform note names the `gsd-tools intel <subcommand>` CLI surface descriptively ("CLI invocations go through..."); not an agent instruction' },
|
||||
|
||||
61
tests/summary-commit-verification.test.cjs
Normal file
61
tests/summary-commit-verification.test.cjs
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* #3968 — commit claims must be measured, never narrated.
|
||||
*
|
||||
* Across 14 plans / 3 phases in a real project, `commits: 1` appeared in every
|
||||
* SUMMARY while `git reflog` showed ZERO git activity in the window, and
|
||||
* HANDOFF.json asserted `uncommitted_files: []` over a dirty tree — invisible
|
||||
* because nothing downstream cross-checks the narration against git. The fix
|
||||
* (maintainer decision, option c) spans three shipped surfaces; their text IS
|
||||
* the runtime-loaded contract, so shape assertions are the faithful check.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
||||
|
||||
describe('#3968 — measured commit claims', () => {
|
||||
test('executor measures commits, never narrates them', () => {
|
||||
const executor = read('agents/gsd-executor.md');
|
||||
// The ledger: HEAD captured at the plan's first commit, once per plan.
|
||||
assert.ok(executor.includes('gsd-plan-head-before'),
|
||||
'the ledger must persist on disk (each Bash call is a fresh shell — a variable measures zero)');
|
||||
assert.ok(executor.includes('git rev-list --count'),
|
||||
'the SUMMARY commit count must come from git rev-list --count, not narration');
|
||||
assert.ok(executor.includes('plan_head_before: ${PLAN_HEAD_BEFORE}'),
|
||||
'the base is recorded in the frontmatter so the verifier reconciles with the same instrument');
|
||||
// A measured zero WITH code changes is a HALT, never a narrated success.
|
||||
assert.ok(/HALT — do not write the\s+SUMMARY/i.test(executor),
|
||||
'a measured zero with uncommitted code changes must halt the SUMMARY write');
|
||||
// actuals.commits sources the measured count; the ADR-2629 calibration shape is kept.
|
||||
assert.ok(/commits: 7 .*MEASURED/.test(executor),
|
||||
'the actuals block sources its count from the measurement');
|
||||
});
|
||||
|
||||
test('verify-work flags SUMMARY-vs-git mismatch as BLOCKER', () => {
|
||||
const verify = read('gsd-core/workflows/verify-work.md');
|
||||
assert.ok(verify.includes('Commit-claim reconciliation'),
|
||||
'verify-work must run a commit-claim reconciliation over each SUMMARY');
|
||||
assert.ok(verify.includes('ACTUAL=$(git rev-list --count "${BASE}"..HEAD)'),
|
||||
'the reconciliation uses the SAME instrument as the executor (rev-list over the recorded base)');
|
||||
assert.ok(/ACTUAL == CLAIMED \+ 1/.test(verify),
|
||||
'the post-measurement SUMMARY commit is an expected +1, not a false BLOCKER');
|
||||
assert.ok(/BLOCKER/.test(verify.slice(verify.indexOf('Commit-claim reconciliation'), verify.indexOf('Commit-claim reconciliation') + 1800)),
|
||||
'a mismatch must be flagged BLOCKER — the phase must not read as done');
|
||||
});
|
||||
|
||||
test('HANDOFF uncommitted_files come from git status --porcelain', () => {
|
||||
const pause = read('gsd-core/workflows/pause-work.md');
|
||||
assert.ok(pause.includes('git status --porcelain'),
|
||||
'uncommitted_files must be populated from an actual git status --porcelain call');
|
||||
// The asserted-empty template literal is gone as the only source.
|
||||
const idx = pause.indexOf('uncommitted_files');
|
||||
assert.ok(idx === -1 || /porcelain/.test(pause.slice(Math.max(0, idx - 3000), idx + 3000)),
|
||||
'the uncommitted_files field is defined by the porcelain command, not a narrated []');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user