* fix(#2931): preserve protected regions and cap emitted per-runtime bytes Route every runtime brand swap through applyClaudeCodeBrandSwap so "Claude Code" survives verbatim inside <runtime_compatibility> regions (#2284b). The fix existed only in bin/install.js's local copies; the src/*.cts exports still used a naive replace, so binding install.js to the single source -- as this phase does for the Windsurf family -- would have silently regressed those runtimes. A table-driven parity guard now covers all nine brand-swapping converters. De-duplicate the Windsurf converter family: delete the six local copies in bin/install.js and bind the four exported ones by reference, guarded by reference-identity assertions (the ADR-1508/#1675 pattern). The two unexported helpers and an unused tool table go with them. Replace the Windsurf 12,000-byte throw with description truncation, matching the bound its sibling skill converter already applied. The throw could only fire on an ~11.7 KB frontmatter description: the largest emitted workflow is 311 bytes. Truncation makes the cap unreachable by construction and leaves 12,000 in exactly one place, eliminating the dual-surface duplication rather than testing for it. Add the emitted-byte cap gate: buildEmittedSizes captures LF- and <HOME>-normalized bytes from the walk buildParityManifest already performs, and evaluateEmittedCaps asserts them against a per-runtime cap table with dead-rule detection. buildParityManifest's return shape is deliberately unchanged -- diffEmitted compares its values with ===, so making them objects would report all 8,529 emitted paths as moved. A regression test pins the values as strings. Add a deterministic trim-safety gate over composeWithinBudget's omitted/shrunk/floored/isolatePrefix metadata, with an anti-vacuity rule, replacing the model-graded eval gate the issue described. * docs(#2931): correct ADR-1671 windsurf premise and trim-safety contract * fix(#2931): bound the windsurf command name and single-source the brand swap Review findings from the orthogonal passes, all fixed inline. The claim that removing the 12,000-byte throw left total emission "bounded by construction" was false. The #1615 regex constrains the character class but not the length, and commandName is interpolated three times into the emitted workflow: a 20,000-character name emitted 60,162 bytes silently. Add WINDSURF_COMMAND_NAME_MAX=128 as a separate, clearly-labelled size control that THROWS -- commandName is the @-ref path target, so truncating it would point the workflow at a file that does not exist (DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED). The #1615 security regex is untouched and still runs first. 128 is generous: the longest shipped name is gsd-plan-review-convergence at 27. Harmonize convertClaudeCommandToWindsurfSkill onto the code-point-safe truncation helper. It still used a UTF-16 slice(0,177) -- the exact surrogate-splitting bug the helper was written to avoid, in the very sibling the helper's comment cites as its model. Bounds are unchanged, so output is byte-identical for every shipped command (descriptions max out at 99 chars). Export applyClaudeCodeBrandSwap and bind it in bin/install.js, deleting the local copy. Adding it to the .cts left two unlinked implementations of identical logic -- the drift class this change exists to remove. Verified byte-identical across eight fixtures and five sequential calls before merging, and guarded by a reference-identity assertion. Convert three try/finally test bodies to t.after (CONTRIBUTING.md:344), add fast-check property coverage for the trim-safety contract, and use fc.pre instead of a bare return in a property callback. * test(#2931): fix three test-authoring bugs the remote matrix caught The remote runner returned 8 unique failures on 6f15cdeb8. All three causes were in the test files, not the modules under test -- local harnesses exercise the modules directly, so nothing executed the test bodies until the matrix did. `{ __proto__: [...] }` in an object literal sets the prototype instead of an own key, so the JSON round-trip erased it and the cap table never saw a reserved runtime key. The production rejection was already correct; the test could not reach it. Use a computed key. Two cap fixtures tripped orthogonal error paths rather than the paths they name: one declared windsurf in the cap table but omitted it from sizes (UNKNOWN_RUNTIME), the other left the sole windsurf pattern matching nothing (a genuine dead rule). Both now include a compliant artifact so the intended branch is what is asserted. The dead-rule and unknown-runtime contracts are deliberate and unchanged. `const { root } = makeSyntheticConfig({ ... `${root}` })` referenced `root` from inside its own initializer -- a temporal dead zone error. makeSyntheticConfig now optionally takes a (root) => files factory. Also raise the npm pack --dry-run bound 60s -> 120s in the shipped- scripts packaging test. That failure is NOT from this branch: the file is byte-identical to next, a fresh tsc measures 1.98s there vs 2.14s here, and the run recorded 60,637ms against a 60,000ms bound -- a timeout under 28,948-test parallel contention, not a slowdown. Fixed rather than deferred because a bound that tight is fragile regardless of which branch trips it. * chore(#2931): backfill changeset pr number to 2984 --------- Co-authored-by: sim <sim@local>
135 lines
6.1 KiB
JavaScript
135 lines
6.1 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* emitted-caps-gate.test.cjs — the cap gate integration test (issue #2931,
|
|
* epic #1671, Phase 4, section A of `.gsd/phase/chore-2931-emitted-byte-caps/
|
|
* 50-test-matrix.md`).
|
|
*
|
|
* `evaluateEmittedCaps` (tests/helpers/emitted-caps.cjs) is a PURE decision
|
|
* function — this file is the one place that feeds it REAL measured bytes
|
|
* from a REAL install, proving the shipped `EMITTED_CAPS` table actually
|
|
* guards something rather than passing vacuously.
|
|
*
|
|
* ── Scope note (A15) ────────────────────────────────────────────────────────
|
|
* `.gsd/phase/chore-2931-emitted-byte-caps/40-design.md` "Scope resolution
|
|
* (A15, made concrete during implementation)": `capabilities/windsurf/
|
|
* capability.json` declares `commands -> destSubpath "workflows"` ONLY under
|
|
* `artifactLayout.local`. The committed GLOBAL fixture
|
|
* (tests/fixtures/install-tree/windsurf.json) holds 344 paths and has ZERO
|
|
* `workflows/` entries — a global install cannot exercise the windsurf cap
|
|
* rule at all. This file therefore builds a LOCAL windsurf install
|
|
* (`runMinimalInstall({ runtime: 'windsurf', scope: 'local' })`), the only
|
|
* scope where the capped artifact family exists.
|
|
*/
|
|
|
|
const { test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const { cleanup } = require('./helpers.cjs');
|
|
const {
|
|
BUILD_SCRIPT,
|
|
runMinimalInstall,
|
|
buildEmittedSizes,
|
|
} = require('./helpers/install-shared.cjs');
|
|
const { evaluateEmittedCaps, formatCapReport } = require('./helpers/emitted-caps.cjs');
|
|
|
|
// hooks/dist is gitignored and built (DEFECT.HOOKS-DIST-SCOPED-CI). Build it
|
|
// idempotently before the shared real-install fixture, mirroring
|
|
// tests/emitted-sizes.test.cjs.
|
|
before(() => {
|
|
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
|
});
|
|
|
|
// ─── Shared real LOCAL windsurf install, built once ───────────────────────────
|
|
let fixture = null;
|
|
let sizes = null;
|
|
|
|
before(() => {
|
|
const { configDir, root } = runMinimalInstall({ runtime: 'windsurf', scope: 'local' });
|
|
fixture = { configDir, root };
|
|
sizes = buildEmittedSizes(fixture.configDir, fixture.root);
|
|
});
|
|
|
|
after(() => {
|
|
if (fixture) cleanup(fixture.root);
|
|
});
|
|
|
|
function windsurfWorkflowRels() {
|
|
return Object.keys(sizes).filter((rel) => /^workflows\/[^/]*\.md$/.test(rel));
|
|
}
|
|
|
|
// ─── The non-vacuous assertion ────────────────────────────────────────────────
|
|
// Without this, evaluateEmittedCaps could report `ok:true` purely because
|
|
// sizes.windsurf never contained a path matching "workflows/*.md" — a gate
|
|
// that is green because it is blind, exactly the failure mode the design
|
|
// doc's A14/dead-rule guard exists to catch structurally. This test proves
|
|
// the fixture really reaches the capped artifact family before trusting any
|
|
// later "ok:true" assertion in this file.
|
|
test('the local windsurf install actually emits workflows/*.md artifacts', () => {
|
|
const rels = windsurfWorkflowRels();
|
|
assert.ok(
|
|
rels.length > 0,
|
|
`expected at least one "workflows/*.md" artifact from a local windsurf install, `
|
|
+ `got top-level dirs: ${JSON.stringify([...new Set(Object.keys(sizes).map((k) => k.split('/')[0]))])}`,
|
|
);
|
|
});
|
|
|
|
// ─── The real gate, run against real bytes ────────────────────────────────────
|
|
|
|
test('evaluateEmittedCaps reports ok:true with zero violations for the real windsurf install', () => {
|
|
const result = evaluateEmittedCaps({ sizes: { windsurf: sizes } });
|
|
assert.strictEqual(result.ok, true, formatCapReport(result) || 'expected ok:true');
|
|
assert.deepStrictEqual(result.violations, []);
|
|
});
|
|
|
|
test('the shipped EMITTED_CAPS table is live — zero dead rules against a real install', () => {
|
|
const result = evaluateEmittedCaps({ sizes: { windsurf: sizes } });
|
|
assert.deepStrictEqual(result.deadRules, [], formatCapReport(result) || 'expected no dead rules');
|
|
});
|
|
|
|
// ─── Boundary trio (cap-1 / cap / cap+1) on the EMITTED path, real bytes ──────
|
|
|
|
test('boundary trio against the real measured max workflows/*.md byte count', () => {
|
|
const rels = windsurfWorkflowRels();
|
|
const maxBytes = Math.max(...rels.map((rel) => sizes[rel]));
|
|
const maxRel = rels.find((rel) => sizes[rel] === maxBytes);
|
|
|
|
const capTableAt = (cap) => ({
|
|
windsurf: [{ pattern: 'workflows/*.md', maxBytes: cap, note: 'synthetic boundary cap for #2931 A3-A5' }],
|
|
});
|
|
|
|
const capMinusOne = evaluateEmittedCaps({ sizes: { windsurf: sizes }, capTable: capTableAt(maxBytes - 1) });
|
|
const capExact = evaluateEmittedCaps({ sizes: { windsurf: sizes }, capTable: capTableAt(maxBytes) });
|
|
const capPlusOne = evaluateEmittedCaps({ sizes: { windsurf: sizes }, capTable: capTableAt(maxBytes + 1) });
|
|
|
|
assert.strictEqual(
|
|
capMinusOne.violations.length, 1,
|
|
`cap=maxBytes-1 (${maxBytes - 1}) must flag "${maxRel}" (${maxBytes} bytes) as a violation`,
|
|
);
|
|
assert.strictEqual(capMinusOne.violations[0].rel, maxRel);
|
|
|
|
assert.strictEqual(
|
|
capExact.violations.length, 0,
|
|
`cap=maxBytes (${maxBytes}) must be inclusive (<=) — no violation`,
|
|
);
|
|
|
|
assert.strictEqual(
|
|
capPlusOne.violations.length, 0,
|
|
`cap=maxBytes+1 (${maxBytes + 1}) must have headroom — no violation`,
|
|
);
|
|
});
|
|
|
|
// ─── A9/A10 not re-derived here: buildEmittedSizes already covers CRLF/UTF-8 ──
|
|
// byte counting in tests/emitted-sizes.test.cjs (B3/B4). This file only
|
|
// re-uses those already-normalized real bytes as input to the cap decision.
|
|
|
|
test('the real max emitted windsurf workflow is comfortably under the shipped 12,000-byte cap', () => {
|
|
const rels = windsurfWorkflowRels();
|
|
const maxBytes = Math.max(...rels.map((rel) => sizes[rel]));
|
|
assert.ok(
|
|
maxBytes < 12000,
|
|
`measured max windsurf workflows/*.md = ${maxBytes} bytes — expected comfortably under the 12,000-byte cap`,
|
|
);
|
|
});
|