Merge pull request #263 from open-gsd/fix/187-delete-parity-tests
chore(#187): delete all *-parity.test.cjs files
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* For every `agents/gsd-*.md`, assert its agent name appears as a row
|
||||
* in docs/INVENTORY.md's Agents table. AGENTS.md card presence is NOT
|
||||
* enforced — that file is allowed to be a curated subset (primary
|
||||
* cards + advanced stubs).
|
||||
*
|
||||
* Related: docs readiness refresh, lane-12 recommendation.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const AGENTS_DIR = path.join(ROOT, 'agents');
|
||||
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
|
||||
|
||||
const agentFiles = fs
|
||||
.readdirSync(AGENTS_DIR)
|
||||
.filter((f) => /^gsd-.*\.md$/.test(f));
|
||||
|
||||
function mentionedInInventoryAgents(name) {
|
||||
// Row form in the Agents table: `| agent-name | role | ... |`
|
||||
// The Agents table uses the raw name (no code fence) in column 1.
|
||||
const rowRe = new RegExp(`^\\|\\s*${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*\\|`, 'm');
|
||||
return rowRe.test(INVENTORY_MD);
|
||||
}
|
||||
|
||||
describe('every shipped agent has a row in INVENTORY.md', () => {
|
||||
for (const file of agentFiles) {
|
||||
const name = file.replace(/\.md$/, '');
|
||||
test(name, () => {
|
||||
assert.ok(
|
||||
mentionedInInventoryAgents(name),
|
||||
`agents/${file} has no row in docs/INVENTORY.md Agents table — add one`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Regression test for #2687 — loadConfig must not emit "unknown config key"
|
||||
* warnings for keys that are registered in DYNAMIC_KEY_PATTERNS (e.g. review,
|
||||
* model_profile_overrides, claude_md_assembly). These keys were absent from
|
||||
* the hand-maintained KNOWN_TOP_LEVEL set in core.cjs, causing false-positive
|
||||
* warnings on every read.
|
||||
*
|
||||
* We trigger loadConfig via `resolve-model` (which calls loadConfig internally)
|
||||
* and assert that stderr is EMPTY on success — a typed-IR equivalent of
|
||||
* "no warning was emitted" without grepping for specific warning text. The
|
||||
* absence of any stderr output IS the contract: loadConfig prints nothing
|
||||
* to stderr when every top-level key in config.json is recognized.
|
||||
*
|
||||
* Migrated from substring `.includes('unknown config key')` text matching
|
||||
* to typed empty-stderr assertions per #2974.
|
||||
*/
|
||||
|
||||
const { describe, test, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { createTempProject, cleanup, TOOLS_PATH } = require('./helpers.cjs');
|
||||
|
||||
const TEST_ENV_BASE = {
|
||||
GSD_SESSION_KEY: '',
|
||||
CODEX_THREAD_ID: '',
|
||||
CLAUDE_SESSION_ID: '',
|
||||
CLAUDE_CODE_SSE_PORT: '',
|
||||
OPENCODE_SESSION_ID: '',
|
||||
GEMINI_SESSION_ID: '',
|
||||
CURSOR_SESSION_ID: '',
|
||||
WINDSURF_SESSION_ID: '',
|
||||
TERM_SESSION_ID: '',
|
||||
WT_SESSION: '',
|
||||
TMUX_PANE: '',
|
||||
ZELLIJ_SESSION_NAME: '',
|
||||
TTY: '',
|
||||
SSH_TTY: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Run gsd-tools and return { stdout, stderr, status }.
|
||||
* Captures stderr even when the process exits 0 (unlike runGsdTools which only
|
||||
* surfaces stderr via result.error on non-zero exit).
|
||||
*/
|
||||
function runWithStderr(args, cwd) {
|
||||
const result = spawnSync(process.execPath, [TOOLS_PATH, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, ...TEST_ENV_BASE },
|
||||
});
|
||||
return {
|
||||
stdout: result.stdout || '',
|
||||
stderr: result.stderr || '',
|
||||
status: result.status,
|
||||
};
|
||||
}
|
||||
|
||||
describe('bug-2687 — no warning for dynamic-pattern containers in loadConfig', () => {
|
||||
let tmpDir;
|
||||
|
||||
afterEach(() => {
|
||||
if (tmpDir) cleanup(tmpDir);
|
||||
tmpDir = null;
|
||||
});
|
||||
|
||||
test('review — loadConfig emits no warning when config.json contains review key', () => {
|
||||
tmpDir = createTempProject('gsd-2687-review-');
|
||||
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ review: { models: { 'test-cli': 'test-command' } } }, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
// resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check
|
||||
const result = runWithStderr(['resolve-model', 'planner'], tmpDir);
|
||||
|
||||
assert.equal(
|
||||
result.stderr.trim(),
|
||||
'',
|
||||
`loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}`
|
||||
);
|
||||
});
|
||||
|
||||
test('model_profile_overrides — loadConfig emits no warning when config.json contains model_profile_overrides key', () => {
|
||||
tmpDir = createTempProject('gsd-2687-mpo-');
|
||||
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ model_profile_overrides: { codex: { sonnet: 'claude-sonnet-4' } } }, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
// resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check
|
||||
const result = runWithStderr(['resolve-model', 'planner'], tmpDir);
|
||||
|
||||
assert.equal(
|
||||
result.stderr.trim(),
|
||||
'',
|
||||
`loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}`
|
||||
);
|
||||
});
|
||||
|
||||
test('claude_md_assembly — loadConfig emits no warning when config.json contains claude_md_assembly key', () => {
|
||||
tmpDir = createTempProject('gsd-2687-cma-');
|
||||
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ claude_md_assembly: { mode: 'custom', blocks: { identity: true } } }, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
// resolve-model calls loadConfig internally, triggering the KNOWN_TOP_LEVEL check
|
||||
const result = runWithStderr(['resolve-model', 'planner'], tmpDir);
|
||||
|
||||
assert.equal(
|
||||
result.stderr.trim(),
|
||||
'',
|
||||
`loadConfig must emit no stderr output for valid dynamic-pattern keys (#2687) — got: ${result.stderr}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* For every `get-shit-done/bin/lib/*.cjs`, assert the module name
|
||||
* appears as a row in docs/INVENTORY.md's CLI Modules table.
|
||||
* docs/CLI-TOOLS.md is allowed to describe a subset (narrative doc);
|
||||
* INVENTORY.md is the authoritative module roster.
|
||||
*
|
||||
* Related: docs readiness refresh, lane-12 recommendation.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LIB_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib');
|
||||
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
|
||||
|
||||
const moduleFiles = fs
|
||||
.readdirSync(LIB_DIR)
|
||||
.filter((f) => f.endsWith('.cjs'));
|
||||
|
||||
function mentionedInInventoryCliModules(filename) {
|
||||
// Row form: | `filename.cjs` | responsibility |
|
||||
const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm');
|
||||
return rowRe.test(INVENTORY_MD);
|
||||
}
|
||||
|
||||
describe('every CLI module has a row in INVENTORY.md', () => {
|
||||
for (const file of moduleFiles) {
|
||||
test(file, () => {
|
||||
assert.ok(
|
||||
mentionedInInventoryCliModules(file),
|
||||
`get-shit-done/bin/lib/${file} has no row in docs/INVENTORY.md CLI Modules table — add one`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* For every `commands/gsd/*.md`, assert its `/gsd-<name>` slash command
|
||||
* appears either (a) as a `### /gsd-...` heading in docs/COMMANDS.md or
|
||||
* (b) as a row in docs/INVENTORY.md's Commands table. At least one of
|
||||
* these must be true so every shipped command is reachable from docs.
|
||||
*
|
||||
* The slug is derived from the `name:` frontmatter field (e.g. `gsd-workflow`)
|
||||
* rather than the filename (e.g. `ns-workflow.md`), so the test stays aligned
|
||||
* with the actual deployed command token even when the file has a legacy name.
|
||||
*
|
||||
* Related: docs readiness refresh, lane-12 recommendation.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
|
||||
const COMMANDS_MD = fs.readFileSync(path.join(ROOT, 'docs', 'COMMANDS.md'), 'utf8');
|
||||
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
|
||||
|
||||
const commandFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
|
||||
|
||||
/**
|
||||
* Extract the slug from the `name:` frontmatter field.
|
||||
* Accepts both `gsd:slug` and `gsd-slug` forms.
|
||||
* Returns the slug portion only (e.g. `workflow`, `plan-phase`).
|
||||
* Throws if frontmatter is missing or malformed.
|
||||
*/
|
||||
function parseSlugFromFrontmatter(content, filePath) {
|
||||
// allow-test-rule: validating YAML frontmatter delimiter structure, not application source
|
||||
if (!content.startsWith('---')) {
|
||||
throw new Error('commands-doc-parity: missing YAML frontmatter in ' + filePath);
|
||||
}
|
||||
const closingIdx = content.indexOf('\n---', 3);
|
||||
if (closingIdx < 0) {
|
||||
throw new Error('commands-doc-parity: unclosed YAML frontmatter in ' + filePath);
|
||||
}
|
||||
const frontmatter = content.slice(0, closingIdx);
|
||||
const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:-])([a-z0-9][a-z0-9-]*)"?\s*$/m);
|
||||
if (!nameMatch) {
|
||||
throw new Error('commands-doc-parity: could not extract slug from name: field in ' + filePath);
|
||||
}
|
||||
return nameMatch[2];
|
||||
}
|
||||
|
||||
function mentionedInCommandsDoc(slug) {
|
||||
// Match a heading like: ### /gsd-<slug> or ## /gsd-<slug>
|
||||
const headingRe = new RegExp(`^#{2,4}\\s+\\\`?/gsd-${slug}\\\`?(?:[\\s(]|$)`, 'm');
|
||||
return headingRe.test(COMMANDS_MD);
|
||||
}
|
||||
|
||||
function mentionedInInventory(slug) {
|
||||
// Match a row like: | `/gsd-<slug>` | ... |
|
||||
const rowRe = new RegExp(`\\|\\s*\\\`/gsd-${slug}\\\`\\s*\\|`, 'm');
|
||||
return rowRe.test(INVENTORY_MD);
|
||||
}
|
||||
|
||||
describe('every shipped command is documented somewhere', () => {
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(COMMANDS_DIR, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const slug = parseSlugFromFrontmatter(content, filePath);
|
||||
test(`/gsd-${slug}`, () => {
|
||||
const inCommandsDoc = mentionedInCommandsDoc(slug);
|
||||
const inInventory = mentionedInInventory(slug);
|
||||
assert.ok(
|
||||
inCommandsDoc || inInventory,
|
||||
`commands/gsd/${file} (name: gsd-${slug}) is not mentioned in docs/COMMANDS.md (as a heading) or docs/INVENTORY.md (as a Commands row) — add a one-line entry to at least one`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Reads .md/.json/.yml product files whose deployed text IS what the
|
||||
// runtime loads — testing text content tests the deployed contract.
|
||||
|
||||
/**
|
||||
* Asserts every exact-match key in config-schema.cjs appears at least once
|
||||
* in docs/CONFIGURATION.md. A key present in the validator but absent from
|
||||
* the docs means users can set it but have no guidance. A key in the docs but
|
||||
* absent from the validator means config-set silently rejects it.
|
||||
*
|
||||
* Dynamic patterns (agent_skills.*, review.models.*, features.*) are excluded
|
||||
* from this check — they are documented by namespace in CONFIGURATION.md.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const { VALID_CONFIG_KEYS } = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
const CONFIGURATION_MD = fs.readFileSync(path.join(ROOT, 'docs', 'CONFIGURATION.md'), 'utf8');
|
||||
|
||||
// Reserved for future internal keys; workflow._auto_chain_active removed from VALID_CONFIG_KEYS (#2530).
|
||||
const INTERNAL_KEYS = new Set();
|
||||
|
||||
test('every key in VALID_CONFIG_KEYS is documented in docs/CONFIGURATION.md', () => {
|
||||
const undocumented = [];
|
||||
for (const key of VALID_CONFIG_KEYS) {
|
||||
if (INTERNAL_KEYS.has(key)) continue;
|
||||
if (!CONFIGURATION_MD.includes('`' + key + '`')) {
|
||||
undocumented.push(key);
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
undocumented,
|
||||
[],
|
||||
'Keys in VALID_CONFIG_KEYS with no mention in docs/CONFIGURATION.md:\n' +
|
||||
undocumented.map((k) => ' ' + k).join('\n') +
|
||||
'\nAdd a row in the appropriate section of docs/CONFIGURATION.md.',
|
||||
);
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: structural source assertion — verifies that sdk/src/query/config-schema.ts
|
||||
// is a re-export shell (no inline literals) rather than a redeclared Set. Runtime/IR comparison
|
||||
// cannot distinguish a re-export from a redeclared `new Set([...])` with identical contents
|
||||
// because both would deep-equal the manifest set; only source-shape inspection catches drift
|
||||
// back to inline literals. See test "SDK config-schema.ts re-exports from configuration module".
|
||||
|
||||
/**
|
||||
* Manifest-as-source-of-truth guard (Phase 2, Cycle 5, #3536).
|
||||
*
|
||||
* Prior to Cycle 5, the CJS and SDK schema files each had independent inline
|
||||
* literals. This test existed to prevent drift between them. After Cycle 5,
|
||||
* BOTH sides derive their data from sdk/shared/config-schema.manifest.json,
|
||||
* so there is nothing to drift — but the guard still serves a purpose:
|
||||
*
|
||||
* 1. Confirm that VALID_CONFIG_KEYS loaded at runtime from config-schema.cjs
|
||||
* exactly matches the manifest's validKeys array.
|
||||
* 2. Confirm that VALID_CONFIG_KEYS exported from sdk/src/query/config-schema.ts
|
||||
* (via sdk/dist/) equals the same manifest.
|
||||
* 3. Confirm that DYNAMIC_KEY_PATTERNS from config-schema.cjs have .source
|
||||
* fields matching the manifest's dynamicKeyPatterns.
|
||||
* 4. Confirm RUNTIME_STATE_KEYS from config-schema.cjs matches the manifest.
|
||||
*
|
||||
* This ensures neither side has accidentally disconnected from the manifest
|
||||
* (e.g. reverted to inline literals or switched to a different data source).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const MANIFEST_PATH = path.join(ROOT, 'sdk', 'shared', 'config-schema.manifest.json');
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
||||
const manifestValidKeys = new Set(manifest.validKeys);
|
||||
const manifestRuntimeKeys = new Set(manifest.runtimeStateKeys);
|
||||
const manifestPatternSources = manifest.dynamicKeyPatterns.map((p) => p.source);
|
||||
|
||||
const {
|
||||
VALID_CONFIG_KEYS: CJS_KEYS,
|
||||
RUNTIME_STATE_KEYS: CJS_RUNTIME_KEYS,
|
||||
DYNAMIC_KEY_PATTERNS: CJS_PATTERNS,
|
||||
} = require('../get-shit-done/bin/lib/config-schema.cjs');
|
||||
|
||||
// ─── CJS side: verify manifest-sourced values ─────────────────────────────
|
||||
|
||||
test('CJS VALID_CONFIG_KEYS matches manifest validKeys exactly', () => {
|
||||
const missingInCjs = [...manifestValidKeys].filter((k) => !CJS_KEYS.has(k));
|
||||
const extraInCjs = [...CJS_KEYS].filter((k) => !manifestValidKeys.has(k));
|
||||
assert.deepStrictEqual(
|
||||
missingInCjs,
|
||||
[],
|
||||
'Manifest keys missing from CJS VALID_CONFIG_KEYS:\n' +
|
||||
missingInCjs.map((k) => ' ' + k).join('\n'),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
extraInCjs,
|
||||
[],
|
||||
'CJS VALID_CONFIG_KEYS has keys not in manifest:\n' +
|
||||
extraInCjs.map((k) => ' ' + k).join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
test('CJS RUNTIME_STATE_KEYS matches manifest runtimeStateKeys exactly', () => {
|
||||
const missingInCjs = [...manifestRuntimeKeys].filter((k) => !CJS_RUNTIME_KEYS.has(k));
|
||||
const extraInCjs = [...CJS_RUNTIME_KEYS].filter((k) => !manifestRuntimeKeys.has(k));
|
||||
assert.deepStrictEqual(missingInCjs, [], 'Manifest runtime keys missing from CJS RUNTIME_STATE_KEYS');
|
||||
assert.deepStrictEqual(extraInCjs, [], 'CJS RUNTIME_STATE_KEYS has keys not in manifest');
|
||||
});
|
||||
|
||||
test('CJS DYNAMIC_KEY_PATTERNS .source fields match manifest dynamicKeyPatterns', () => {
|
||||
assert.strictEqual(
|
||||
CJS_PATTERNS.length,
|
||||
manifestPatternSources.length,
|
||||
`CJS has ${CJS_PATTERNS.length} patterns but manifest has ${manifestPatternSources.length}`,
|
||||
);
|
||||
for (let i = 0; i < manifestPatternSources.length; i++) {
|
||||
const expected = manifestPatternSources[i];
|
||||
const actual = CJS_PATTERNS[i].source;
|
||||
assert.strictEqual(
|
||||
actual,
|
||||
expected,
|
||||
`CJS pattern[${i}].source mismatch: expected "${expected}", got "${actual}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── SDK side: verify config-schema.ts re-exports from configuration module ─
|
||||
|
||||
test('SDK config-schema.ts re-exports from config module (not inline literals)', () => {
|
||||
const SDK_SCHEMA_PATH = path.join(ROOT, 'sdk', 'src', 'query', 'config-schema.ts');
|
||||
const src = fs.readFileSync(SDK_SCHEMA_PATH, 'utf8');
|
||||
|
||||
// After Cycle 5, the file must NOT contain inline key literals.
|
||||
// It should import/re-export from '../config/index.js'.
|
||||
assert.ok(
|
||||
src.includes("from '../config/index.js'"),
|
||||
'sdk/src/query/config-schema.ts must re-export from ../config/index.js (not inline literals)',
|
||||
);
|
||||
|
||||
// Must NOT contain a standalone new Set([...]) block with key literals.
|
||||
// A minimal check: the file should not define VALID_CONFIG_KEYS as a Set literal.
|
||||
assert.ok(
|
||||
!src.includes("new Set([\n 'mode'") && !src.includes("new Set(['mode'"),
|
||||
'sdk/src/query/config-schema.ts must not contain an inline VALID_CONFIG_KEYS Set literal',
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Cross-check: CJS equals SDK via manifest ─────────────────────────────
|
||||
|
||||
test('#2653 — CJS and SDK both source from the same manifest (set equality via manifest)', () => {
|
||||
// Since both sides derive from sdk/shared/config-schema.manifest.json,
|
||||
// the CJS runtime set must equal the manifest set (verified above).
|
||||
// This test is the explicit statement of the invariant for audit purposes.
|
||||
const cjsKeysSorted = [...CJS_KEYS].sort();
|
||||
const manifestKeysSorted = [...manifestValidKeys].sort();
|
||||
assert.deepStrictEqual(
|
||||
cjsKeysSorted,
|
||||
manifestKeysSorted,
|
||||
'CJS VALID_CONFIG_KEYS must equal manifest validKeys — both sides source from the manifest',
|
||||
);
|
||||
});
|
||||
|
||||
test('#2653 — CJS DYNAMIC_KEY_PATTERNS test functions work correctly', () => {
|
||||
// Verify that each pattern's test() function (reconstructed from manifest source)
|
||||
// correctly accepts sample keys and rejects non-matching ones.
|
||||
const samples = [
|
||||
['agent_skills.gsd-planner', 0],
|
||||
['review.models.claude', 1],
|
||||
['features.some_feature', 2],
|
||||
['claude_md_assembly.blocks.intro', 3],
|
||||
['model_profile_overrides.codex.opus', 4],
|
||||
['models.planning', 5],
|
||||
['dynamic_routing.enabled', 6],
|
||||
['model_overrides.my-agent', 7],
|
||||
['review.max_prompt_tokens_per_reviewer.ollama', 8],
|
||||
];
|
||||
for (const [key, idx] of samples) {
|
||||
assert.ok(
|
||||
CJS_PATTERNS[idx].test(key),
|
||||
`CJS pattern[${idx}] must accept "${key}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* For every `hooks/*.(js|sh)`, assert the hook filename appears as a
|
||||
* row in docs/INVENTORY.md's Hooks table. docs/ARCHITECTURE.md's hook
|
||||
* table is allowed to lag — INVENTORY.md is authoritative.
|
||||
*
|
||||
* Related: docs readiness refresh, lane-12 recommendation.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const HOOKS_DIR = path.join(ROOT, 'hooks');
|
||||
const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
|
||||
|
||||
const hookFiles = fs
|
||||
.readdirSync(HOOKS_DIR)
|
||||
.filter((f) => /\.(js|sh)$/.test(f));
|
||||
|
||||
function mentionedInInventoryHooks(filename) {
|
||||
// Row form: | `filename.js` | event | purpose |
|
||||
const rowRe = new RegExp(`\\|\\s*\\\`${filename.replace(/\./g, '\\.')}\\\`\\s*\\|`, 'm');
|
||||
return rowRe.test(INVENTORY_MD);
|
||||
}
|
||||
|
||||
describe('every shipped hook has a row in INVENTORY.md', () => {
|
||||
for (const file of hookFiles) {
|
||||
test(file, () => {
|
||||
assert.ok(
|
||||
mentionedInInventoryHooks(file),
|
||||
`hooks/${file} has no row in docs/INVENTORY.md Hooks table — add one`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Reverse-direction parity: every row declared in docs/INVENTORY.md must
|
||||
* resolve to a real file on the filesystem. Complements the forward tests
|
||||
* (actual ⊆ INVENTORY) with the reverse direction (INVENTORY ⊆ actual),
|
||||
* catching ghost entries left behind when artifacts are deleted or renamed.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const INVENTORY = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'utf8');
|
||||
|
||||
/** Extract the text of a named top-level section (## Header ... next ##). */
|
||||
function section(header) {
|
||||
const start = INVENTORY.indexOf('## ' + header);
|
||||
if (start === -1) return '';
|
||||
const next = INVENTORY.indexOf('\n## ', start + 1);
|
||||
return next === -1 ? INVENTORY.slice(start) : INVENTORY.slice(start, next);
|
||||
}
|
||||
|
||||
/** Extract backtick-quoted filenames from column-1 table cells. */
|
||||
function backtickNames(text, ext) {
|
||||
const re = new RegExp('\\|\\s*`([^`]+\\.' + ext + ')`\\s*\\|', 'gm');
|
||||
const names = [];
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) names.push(m[1]);
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Extract agent names from `| gsd-xxx | ...` rows (no backticks). */
|
||||
function agentNames(text) {
|
||||
const re = /^\|\s*(gsd-[a-z0-9-]+)\s*\|/gm;
|
||||
const names = [];
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) names.push(m[1]);
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Extract relative source paths from markdown links in Commands section. */
|
||||
function commandSourcePaths(text) {
|
||||
const re = /\[commands\/gsd\/[^\]]+\]\(\.\.\/(commands\/gsd\/[^)]+)\)/g;
|
||||
const paths = [];
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) paths.push(m[1]);
|
||||
return paths;
|
||||
}
|
||||
|
||||
describe('INVENTORY.md declared artifacts exist on the filesystem (ghost-entry guard)', () => {
|
||||
describe('Agents', () => {
|
||||
const names = agentNames(section('Agents'));
|
||||
for (const name of names) {
|
||||
test(name, () => {
|
||||
const p = path.join(ROOT, 'agents', name + '.md');
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares agent "' + name + '" but agents/' + name + '.md does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Commands', () => {
|
||||
const paths = commandSourcePaths(section('Commands'));
|
||||
for (const rel of paths) {
|
||||
test(rel, () => {
|
||||
const p = path.join(ROOT, rel);
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares source "' + rel + '" but the file does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Workflows', () => {
|
||||
const names = backtickNames(section('Workflows'), 'md');
|
||||
for (const name of names) {
|
||||
test(name, () => {
|
||||
const p = path.join(ROOT, 'get-shit-done', 'workflows', name);
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares workflow "' + name + '" but get-shit-done/workflows/' + name + ' does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('References', () => {
|
||||
const names = backtickNames(section('References'), 'md');
|
||||
for (const name of names) {
|
||||
test(name, () => {
|
||||
const p = path.join(ROOT, 'get-shit-done', 'references', name);
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares reference "' + name + '" but get-shit-done/references/' + name + ' does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('CLI Modules', () => {
|
||||
const names = backtickNames(section('CLI Modules'), 'cjs');
|
||||
for (const name of names) {
|
||||
test(name, () => {
|
||||
const p = path.join(ROOT, 'get-shit-done', 'bin', 'lib', name);
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares CLI module "' + name + '" but get-shit-done/bin/lib/' + name + ' does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Hooks', () => {
|
||||
const jsNames = backtickNames(section('Hooks'), 'js');
|
||||
const shNames = backtickNames(section('Hooks'), 'sh');
|
||||
for (const name of [...jsNames, ...shNames]) {
|
||||
test(name, () => {
|
||||
const p = path.join(ROOT, 'hooks', name);
|
||||
assert.ok(
|
||||
fs.existsSync(p),
|
||||
'INVENTORY.md declares hook "' + name + '" but hooks/' + name + ' does not exist — remove the ghost row or restore the file',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user