AGENTS.md is legitimately created by gsd install copilot (issue #786) when the installer runs inside a repo checkout. The file may exist on disk — that is expected. The test must verify it is not git-tracked, not that it is absent from the working tree. Uses git ls-files --error-unmatch instead of fs.existsSync.
194 lines
8.8 KiB
JavaScript
194 lines
8.8 KiB
JavaScript
'use strict';
|
|
// allow-test-rule: structural 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 #1188
|
|
|
|
/**
|
|
* 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`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|