An allow-test-rule annotation citing a category that does not apply is worse than no annotation, because it reads as reviewed. Eight were confirmed by reading the assertions each one covered, and auditing the rest found five more plus one refutation — a converter test whose wording described the wrong mechanism while the covered assertion genuinely was deployed-text. The instructive one used the CANONICAL string for the same mistake: STATE.md command output labelled as a deployed artifact. A canonical string is not evidence the category fits, which is why normalising strings alone would have laundered the problem rather than fixed it. Every mapping the audit had inferred rather than code-verified was spot-checked before rewriting, and the ones that turned out not to fit were re-annotated rather than relabelled. Fourteen STATE.md assertions had a typed extractor available all along and now use it; their annotations came out because nothing needs exempting. Eight assertions genuinely need a production change first — CLI stdout and stderr with no structured mode — and are tagged pending-migration-to-typed-ir citing #3090, which is what that category is for. It had zero real uses before this, while one file carried a real citation to migration issue #2974 under a non-canonical tag. Six annotations covered assertions that do no text matching at all. An exemption for a violation that does not exist is noise that makes the real ones harder to audit; those are removed. atomic-write-coverage gains the annotation it always warranted — its own docstring describes a structural-regression-guard while the file carried none. Fifty-nine non-canonical strings across roughly thirty files are normalised, and the allow-test-rule allowlist is regenerated to match. 472 annotations became 463: every one now uses a canonical category, and the two remaining non-canonical strings are ESLint RuleTester fixtures, not annotations. Refs #3057 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
197 lines
8.8 KiB
JavaScript
197 lines
8.8 KiB
JavaScript
'use strict';
|
|
// allow-test-rule: structural-regression-guard (#1188)
|
|
// Guard-placement verification in bin/install.js requires source-text
|
|
// analysis; install.js is a non-exportable CLI script and the guard must be
|
|
// in a specific lexical scope which require()+behavior cannot verify.
|
|
|
|
/**
|
|
* Governance tests for the gsd-core repository root layout.
|
|
*
|
|
* Invariant: the repository root must not contain ad-hoc AI instruction files
|
|
* (such as AGENTS.md) that would become an untracked source of truth running
|
|
* in parallel with the canonical CONTEXT.md and docs/adr/ records.
|
|
*
|
|
* Context: bin/install.js (local Copilot install path, issue #786) writes an
|
|
* AGENTS.md to process.cwd() when `gsd install copilot` is run inside a repo
|
|
* checkout. If that file is ever committed, editors and AI tools that auto-load
|
|
* repo-root instruction files will silently pick up GSD's installer-generated
|
|
* stub rather than the authoritative documentation. This test ensures that
|
|
* artefact never lands in source control.
|
|
*/
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
|
|
test('repo-layout: root AGENTS.md is not git-tracked — no ad-hoc AI instruction file committed alongside CONTEXT.md', () => {
|
|
// The installer legitimately writes AGENTS.md to process.cwd() when
|
|
// `gsd install copilot` runs inside a repo checkout (issue #786). The file
|
|
// may exist on disk — that's expected after a local install. What must NOT
|
|
// happen is committing it to git, where editors and AI tools would silently
|
|
// pick up the installer-generated stub instead of CONTEXT.md.
|
|
let tracked;
|
|
try {
|
|
execFileSync('git', ['ls-files', '--error-unmatch', 'AGENTS.md'], {
|
|
cwd: ROOT, encoding: 'utf8', stdio: 'pipe',
|
|
});
|
|
tracked = true;
|
|
} catch {
|
|
tracked = false;
|
|
}
|
|
assert.equal(
|
|
tracked,
|
|
false,
|
|
[
|
|
'root AGENTS.md must not be git-tracked.',
|
|
'This file is written by `gsd install copilot` (bin/install.js, local Copilot path, issue #786)',
|
|
'when the installer runs inside a repo checkout — its presence on disk is fine,',
|
|
'but it must never be committed.',
|
|
'The repository source of truth for architecture and contributor guidance is',
|
|
'CONTEXT.md and docs/adr/ — not an installer-generated instruction stub.',
|
|
'Run `git rm --cached AGENTS.md` to untrack it if accidentally staged.',
|
|
].join(' '),
|
|
);
|
|
});
|
|
|
|
test('repo-layout: installer writes AGENTS.md only for local Copilot scope (not global), confirming the commit risk is scoped', () => {
|
|
// Verify the installer source encodes the "!isGlobal" guard that restricts
|
|
// AGENTS.md emission to local installs. If that guard were removed, the file
|
|
// could be silently created in any directory the installer runs from,
|
|
// including the repo root during development. This test is a static read of
|
|
// the install source — it does not execute the installer.
|
|
//
|
|
// Why structural rather than a fixed-window regex: a 200-char sliding-window
|
|
// regex between `if (!isGlobal)` and the assignment produces false failures
|
|
// on semantically-equivalent refactors (early-return guards, added comments,
|
|
// interposed conditions that push the tokens apart). The structural approach
|
|
// instead verifies that the agentsMdPath assignment appears INSIDE the body
|
|
// of the `if (!isGlobal)` block in the copilot-instructions surface handler
|
|
// — which is the invariant that actually matters.
|
|
const installJs = fs.readFileSync(path.join(ROOT, 'bin', 'install.js'), 'utf8');
|
|
|
|
// Step 1: Locate the copilot-instructions surface block.
|
|
const copilotBlockStart = installJs.indexOf("plan.installSurface === 'copilot-instructions'");
|
|
assert.ok(
|
|
copilotBlockStart !== -1,
|
|
"bin/install.js must contain a 'copilot-instructions' surface handler; " +
|
|
"the Copilot AGENTS.md guard lives inside it.",
|
|
);
|
|
|
|
// Step 2: Slice to the next installSurface branch so we don't accidentally
|
|
// match tokens from a sibling surface handler.
|
|
const nextSurface = installJs.indexOf('plan.installSurface ===', copilotBlockStart + 1);
|
|
const copilotBlock = installJs.substring(
|
|
copilotBlockStart,
|
|
nextSurface > copilotBlockStart ? nextSurface : copilotBlockStart + 5000,
|
|
);
|
|
|
|
// Step 3: Find the `if (!isGlobal)` guard inside the copilot block.
|
|
const guardIdx = copilotBlock.indexOf('if (!isGlobal)');
|
|
assert.ok(
|
|
guardIdx !== -1,
|
|
'bin/install.js copilot-instructions surface handler must contain an `if (!isGlobal)` guard; ' +
|
|
'removing that guard would allow a local Copilot install to silently create AGENTS.md ' +
|
|
'in any working directory, including this repo checkout.',
|
|
);
|
|
|
|
// Step 4: Walk the brace tree to extract the body of the `if (!isGlobal)` block.
|
|
const openBrace = copilotBlock.indexOf('{', guardIdx);
|
|
assert.ok(openBrace !== -1, 'if (!isGlobal) guard must have an opening brace');
|
|
let depth = 0;
|
|
let i = openBrace;
|
|
while (i < copilotBlock.length) {
|
|
if (copilotBlock[i] === '{') depth++;
|
|
else if (copilotBlock[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
i++;
|
|
}
|
|
const guardBody = copilotBlock.substring(openBrace, i + 1);
|
|
|
|
// Step 5: Assert the repo-root AGENTS.md write site lives inside the guard body.
|
|
assert.ok(
|
|
guardBody.includes('agentsMdPath = path.join(process.cwd()'),
|
|
'bin/install.js must assign `agentsMdPath = path.join(process.cwd(), ...)` INSIDE the ' +
|
|
'`if (!isGlobal)` block in the copilot-instructions surface handler. ' +
|
|
'If this assignment moves outside that block the installer would unconditionally create ' +
|
|
'AGENTS.md in the working directory on every Copilot install, including repo-root runs.',
|
|
);
|
|
});
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
// Folded from tests/enh-191-retire-sdk-package.test.cjs — consolidation epic #1969 (B5 #1974)
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
{
|
|
const { describe: __foldDescribe } = require('node:test');
|
|
__foldDescribe("folded:enh-191-retire-sdk-package (consolidation epic #1969 B5 #1974)", () => {
|
|
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
const INSTALL_PATH = path.join(ROOT, 'bin', 'install.js');
|
|
const ACTIVE_GUIDANCE_PATHS = [
|
|
'docs/contributing/bootstrap.md',
|
|
];
|
|
|
|
function readPackageJson() {
|
|
return JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
|
|
}
|
|
|
|
test('enhancement #191: sdk package artifacts are removed from repository layout', () => {
|
|
const sdkDir = path.join(ROOT, 'sdk');
|
|
const shimPath = path.join(ROOT, 'bin', 'gsd-sdk.js');
|
|
|
|
assert.equal(fs.existsSync(sdkDir), false, 'sdk/ directory must be deleted');
|
|
assert.equal(fs.existsSync(shimPath), false, 'bin/gsd-sdk.js must be deleted');
|
|
});
|
|
|
|
test('enhancement #191: published package no longer exposes gsd-sdk artifacts', () => {
|
|
const pkg = readPackageJson();
|
|
|
|
assert.equal(Object.prototype.hasOwnProperty.call(pkg.bin || {}, 'gsd-sdk'), false,
|
|
'package.json bin must not expose gsd-sdk');
|
|
assert.equal(pkg.bin && pkg.bin['gsd-tools'], 'gsd-core/bin/gsd-tools.cjs',
|
|
'package.json bin.gsd-tools must point to gsd-core/bin/gsd-tools.cjs');
|
|
|
|
const publishedFiles = Array.isArray(pkg.files) ? pkg.files : [];
|
|
const hasSdkPublishedPaths = publishedFiles.some((entry) => String(entry).startsWith('sdk'));
|
|
assert.equal(hasSdkPublishedPaths, false,
|
|
'package.json files must not include sdk artifacts');
|
|
});
|
|
|
|
test('enhancement #191: installer does not maintain gsd-sdk shim compatibility path', () => {
|
|
const installJs = fs.readFileSync(INSTALL_PATH, 'utf8');
|
|
|
|
assert.equal(/\b--sdk\b/.test(installJs), false,
|
|
'bin/install.js must not expose --sdk flag');
|
|
assert.equal(/\b--no-sdk\b/.test(installJs), false,
|
|
'bin/install.js must not expose --no-sdk flag');
|
|
assert.equal(/installSdkIfNeeded\(\{/.test(installJs), false,
|
|
'bin/install.js must not run installSdkIfNeeded during installation');
|
|
});
|
|
|
|
test('enhancement #191: active contributor guidance does not reference retired SDK build steps', () => {
|
|
for (const relPath of ACTIVE_GUIDANCE_PATHS) {
|
|
const body = fs.readFileSync(path.join(ROOT, relPath), 'utf8');
|
|
|
|
assert.equal(
|
|
/\bbuild:sdk\b|\bcd sdk\b|\bsdk\/dist\b|\bsdk\/src\b/.test(body),
|
|
false,
|
|
`${relPath} must not direct contributors or agents to use the retired SDK package workflow`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|