fix(constraints): floor-guard reference sees untracked files, deletions and maximum thresholds

The reference guard in constraint-driven-development promised to include
untracked files, catch deleted test files and weakened thresholds, and stay
silent when constraints tighten. Run unchanged, it did none of those for
ordinary local git changes:

- `git diff --no-index /dev/null <file>` exits 1 whenever the sides differ,
  and the shared git() wrapper turned every nonzero exit into null, so every
  untracked file diffed as empty and no added-line rule could see it.
- The filename came from the `+++` header only, so a deletion
  (`+++ /dev/null`) sliced to `ev/null` and matched nothing; there was no
  deletion rule, and staging did not help.
- A number going down was treated as weaker, which is right for a minimum
  (coverage) and inverted for a maximum (size, latency, error budget).
- A removed floor bullet was never reported, although the comment said it was.

The wrapper now keeps stdout on the expected exit 1 and every other failure
exits 2 rather than reading as clean; both diff headers name the file and a
deleted test file is its own finding; each threshold carries a direction read
from the words around it (>=, at least, must not fall; <=, at most, under,
must not grow), a number with no readable direction is reported when it
changes; a removed floor bullet or table row is reported, except an Exceptions
row, whose removal tightens. The three language patterns are unchanged.

scripts/floor-guard-reference-test.js extracts the fenced block and runs it
with real git against fourteen fixtures plus three edge cases; eight of them
fail on the previous reference and all pass now. The workflow runs it beside
the other script tests.
This commit is contained in:
mavericksea-ai committed 2026-09-24 02:25:59 +00:00
1 parent bcab6a1b85
commit 6911a38594
3 files changed
+263 -23

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env node
'use strict';
// Contract test for skills/constraint-driven-development/references/floor-guard.md.
// The fenced `js` block is extracted unchanged and run, with real git, against fourteen
// small fixtures: each is a fresh repository with one source file, one test file and a
// CONSTRAINTS.md carrying a floor bullet, a coverage minimum, a bundle maximum and an
// Exceptions table. Loosening moves must exit 1, tightening and no-op moves must exit 0.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { after, before, test } = require('node:test');
const REFERENCE = path.join(
__dirname, '..', 'skills', 'constraint-driven-development', 'references', 'floor-guard.md',
);
const CONSTRAINTS = [
'# Constraints',
'',
'Last reviewed: 2026-08-08 by @owner',
'',
'## Floor',
'- No skipped tests.',
'',
'| Dimension | Rule |',
'|---|---|',
'| Coverage | >= 80% |',
'| Bundle | <= 200 KB |',
'',
'## Exceptions',
'',
'| ID | Rule | Owner | Expires |',
'|---|---|---|---|',
'| W1 | no-explicit-any | @owner | 2026-11-01 |',
'',
].join('\n');
let scratch;
let guard;
const sandboxes = [];
before(() => {
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-test-'));
const source = fs.readFileSync(REFERENCE, 'utf8');
const match = source.match(/```js\n([\s\S]*?)\n```/);
assert.ok(match, 'floor-guard.md must contain one fenced js block');
guard = path.join(scratch, 'floor-guard.mjs');
fs.writeFileSync(guard, match[1] + '\n');
});
after(() => {
for (const dir of sandboxes) fs.rmSync(dir, { recursive: true, force: true });
if (scratch) fs.rmSync(scratch, { recursive: true, force: true });
});
function git(cwd, ...args) {
const result = spawnSync('git', args, {
cwd,
encoding: 'utf8',
env: {
...process.env,
GIT_AUTHOR_NAME: 'Floor Guard Test',
GIT_AUTHOR_EMAIL: 'floor-guard@example.invalid',
GIT_COMMITTER_NAME: 'Floor Guard Test',
GIT_COMMITTER_EMAIL: 'floor-guard@example.invalid',
GIT_CONFIG_GLOBAL: '/dev/null',
},
});
assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
return result.stdout;
}
function makeRepo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-fixture-'));
sandboxes.push(root);
git(root, 'init', '-q');
fs.writeFileSync(path.join(root, 'app.js'), 'module.exports = 1;\n');
fs.writeFileSync(
path.join(root, 'app.test.js'),
"const assert = require('assert');\nassert.equal(require('./app'), 1);\n",
);
fs.writeFileSync(path.join(root, 'CONSTRAINTS.md'), CONSTRAINTS);
git(root, 'add', '.');
git(root, 'commit', '-qm', 'baseline');
return root;
}
function editConstraints(root, from, to) {
const file = path.join(root, 'CONSTRAINTS.md');
const text = fs.readFileSync(file, 'utf8');
assert.ok(text.includes(from), `fixture CONSTRAINTS.md must contain ${JSON.stringify(from)}`);
fs.writeFileSync(file, text.replace(from, to));
}
function runGuard(root) {
return spawnSync(process.execPath, [guard, '--base', 'HEAD'], { cwd: root, encoding: 'utf8' });
}
const cases = [
['unchanged repository is clean', 0, () => {}],
['a suppression added to a tracked file is a violation', 1, (root) => {
fs.writeFileSync(path.join(root, 'app.js'), '// @ts-ignore\nmodule.exports = 1;\n');
}],
['a suppression in a new untracked file is a violation', 1, (root) => {
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
}],
['a skipped test in a new untracked file is a violation', 1, (root) => {
fs.writeFileSync(path.join(root, 'new.test.js'), "test.skip('regression', () => {});\n");
}],
['a suppression in a staged new file is a violation', 1, (root) => {
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
git(root, 'add', '-A');
}],
['a deleted test file is a violation', 1, (root) => {
fs.unlinkSync(path.join(root, 'app.test.js'));
}],
['a staged deleted test file is a violation', 1, (root) => {
fs.unlinkSync(path.join(root, 'app.test.js'));
git(root, 'add', '-A');
}],
['an assertion removed from a surviving test file is a violation', 1, (root) => {
fs.writeFileSync(path.join(root, 'app.test.js'), "const assert = require('assert');\n");
}],
['lowering a minimum is a violation', 1, (root) => {
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 60% |');
}],
['raising a minimum is silent', 0, (root) => {
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 90% |');
}],
['raising a maximum is a violation', 1, (root) => {
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 300 KB |');
}],
['lowering a maximum is silent', 0, (root) => {
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 150 KB |');
}],
['a deleted floor rule is a violation', 1, (root) => {
editConstraints(root, '- No skipped tests.\n', '');
}],
['a new exception row is a violation', 1, (root) => {
const file = path.join(root, 'CONSTRAINTS.md');
fs.appendFileSync(file, '| W2 | temporary waiver | @owner | 2026-10-01 |\n');
}],
];
for (const [name, expectedExit, mutate] of cases) {
test(`floor guard: ${name}`, () => {
const root = makeRepo();
mutate(root);
const result = runGuard(root);
assert.equal(
result.status, expectedExit,
`expected exit ${expectedExit}, got ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
});
}
test('floor guard: a removed exception row is silent', () => {
const root = makeRepo();
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |\n', '');
const result = runGuard(root);
assert.equal(result.status, 0, result.stderr);
});
test('floor guard: a missing merge base exits 2, never 0', () => {
const root = makeRepo();
const result = spawnSync(process.execPath, [guard, '--base', 'no-such-ref'], {
cwd: root, encoding: 'utf8',
});
assert.equal(result.status, 2, result.stderr);
});
test('floor guard: a threshold with no readable direction is reported when it changes', () => {
const root = makeRepo();
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |', '| W1 | no-explicit-any | @owner | 2027-11-01 |');
const result = runGuard(root);
assert.equal(result.status, 1, result.stderr);
assert.match(result.stderr, /threshold-changed/);
});