mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Round two of review findings, all four valid. `doctor` builds its own finding list and never called `checkBuildPathUnset`, so `config-build-path-unset` could not appear in the report even though doctor.md documents it. That is also the only path left once stalenessCheck is off, which is exactly when someone is looking for it. The lookup chain included `process.cwd()`, which lets an ambient invoking directory decide another project's workflow: run from workspace A with --target resolving onto workspace B, and B inherited A's buildPath ahead of the repository default. The chain is now the resolved project then the repo root, matching `checkBuildPathUnset` exactly; cwd stands in only when no project resolved at all. Two prose contradictions, both mine. new-work said to write the value "when the user says yes" and then to "record the answer either way", which reads as persist-on-yes-only and leaves the decline to be asked again next session. It now says the write always happens and the answer picks the value. The README still pointed existing projects at re-running init, which is the problem this PR exists to solve; it now names the toggle as the migration path. The workspace-isolation test earned a correction of its own: the first version passed a relative --target, which resolves against the caller's cwd and puts projectRoot back on the calling workspace, so it asserted nothing. Written with AI assistance (Claude Code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1592 lines
72 KiB
JavaScript
1592 lines
72 KiB
JavaScript
/**
|
|
* Tests for the shared context loader (PRODUCT.md / DESIGN.md resolver).
|
|
* Run with: node --test tests/load-context.test.mjs
|
|
*
|
|
* Covers the resolution order:
|
|
* 1. cwd, when canonical files are at the root
|
|
* 2. Auto-fallback to .agents/context/ then docs/
|
|
* 3. IMPECCABLE_CONTEXT_DIR env var as a power-user escape hatch (only
|
|
* consulted when the default paths come up empty)
|
|
* 4. Default to cwd when nothing is found
|
|
*
|
|
* Each test runs in its own scratch dir under os.tmpdir() so the suite stays
|
|
* independent of the project root and parallel-safe.
|
|
*/
|
|
|
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import { spawnSync, spawn } from 'node:child_process';
|
|
import http from 'node:http';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
|
|
import { loadContext, resolveContextDir, resolveProjectRoot, extractPlatform, hasVisualImplementation } from '../skill/scripts/context.mjs';
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
const SCRIPT_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'scripts', 'context.mjs');
|
|
|
|
let scratch;
|
|
let savedEnv;
|
|
|
|
beforeEach(() => {
|
|
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-loadctx-'));
|
|
savedEnv = process.env.IMPECCABLE_CONTEXT_DIR;
|
|
delete process.env.IMPECCABLE_CONTEXT_DIR;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (savedEnv === undefined) delete process.env.IMPECCABLE_CONTEXT_DIR;
|
|
else process.env.IMPECCABLE_CONTEXT_DIR = savedEnv;
|
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
});
|
|
|
|
function write(rel, body = '# placeholder\n') {
|
|
const abs = path.join(scratch, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, body);
|
|
return abs;
|
|
}
|
|
|
|
// Stage a runnable copy of context.mjs plus its whole lib/ directory.
|
|
// The bundle tests used to enumerate the helpers they needed, which turned
|
|
// every new import in context.mjs into a mysterious non-zero exit here.
|
|
// Copying the directory keeps them honest about what the real script loads.
|
|
function stageContextBundle(scriptsDir, { providerId } = {}) {
|
|
const libSrc = path.join(path.dirname(SCRIPT_PATH), 'lib');
|
|
const libDest = path.join(scriptsDir, 'lib');
|
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
fs.copyFileSync(SCRIPT_PATH, path.join(scriptsDir, 'context.mjs'));
|
|
fs.cpSync(libSrc, libDest, { recursive: true });
|
|
if (providerId) {
|
|
const providerPath = path.join(libDest, 'provider.mjs');
|
|
fs.writeFileSync(
|
|
providerPath,
|
|
fs.readFileSync(providerPath, 'utf8')
|
|
.replace("IMPECCABLE_PROVIDER_ID = 'source'", `IMPECCABLE_PROVIDER_ID = '${providerId}'`),
|
|
);
|
|
}
|
|
return path.join(scriptsDir, 'context.mjs');
|
|
}
|
|
|
|
function parseTargetSelection(stdout) {
|
|
const tail = stdout.split('TARGET_SELECTION_REQUIRED:\n')[1];
|
|
assert.ok(tail, `missing TARGET_SELECTION_REQUIRED block in:\n${stdout}`);
|
|
return JSON.parse(tail.split('\n\n')[0].trim());
|
|
}
|
|
|
|
describe('resolveContextDir', () => {
|
|
it('returns cwd when PRODUCT.md is at the root', () => {
|
|
write('PRODUCT.md');
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
|
|
it('returns cwd when DESIGN.md is at the root', () => {
|
|
write('DESIGN.md');
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
|
|
it('falls back to .agents/context/ when root is clean', () => {
|
|
write('.agents/context/PRODUCT.md');
|
|
assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context'));
|
|
});
|
|
|
|
it('falls back to docs/ when root is clean and .agents/context/ is empty', () => {
|
|
write('docs/PRODUCT.md');
|
|
assert.equal(resolveContextDir(scratch), path.join(scratch, 'docs'));
|
|
});
|
|
|
|
it('prefers .agents/context/ over docs/ when both exist', () => {
|
|
write('.agents/context/PRODUCT.md');
|
|
write('docs/PRODUCT.md');
|
|
assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context'));
|
|
});
|
|
|
|
it('prefers cwd over fallback dirs when canonical files are at the root', () => {
|
|
write('PRODUCT.md');
|
|
write('.agents/context/PRODUCT.md');
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
|
|
it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (relative path)', () => {
|
|
write('design/PRODUCT.md');
|
|
process.env.IMPECCABLE_CONTEXT_DIR = 'design';
|
|
assert.equal(resolveContextDir(scratch), path.join(scratch, 'design'));
|
|
});
|
|
|
|
it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (absolute path)', () => {
|
|
const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-elsewhere-'));
|
|
try {
|
|
process.env.IMPECCABLE_CONTEXT_DIR = elsewhere;
|
|
assert.equal(resolveContextDir(scratch), elsewhere);
|
|
} finally {
|
|
fs.rmSync(elsewhere, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('default paths win over IMPECCABLE_CONTEXT_DIR (lazy escape hatch)', () => {
|
|
write('PRODUCT.md', 'root');
|
|
write('design/PRODUCT.md', 'overridden');
|
|
process.env.IMPECCABLE_CONTEXT_DIR = 'design';
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
|
|
it('ignores empty IMPECCABLE_CONTEXT_DIR', () => {
|
|
write('PRODUCT.md');
|
|
process.env.IMPECCABLE_CONTEXT_DIR = ' ';
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
|
|
it('returns cwd when nothing is found anywhere', () => {
|
|
assert.equal(resolveContextDir(scratch), scratch);
|
|
});
|
|
});
|
|
|
|
describe('loadContext', () => {
|
|
it('reads PRODUCT.md and DESIGN.md from the root', () => {
|
|
write('PRODUCT.md', '# product content\n');
|
|
write('DESIGN.md', '# design content\n');
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.hasProduct, true);
|
|
assert.equal(ctx.hasDesign, true);
|
|
assert.match(ctx.product, /product content/);
|
|
assert.match(ctx.design, /design content/);
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
assert.equal(ctx.contextDir, scratch);
|
|
});
|
|
|
|
it('reads from .agents/context/ when the root is clean', () => {
|
|
write('.agents/context/PRODUCT.md', '# product in agents\n');
|
|
write('.agents/context/DESIGN.md', '# design in agents\n');
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.hasProduct, true);
|
|
assert.equal(ctx.hasDesign, true);
|
|
assert.match(ctx.product, /product in agents/);
|
|
assert.equal(ctx.contextDir, path.join(scratch, '.agents', 'context'));
|
|
// productPath/designPath are relative to cwd, not contextDir
|
|
assert.equal(ctx.productPath, path.join('.agents', 'context', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, path.join('.agents', 'context', 'DESIGN.md'));
|
|
});
|
|
|
|
it('reads from docs/ when .agents/context/ is empty', () => {
|
|
write('docs/PRODUCT.md', '# product in docs\n');
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.hasProduct, true);
|
|
assert.equal(ctx.contextDir, path.join(scratch, 'docs'));
|
|
assert.equal(ctx.productPath, path.join('docs', 'PRODUCT.md'));
|
|
});
|
|
});
|
|
|
|
describe('loadContext (monorepo project context)', () => {
|
|
function writeMonorepo() {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*', 'packages/*'],
|
|
}, null, 2));
|
|
write('turbo.json', JSON.stringify({ tasks: {} }));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
for (const app of ['marketing', 'dashboard', 'admin']) {
|
|
write(`apps/${app}/src/App.jsx`, `export default function App() { return ${JSON.stringify(app)}; }\n`);
|
|
}
|
|
}
|
|
|
|
it('inherits root PRODUCT.md and DESIGN.md for child apps without project context', () => {
|
|
writeMonorepo();
|
|
|
|
for (const app of ['marketing', 'dashboard', 'admin']) {
|
|
const ctx = loadContext(scratch, { targetPath: `apps/${app}/src/App.jsx` });
|
|
assert.equal(ctx.hasProduct, true);
|
|
assert.equal(ctx.hasDesign, true);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', app));
|
|
assert.equal(ctx.repoRoot, scratch);
|
|
assert.equal(ctx.isMonorepo, true);
|
|
}
|
|
});
|
|
|
|
it('lets child app context override root files independently', () => {
|
|
writeMonorepo();
|
|
write('apps/marketing/PRODUCT.md', '# Marketing product\n');
|
|
write('apps/marketing/DESIGN.md', '# Marketing design\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
|
|
const marketing = loadContext(scratch, { targetPath: 'apps/marketing/src/App.jsx' });
|
|
assert.match(marketing.product, /Marketing product/);
|
|
assert.match(marketing.design, /Marketing design/);
|
|
assert.equal(marketing.productPath, path.join('apps', 'marketing', 'PRODUCT.md'));
|
|
assert.equal(marketing.designPath, path.join('apps', 'marketing', 'DESIGN.md'));
|
|
|
|
const dashboard = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
|
|
assert.match(dashboard.product, /Dashboard product/);
|
|
assert.match(dashboard.design, /Root design/);
|
|
assert.equal(dashboard.productPath, path.join('apps', 'dashboard', 'PRODUCT.md'));
|
|
assert.equal(dashboard.designPath, 'DESIGN.md');
|
|
|
|
const admin = loadContext(scratch, { targetPath: 'apps/admin/src/App.jsx' });
|
|
assert.match(admin.product, /Root product/);
|
|
assert.match(admin.design, /Root design/);
|
|
assert.equal(admin.productPath, 'PRODUCT.md');
|
|
assert.equal(admin.designPath, 'DESIGN.md');
|
|
});
|
|
|
|
it('resolves child project roots from cwd inside a workspace', () => {
|
|
writeMonorepo();
|
|
const appDir = path.join(scratch, 'apps', 'dashboard');
|
|
const ctx = loadContext(appDir);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
assert.equal(ctx.productPath, path.join('..', '..', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, path.join('..', '..', 'DESIGN.md'));
|
|
assert.equal(resolveProjectRoot(appDir), appDir);
|
|
});
|
|
|
|
it('supports pnpm workspace patterns when resolving the active project', () => {
|
|
write('pnpm-workspace.yaml', 'packages:\n - "services/*"\n');
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('services/checkout/src/App.jsx');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'services/checkout/src/App.jsx' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'services', 'checkout'));
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
});
|
|
|
|
it('supports pnpm workspace patterns with inline comments and flow arrays', () => {
|
|
write('pnpm-workspace.yaml', 'packages: ["services/*", "tools/*"] # workspace packages\n');
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('tools/inspector/PRODUCT.md', '# Inspector product\n');
|
|
write('tools/inspector/src/App.jsx');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'tools/inspector/src/App.jsx' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'tools', 'inspector'));
|
|
assert.match(ctx.product, /Inspector product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
});
|
|
|
|
it('honors negated pnpm workspace patterns', () => {
|
|
write('pnpm-workspace.yaml', 'packages:\n - "packages/**"\n - "!packages/private/**"\n');
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('packages/private/app/src/index.ts', 'export const hidden = true;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'packages/private/app/src/index.ts' });
|
|
assert.equal(ctx.projectRoot, scratch);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
});
|
|
|
|
it('keeps unmatched child projects from being hijacked by an ancestor workspace', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Ancestor product\n');
|
|
write('side-project/PRODUCT.md', '# Side project product\n');
|
|
write('side-project/src/App.jsx', 'export default null;\n');
|
|
|
|
const ctx = loadContext(path.join(scratch, 'side-project'), { targetPath: 'src/App.jsx' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'side-project'));
|
|
assert.match(ctx.product, /Side project product/);
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
});
|
|
|
|
it('does not reuse stale project resolution after workspace markers change', () => {
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/package.json', JSON.stringify({ name: 'dashboard' }, null, 2));
|
|
write('apps/dashboard/src/App.jsx', 'export default null;\n');
|
|
|
|
const before = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
|
|
assert.equal(before.projectRoot, scratch);
|
|
assert.equal(before.isMonorepo, false);
|
|
assert.match(before.product, /Root product/);
|
|
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*'],
|
|
}, null, 2));
|
|
|
|
const after = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
|
|
assert.equal(after.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
|
assert.equal(after.isMonorepo, true);
|
|
assert.match(after.product, /Root product/);
|
|
});
|
|
|
|
it('resolves an explicit target onto a nested product context in a non-monorepo repo', () => {
|
|
write('nested/product/PRODUCT.md', '# Nested product\n');
|
|
write('nested/product/DESIGN.md', '# Nested design\n');
|
|
write('nested/product/file.ts', 'export const x = 1;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
|
assert.equal(ctx.repoRoot, scratch);
|
|
assert.match(ctx.product, /Nested product/);
|
|
assert.match(ctx.design, /Nested design/);
|
|
assert.equal(ctx.productPath, path.join('nested', 'product', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, path.join('nested', 'product', 'DESIGN.md'));
|
|
});
|
|
|
|
it('resolves a nested target whose context lives in .agents/context/', () => {
|
|
write('nested/product/.agents/context/PRODUCT.md', '# Nested product\n');
|
|
write('nested/product/file.ts', 'export const x = 1;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
|
assert.match(ctx.product, /Nested product/);
|
|
assert.equal(ctx.productPath, path.join('nested', 'product', '.agents', 'context', 'PRODUCT.md'));
|
|
});
|
|
|
|
it('resolves a nested target whose context lives in docs/', () => {
|
|
write('nested/product/docs/DESIGN.md', '# Nested design\n');
|
|
write('nested/product/file.ts', 'export const x = 1;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
|
assert.match(ctx.design, /Nested design/);
|
|
assert.equal(ctx.designPath, path.join('nested', 'product', 'docs', 'DESIGN.md'));
|
|
});
|
|
|
|
it('does not treat the root fallback context dirs as a nested product', () => {
|
|
write('.agents/context/PRODUCT.md', '# Root product\n');
|
|
write('.agents/context/notes/file.md', '# Notes\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: '.agents/context/notes/file.md' });
|
|
assert.equal(ctx.projectRoot, scratch);
|
|
assert.match(ctx.product, /Root product/);
|
|
});
|
|
|
|
it('inherits missing root context per-file for a nested target in a non-monorepo repo', () => {
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('nested/product/PRODUCT.md', '# Nested product\n');
|
|
write('nested/product/file.ts', 'export const x = 1;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
|
|
assert.match(ctx.product, /Nested product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
});
|
|
|
|
it('keeps the repo root project for targets without nearby context files', () => {
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('src/App.jsx', 'export default null;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'src/App.jsx' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
assert.equal(ctx.projectRoot, scratch);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
});
|
|
|
|
it('does not escape a nested git repo to an ancestor workspace', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['repos/*'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Outer product\n');
|
|
write('DESIGN.md', '# Outer design\n');
|
|
write('repos/standalone/.git/HEAD', 'ref: refs/heads/main\n');
|
|
write('repos/standalone/PRODUCT.md', '# Standalone product\n');
|
|
write('repos/standalone/src/App.jsx', 'export default null;\n');
|
|
|
|
const project = path.join(scratch, 'repos', 'standalone');
|
|
const ctx = loadContext(project, { targetPath: 'src/App.jsx' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
assert.equal(ctx.projectRoot, project);
|
|
assert.equal(ctx.repoRoot, project);
|
|
assert.match(ctx.product, /Standalone product/);
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
assert.equal(ctx.designPath, null);
|
|
});
|
|
|
|
it('resolves an explicit root target into a nested-git workspace child', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['repos/*'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Outer product\n');
|
|
write('DESIGN.md', '# Outer design\n');
|
|
write('repos/standalone/.git/HEAD', 'ref: refs/heads/main\n');
|
|
write('repos/standalone/PRODUCT.md', '# Standalone product\n');
|
|
write('repos/standalone/src/App.jsx', 'export default null;\n');
|
|
|
|
const project = path.join(scratch, 'repos', 'standalone');
|
|
const ctx = loadContext(scratch, { targetPath: 'repos/standalone/src/App.jsx' });
|
|
assert.equal(ctx.isMonorepo, true);
|
|
assert.equal(ctx.projectRoot, project);
|
|
assert.equal(ctx.repoRoot, scratch);
|
|
assert.match(ctx.product, /Standalone product/);
|
|
assert.match(ctx.design, /Outer design/);
|
|
assert.equal(ctx.productPath, path.join('repos', 'standalone', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
});
|
|
|
|
it('supports double-star workspace patterns by resolving the shallow child project', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['libs/**'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('libs/ui/PRODUCT.md', '# UI product\n');
|
|
write('libs/ui/src/index.ts', 'export const ui = true;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'libs/ui/src/index.ts' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'libs', 'ui'));
|
|
assert.match(ctx.product, /UI product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
assert.equal(ctx.productPath, path.join('libs', 'ui', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
});
|
|
|
|
it('supports packages/**/* workspace patterns without promoting src folders to projects', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['packages/**/*'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('packages/dashboard/PRODUCT.md', '# Dashboard package product\n');
|
|
write('packages/dashboard/src/index.ts', 'export const dashboard = true;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'packages/dashboard/src/index.ts' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'dashboard'));
|
|
assert.match(ctx.product, /Dashboard package product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
assert.equal(ctx.productPath, path.join('packages', 'dashboard', 'PRODUCT.md'));
|
|
assert.equal(ctx.designPath, 'DESIGN.md');
|
|
});
|
|
|
|
it('supports packages/** workspace patterns for nested package roots', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['packages/**'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('packages/group/app/package.json', JSON.stringify({ name: '@acme/app' }, null, 2));
|
|
write('packages/group/app/PRODUCT.md', '# Group app product\n');
|
|
write('packages/group/app/src/index.ts', 'export const app = true;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'packages/group/app/src/index.ts' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'group', 'app'));
|
|
assert.match(ctx.product, /Group app product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
});
|
|
|
|
it('does not discover dependency or generated directories as workspace candidates', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['packages/**'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('packages/ui/package.json', JSON.stringify({ name: '@acme/ui' }, null, 2));
|
|
write('packages/ui/src/index.ts', 'export const ui = true;\n');
|
|
write('packages/ui/node_modules/dep/package.json', JSON.stringify({ name: 'dep' }, null, 2));
|
|
write('packages/ui/node_modules/dep/src/index.ts', 'export const dep = true;\n');
|
|
write('packages/ui/dist/package.json', JSON.stringify({ name: '@acme/ui-dist' }, null, 2));
|
|
write('packages/ui/dist/src/index.ts', 'export const dist = true;\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
|
|
assert.deepEqual(selection.targetCandidates.map((candidate) => candidate.path), ['packages/ui']);
|
|
});
|
|
|
|
it('uses apps and packages folders as a fallback when a monorepo marker exists', () => {
|
|
write('nx.json', '{}\n');
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('packages/ui/src/index.ts');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'packages/ui/src/index.ts' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'ui'));
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.match(ctx.design, /Root design/);
|
|
});
|
|
|
|
it('does not treat turbo.json alone as a monorepo marker', () => {
|
|
write('turbo.json', JSON.stringify({ tasks: {} }));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('src/App.jsx', 'export default null;\n');
|
|
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.isMonorepo, false);
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.doesNotMatch(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
|
});
|
|
|
|
it('supports --target in the CLI', async () => {
|
|
writeMonorepo();
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n\n## Platform\n\nweb\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/src/App.jsx'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# Dashboard product/);
|
|
assert.match(res.stdout, /# DESIGN\.md\n\n# Root design/);
|
|
assert.match(res.stdout, /RESOLVED_CONTEXT:/);
|
|
assert.match(res.stdout, /"targetPath": "apps\/dashboard\/src\/App\.jsx"/);
|
|
assert.match(res.stdout, /"productPath": "apps\/dashboard\/PRODUCT\.md"/);
|
|
assert.match(res.stdout, /"designPath": "DESIGN\.md"/);
|
|
assert.doesNotMatch(res.stdout, /REGISTER:/);
|
|
});
|
|
|
|
it('asks for an app when the CLI runs from a monorepo root without selection', () => {
|
|
writeMonorepo();
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
|
|
assert.match(res.stdout, /"targetPath": null/);
|
|
assert.match(res.stdout, /"path": "apps\/dashboard"/);
|
|
assert.match(res.stdout, /"targetExample": "apps\/dashboard\/src\/App\.jsx"/);
|
|
assert.match(res.stdout, /"productStatus": "inherited"/);
|
|
assert.match(res.stdout, /"productPath": "PRODUCT\.md"/);
|
|
assert.match(res.stdout, /"designStatus": "inherited"/);
|
|
assert.match(res.stdout, /"designPath": "DESIGN\.md"/);
|
|
assert.doesNotMatch(res.stdout, /# PRODUCT\.md/);
|
|
assert.doesNotMatch(res.stdout, /# DESIGN\.md/);
|
|
assert.doesNotMatch(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
|
});
|
|
|
|
it('describes child, inherited, and mixed context sources in app selection candidates', () => {
|
|
writeMonorepo();
|
|
write('apps/admin/PRODUCT.md', '# Admin product\n');
|
|
write('apps/admin/DESIGN.md', '# Admin design\n');
|
|
write('apps/marketing/PRODUCT.md', '# Marketing product\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
const byPath = Object.fromEntries(selection.targetCandidates.map((candidate) => [candidate.path, candidate]));
|
|
|
|
assert.deepEqual(
|
|
{
|
|
productStatus: byPath['apps/admin'].productStatus,
|
|
productPath: byPath['apps/admin'].productPath,
|
|
designStatus: byPath['apps/admin'].designStatus,
|
|
designPath: byPath['apps/admin'].designPath,
|
|
},
|
|
{
|
|
productStatus: 'child',
|
|
productPath: 'apps/admin/PRODUCT.md',
|
|
designStatus: 'child',
|
|
designPath: 'apps/admin/DESIGN.md',
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
productStatus: byPath['apps/dashboard'].productStatus,
|
|
productPath: byPath['apps/dashboard'].productPath,
|
|
designStatus: byPath['apps/dashboard'].designStatus,
|
|
designPath: byPath['apps/dashboard'].designPath,
|
|
},
|
|
{
|
|
productStatus: 'inherited',
|
|
productPath: 'PRODUCT.md',
|
|
designStatus: 'inherited',
|
|
designPath: 'DESIGN.md',
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
productStatus: byPath['apps/marketing'].productStatus,
|
|
productPath: byPath['apps/marketing'].productPath,
|
|
designStatus: byPath['apps/marketing'].designStatus,
|
|
designPath: byPath['apps/marketing'].designPath,
|
|
},
|
|
{
|
|
productStatus: 'child',
|
|
productPath: 'apps/marketing/PRODUCT.md',
|
|
designStatus: 'inherited',
|
|
designPath: 'DESIGN.md',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('marks missing context files in app selection candidates', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*'],
|
|
}, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/src/App.jsx', 'export default null;\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
const dashboard = selection.targetCandidates.find((candidate) => candidate.path === 'apps/dashboard');
|
|
assert.equal(dashboard.productStatus, 'inherited');
|
|
assert.equal(dashboard.productPath, 'PRODUCT.md');
|
|
assert.equal(dashboard.designStatus, 'missing');
|
|
assert.equal(dashboard.designPath, null);
|
|
});
|
|
|
|
it('asks for app selection before init when root context is missing but child context exists', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*'],
|
|
}, null, 2));
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('apps/dashboard/src/App.jsx', 'export default null;\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
|
|
assert.match(res.stdout, /"path": "apps\/dashboard"/);
|
|
assert.doesNotMatch(res.stdout, /^NO_PRODUCT_MD:/);
|
|
});
|
|
|
|
it('excludes negated workspace packages from the selection candidates', () => {
|
|
write('pnpm-workspace.yaml', 'packages:\n - "packages/*"\n - "!packages/internal"\n');
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('packages/web/src/App.jsx', 'export default null;\n');
|
|
write('packages/internal/src/App.jsx', 'export default null;\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
const paths = selection.targetCandidates.map((candidate) => candidate.path);
|
|
assert.ok(paths.includes('packages/web'), `expected packages/web in ${JSON.stringify(paths)}`);
|
|
assert.ok(!paths.includes('packages/internal'), `packages/internal should be excluded: ${JSON.stringify(paths)}`);
|
|
});
|
|
|
|
it('does not block on target selection when the monorepo has no child apps', () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['.'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.doesNotMatch(res.stdout, /TARGET_SELECTION_REQUIRED/);
|
|
assert.match(res.stdout, /# Root product/);
|
|
});
|
|
|
|
it('lets --target . explicitly select the monorepo root', () => {
|
|
writeMonorepo();
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', '.'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# PRODUCT\.md\n\n# Root product/);
|
|
assert.match(res.stdout, /# DESIGN\.md\n\n# Root design/);
|
|
assert.match(res.stdout, /"targetPath": "\."/);
|
|
assert.doesNotMatch(res.stdout, /TARGET_SELECTION_REQUIRED/);
|
|
});
|
|
|
|
it('does not parse --help as a --target value', () => {
|
|
writeMonorepo();
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', '--help'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 1);
|
|
assert.match(res.stderr, /--target requires a path value/);
|
|
assert.equal(res.stdout, '');
|
|
});
|
|
|
|
it('uses the last --target value when duplicate target flags are provided', () => {
|
|
writeMonorepo();
|
|
write('apps/marketing/PRODUCT.md', '# Marketing product\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
|
|
const res = spawnSync(process.execPath, [
|
|
SCRIPT_PATH,
|
|
'--target', 'apps/marketing/src/App.jsx',
|
|
'--target', 'apps/dashboard/src/App.jsx',
|
|
], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0, res.stderr);
|
|
assert.match(res.stdout, /# Dashboard product/);
|
|
assert.match(res.stdout, /"targetPath": "apps\/dashboard\/src\/App\.jsx"/);
|
|
assert.doesNotMatch(res.stdout, /# Marketing product/);
|
|
});
|
|
|
|
it('warns when --target names a missing path in a monorepo', () => {
|
|
writeMonorepo();
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/routes/pricing'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
|
|
assert.equal(res.status, 0, res.stderr);
|
|
assert.match(res.stdout, /RESOLVED_CONTEXT:/);
|
|
assert.match(res.stdout, /"targetExists": false/);
|
|
assert.match(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
|
});
|
|
|
|
it('asks for app selection even when root PRODUCT.md is absent', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*'],
|
|
}, null, 2));
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('apps/dashboard/src/App.jsx', 'export default null;\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
|
|
assert.match(res.stdout, /"path": "apps\/dashboard"/);
|
|
assert.doesNotMatch(res.stdout, /^NO_PRODUCT_MD:/);
|
|
});
|
|
});
|
|
|
|
describe('loadContext (impeccable projectRoots config)', () => {
|
|
function writeSkinsConfig(extra = {}) {
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'], ...extra }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
}
|
|
|
|
it('treats a config-declared context root as a monorepo with no package-manager files', () => {
|
|
writeSkinsConfig();
|
|
write('docs/design/skins/neon-seoul/DESIGN.md', '# Neon Seoul design\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/neon-seoul' });
|
|
assert.equal(ctx.isMonorepo, true);
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'docs', 'design', 'skins', 'neon-seoul'));
|
|
assert.equal(ctx.repoRoot, scratch);
|
|
// The skin uses its own DESIGN.md and inherits the root PRODUCT.md per file.
|
|
assert.match(ctx.design, /Neon Seoul design/);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.equal(ctx.designPath, path.join('docs', 'design', 'skins', 'neon-seoul', 'DESIGN.md'));
|
|
assert.equal(ctx.productPath, 'PRODUCT.md');
|
|
});
|
|
|
|
it('resolves a config-declared child from cwd inside the folder', () => {
|
|
writeSkinsConfig();
|
|
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
|
|
|
const skinDir = path.join(scratch, 'docs', 'design', 'skins', 'marble');
|
|
const ctx = loadContext(skinDir);
|
|
assert.equal(ctx.isMonorepo, true);
|
|
assert.equal(ctx.projectRoot, skinDir);
|
|
assert.match(ctx.design, /Marble design/);
|
|
assert.match(ctx.product, /Root product/);
|
|
});
|
|
|
|
it('extends shared projectRoots with config.local.json', () => {
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
|
|
write('.impeccable/config.local.json', JSON.stringify({ projectRoots: ['experiments/*'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('experiments/wip/DESIGN.md', '# WIP design\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'experiments/wip' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'experiments', 'wip'));
|
|
assert.match(ctx.design, /WIP design/);
|
|
assert.match(ctx.product, /Root product/);
|
|
});
|
|
|
|
it('does not treat an .impeccable config without projectRoots as a monorepo', () => {
|
|
write('.impeccable/config.json', JSON.stringify({ hook: { consent: 'accepted' } }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/marble' });
|
|
assert.equal(ctx.isMonorepo, false);
|
|
});
|
|
|
|
it('asks for app selection from a config-declared monorepo root', () => {
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('DESIGN.md', '# Root design\n');
|
|
write('docs/design/skins/neon-seoul/DESIGN.md', '# Neon Seoul\n');
|
|
write('docs/design/skins/marble/DESIGN.md', '# Marble\n');
|
|
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
const paths = selection.targetCandidates.map((candidate) => candidate.path).sort();
|
|
assert.deepEqual(paths, ['docs/design/skins/marble', 'docs/design/skins/neon-seoul']);
|
|
});
|
|
|
|
// Composition with package-manager workspaces: a path matched by any
|
|
// projectRoots pattern (positive or negated) is governed by the impeccable
|
|
// config alone; package-manager patterns fill in the paths it does not match.
|
|
describe('composition with package-manager workspaces', () => {
|
|
function selectionPaths() {
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0, res.stderr);
|
|
const selection = parseTargetSelection(res.stdout);
|
|
return selection.targetCandidates.map((candidate) => candidate.path).sort();
|
|
}
|
|
|
|
it('lets an impeccable negation exclude a package-manager workspace', () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['!apps/internal'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('apps/internal/PRODUCT.md', '# Internal product\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'apps/internal' });
|
|
assert.equal(ctx.isMonorepo, true);
|
|
assert.equal(ctx.projectRoot, scratch);
|
|
assert.match(ctx.product, /Root product/);
|
|
assert.deepEqual(selectionPaths(), ['apps/dashboard']);
|
|
});
|
|
|
|
it('keeps a package-manager negation scoped to its own source', () => {
|
|
write('package.json', JSON.stringify({
|
|
private: true,
|
|
workspaces: ['apps/*', '!docs/design/skins/marble'],
|
|
}, null, 2));
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/marble' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'docs', 'design', 'skins', 'marble'));
|
|
assert.match(ctx.design, /Marble design/);
|
|
assert.deepEqual(selectionPaths(), ['apps/dashboard', 'docs/design/skins/marble']);
|
|
});
|
|
|
|
it('gives a broad impeccable pattern the boundary over a deeper package workspace', () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['apps/web/packages/ui'] }, null, 2));
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['apps/*'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/web/PRODUCT.md', '# Web product\n');
|
|
write('apps/web/packages/ui/PRODUCT.md', '# UI product\n');
|
|
write('apps/web/packages/ui/src/Button.jsx', 'export default null;\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'apps/web/packages/ui/src/Button.jsx' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'web'));
|
|
assert.match(ctx.product, /Web product/);
|
|
// The subsumed package workspace must not appear as its own pick:
|
|
// choosing it would silently resolve to apps/web.
|
|
assert.deepEqual(selectionPaths(), ['apps/web']);
|
|
});
|
|
|
|
it('falls through to package workspaces for paths impeccable does not match', () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'apps/dashboard' });
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
|
assert.match(ctx.product, /Dashboard product/);
|
|
});
|
|
|
|
it('resolves other workspaces normally when impeccable config only negates', () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
|
|
write('.impeccable/config.json', JSON.stringify({ projectRoots: ['!apps/internal'] }, null, 2));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
|
write('apps/internal/PRODUCT.md', '# Internal product\n');
|
|
|
|
const ctx = loadContext(scratch, { targetPath: 'apps/dashboard' });
|
|
assert.equal(ctx.isMonorepo, true);
|
|
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
|
assert.match(ctx.product, /Dashboard product/);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('loadContext (IMPECCABLE_CONTEXT_DIR escape hatch)', () => {
|
|
it('reads from the override path when defaults are empty', () => {
|
|
write('design/PRODUCT.md', '# overridden product\n');
|
|
write('design/DESIGN.md', '# overridden design\n');
|
|
process.env.IMPECCABLE_CONTEXT_DIR = 'design';
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.hasProduct, true);
|
|
assert.equal(ctx.hasDesign, true);
|
|
assert.match(ctx.product, /overridden product/);
|
|
assert.equal(ctx.contextDir, path.join(scratch, 'design'));
|
|
});
|
|
|
|
it('does not override defaults when both exist (lazy escape hatch)', () => {
|
|
write('PRODUCT.md', '# root product\n');
|
|
write('design/PRODUCT.md', '# overridden product\n');
|
|
process.env.IMPECCABLE_CONTEXT_DIR = 'design';
|
|
const ctx = loadContext(scratch);
|
|
assert.match(ctx.product, /root product/);
|
|
assert.equal(ctx.contextDir, scratch);
|
|
});
|
|
|
|
it('reports a missing override directory as no-context, not as a crash', () => {
|
|
process.env.IMPECCABLE_CONTEXT_DIR = 'no/such/dir';
|
|
const ctx = loadContext(scratch);
|
|
assert.equal(ctx.hasProduct, false);
|
|
assert.equal(ctx.hasDesign, false);
|
|
assert.equal(ctx.product, null);
|
|
assert.equal(ctx.design, null);
|
|
assert.equal(ctx.contextDir, path.resolve(scratch, 'no/such/dir'));
|
|
});
|
|
});
|
|
|
|
describe('extractPlatform', () => {
|
|
it('returns null when the product is empty or platform-less', () => {
|
|
assert.equal(extractPlatform(null), null);
|
|
assert.equal(extractPlatform('# P\n\nno platform here\n'), null);
|
|
});
|
|
|
|
it('reads web / ios / android / adaptive case-insensitively', () => {
|
|
assert.equal(extractPlatform('## Platform\n\nweb\n'), 'web');
|
|
assert.equal(extractPlatform('## Platform\n\nios\n'), 'ios');
|
|
assert.equal(extractPlatform('## platform\n\nANDROID\n'), 'android');
|
|
assert.equal(extractPlatform('## Platform\n\nAdaptive\n'), 'adaptive');
|
|
});
|
|
|
|
it('reads a line naming both native targets as adaptive', () => {
|
|
assert.equal(extractPlatform('## Platform\n\nios, android\n'), 'adaptive');
|
|
assert.equal(extractPlatform('## Platform\n\nandroid and ios\n'), 'adaptive');
|
|
assert.equal(extractPlatform('## Platform\n\nios/android\n'), 'adaptive');
|
|
});
|
|
|
|
it('does not read prose mentioning both targets as adaptive', () => {
|
|
// Negations and explanations must fall through to the unrecognized-value
|
|
// warning, never silently classify as cross-platform native.
|
|
assert.equal(extractPlatform('## Platform\n\nweb only, not ios or android\n'), null);
|
|
assert.equal(extractPlatform('## Platform\n\nios first, android later this year\n'), null);
|
|
});
|
|
|
|
it('returns null for an unrecognized value', () => {
|
|
assert.equal(extractPlatform('## Platform\n\ndesktop\n'), null);
|
|
assert.equal(extractPlatform('## Platform\n\nflutter\n'), null);
|
|
});
|
|
|
|
it('ignores a near-miss heading and reads the real one', () => {
|
|
// `## Platform notes` must not be mistaken for the `## Platform` field.
|
|
const product = '## Platform notes\n\nsome prose here\n\n## Platform\n\nios\n';
|
|
assert.equal(extractPlatform(product), 'ios');
|
|
});
|
|
|
|
it('reads the first non-empty line after the heading', () => {
|
|
assert.equal(extractPlatform('## Platform\n\n\nios\n'), 'ios');
|
|
});
|
|
|
|
it('treats an empty section followed by another heading as absent', () => {
|
|
// An empty `## Platform` must not swallow the next heading as its value
|
|
// (which would surface a nonsense "value `## Product Purpose` is not
|
|
// recognized" warning from the CLI).
|
|
assert.equal(extractPlatform('## Platform\n\n## Product Purpose\n\nAn app.\n'), null);
|
|
});
|
|
});
|
|
|
|
describe('context.mjs CLI', () => {
|
|
it('emits NO_PRODUCT_MD directive when no PRODUCT.md is found', async () => {
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^NO_PRODUCT_MD:/);
|
|
assert.match(res.stdout, /reference\/init\.md/);
|
|
assert.match(res.stdout, /structured simulated-user interview/);
|
|
assert.match(res.stdout, /PRODUCT_INIT_REQUIRED:/);
|
|
});
|
|
|
|
it('prints a PRODUCT.md markdown block when only PRODUCT.md exists', async () => {
|
|
write('PRODUCT.md', '# Acme\n\nbody\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^# PRODUCT\.md/);
|
|
assert.match(res.stdout, /# Acme/);
|
|
assert.equal(res.stdout.includes('# DESIGN.md'), false);
|
|
// Directives are appended after `---`; missing visual authority now
|
|
// routes to new-work rather than back through product init.
|
|
assert.match(res.stdout, /\n---\n\n/);
|
|
assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/);
|
|
assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
|
assert.match(res.stdout, /detect\.mjs --json <changed targets>/);
|
|
});
|
|
|
|
// The build-path preference rides the unified config beside hook and
|
|
// detector settings. The local file wins because whether a machine can
|
|
// generate images is a property of that machine, not of the committed
|
|
// default the rest of the team shares.
|
|
describe('BUILD_PATH_DEFAULT', () => {
|
|
const run = () => spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
|
|
it('reports a recorded preference from the shared config', () => {
|
|
write('PRODUCT.md', '# Acme\n');
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
|
|
assert.match(run().stdout, /BUILD_PATH_DEFAULT: code \(from \.impeccable\/config\.json\)/);
|
|
});
|
|
|
|
it('lets the gitignored local config win over the committed one', () => {
|
|
write('PRODUCT.md', '# Acme\n');
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
|
|
write('.impeccable/config.local.json', JSON.stringify({ buildPath: 'code' }));
|
|
assert.match(run().stdout, /BUILD_PATH_DEFAULT: code \(from \.impeccable\/config\.local\.json\)/);
|
|
});
|
|
|
|
it('stays silent when nothing is recorded, leaving new-work its own default', () => {
|
|
write('PRODUCT.md', '# Acme\n');
|
|
assert.equal(run().stdout.includes('BUILD_PATH_DEFAULT'), false);
|
|
});
|
|
|
|
it('stays silent on a value nothing reads rather than guessing at it', () => {
|
|
write('PRODUCT.md', '# Acme\n');
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'code-first' }));
|
|
assert.equal(run().stdout.includes('BUILD_PATH_DEFAULT'), false);
|
|
});
|
|
|
|
// A monorepo commits the preference once at the repo root for every app in
|
|
// it. Reading only projectRoot left the staleness finding (which does read
|
|
// both) silent while the directive never named the value.
|
|
describe('in a monorepo', () => {
|
|
const writeWorkspace = () => {
|
|
write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }));
|
|
write('turbo.json', JSON.stringify({ tasks: {} }));
|
|
write('PRODUCT.md', '# Root product\n');
|
|
write('apps/dashboard/src/App.jsx', 'export default function App() { return "d"; }\n');
|
|
};
|
|
const runFromWorkspace = () => spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/src/App.jsx'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
|
|
it('falls back to the repo-root value for a workspace that sets none', () => {
|
|
writeWorkspace();
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
|
|
assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: code/);
|
|
});
|
|
|
|
it('lets the workspace override the repo root, nearest root first', () => {
|
|
writeWorkspace();
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
|
|
write('apps/dashboard/.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
|
|
assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: comp/);
|
|
});
|
|
|
|
// Running from one workspace while targeting another must not hand the
|
|
// target the caller's preference. The invoking directory is not evidence
|
|
// about the project being worked on.
|
|
it('does not let the invoking workspace lend its value to the target', () => {
|
|
writeWorkspace();
|
|
write('apps/marketing/src/App.jsx', 'export default function App() { return "m"; }\n');
|
|
write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
|
|
write('apps/marketing/.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
|
|
// Absolute, so the target actually resolves onto dashboard. A relative
|
|
// path would be read against the caller's cwd, which resolves the
|
|
// project back to marketing and tests nothing.
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', path.join(scratch, 'apps', 'dashboard', 'src', 'App.jsx')], {
|
|
cwd: path.join(scratch, 'apps', 'marketing'),
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
// The repo-root default, not marketing's comp.
|
|
assert.match(res.stdout, /BUILD_PATH_DEFAULT: code/);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('keeps the manual-detector directive out of early context when the current provider hook is active', () => {
|
|
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
|
|
stageContextBundle(scripts, { providerId: 'codex' });
|
|
|
|
const project = path.join(scratch, 'project');
|
|
fs.mkdirSync(path.join(project, '.codex'), { recursive: true });
|
|
fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
|
|
fs.writeFileSync(path.join(project, '.codex', 'hooks.json'), JSON.stringify({
|
|
hooks: { Stop: [{ hooks: [{ command: 'node .agents/skills/impeccable/scripts/hook.mjs' }] }] },
|
|
}));
|
|
|
|
const res = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
|
|
cwd: project,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0, res.stderr);
|
|
assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
|
|
|
fs.mkdirSync(path.join(project, '.impeccable'), { recursive: true });
|
|
fs.writeFileSync(path.join(project, '.impeccable', 'config.json'), JSON.stringify({ hook: { enabled: false } }));
|
|
const disabled = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
|
|
cwd: project,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(disabled.status, 0, disabled.stderr);
|
|
assert.match(disabled.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
|
assert.match(disabled.stdout, /detect\.mjs --json <changed targets>/);
|
|
});
|
|
|
|
it('adds no detector directive when a per-edit-only hook is active', () => {
|
|
const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
|
|
stageContextBundle(scripts, { providerId: 'cursor' });
|
|
|
|
const project = path.join(scratch, 'project');
|
|
fs.mkdirSync(path.join(project, '.cursor'), { recursive: true });
|
|
fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
|
|
fs.writeFileSync(path.join(project, '.cursor', 'hooks.json'), JSON.stringify({
|
|
hooks: { preToolUse: [{ command: 'node .cursor/skills/impeccable/scripts/hook-before-edit.mjs' }] },
|
|
}));
|
|
|
|
const res = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
|
|
cwd: project,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0, res.stderr);
|
|
assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
|
|
assert.doesNotMatch(res.stdout, /detect\.mjs --json <changed targets>/);
|
|
});
|
|
|
|
it('treats tokenized code as incumbent design authority when DESIGN.md is missing', () => {
|
|
write('PRODUCT.md', '# Acme\n');
|
|
write('src/app.css', ':root { --color-brand: red; --color-surface: white; --color-text: black; }\nbody { font-family: system-ui; background: var(--color-surface); color: var(--color-text); }\n');
|
|
assert.equal(hasVisualImplementation(scratch), true);
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /INCUMBENT_WORLD_UNDOCUMENTED:/);
|
|
assert.match(res.stdout, /For shape or a new-surface\/redesign request, load reference\/new-work\.md/);
|
|
assert.doesNotMatch(res.stdout, /WORLD_DISCOVERY_REQUIRED:/);
|
|
assert.match(res.stdout, /"hasVisualImplementation": true/);
|
|
});
|
|
|
|
it('does not mistake the empty Astro eval scaffold for an incumbent identity', () => {
|
|
write('src/styles/global.css', '@import "tailwindcss";\n');
|
|
write('src/pages/index.astro', `---\nimport "../styles/global.css";\n---\n<!doctype html><html><head><title>Eval Workspace</title></head><body></body></html>\n`);
|
|
assert.equal(hasVisualImplementation(scratch), false);
|
|
});
|
|
|
|
it('recognizes one substantive authored Astro surface', () => {
|
|
write('src/pages/index.astro', `<main class="shell"><h1>Field notes for the night shift</h1><p>Specific authored content.</p></main>\n<style>\n:root { --ink: #111; --paper: #fff; --accent: #c40; }\n.shell { color: var(--ink); background-color: var(--paper); border-color: var(--accent); font-family: serif; padding: 4rem; min-height: 100vh; }\n</style>\n`);
|
|
assert.equal(hasVisualImplementation(scratch), true);
|
|
});
|
|
|
|
it('does not let irrelevant or vendored files exhaust or satisfy the visual scan', () => {
|
|
for (let i = 0; i < 300; i++) write(`src/data/item-${String(i).padStart(3, '0')}.txt`, 'not visual\n');
|
|
write('public/vendor/framework.min.css', ':root { --a: 1; --b: 2; --c: 3; } body { color: red; background: blue; border-color: green; font-family: sans-serif; }\n');
|
|
write('styles/z-theme.css', ':root { --brand: #124; --surface: #fff; --text: #111; } main { color: var(--text); background-color: var(--surface); border-color: var(--brand); }\n');
|
|
assert.equal(hasVisualImplementation(scratch), true);
|
|
});
|
|
|
|
it('routes new surfaces through init but keeps narrow refinements non-blocking when visual code exists without PRODUCT.md', () => {
|
|
write('styles/theme.css', ':root { --brand: #124; --surface: #fff; --text: #111; }\nmain { color: var(--text); background-color: var(--surface); border-color: var(--brand); }\n');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^NO_PRODUCT_MD:/);
|
|
assert.match(res.stdout, /EXISTING_VISUAL_SYSTEM:/);
|
|
assert.match(res.stdout, /BUILD_INIT_REQUIRED:/);
|
|
assert.match(res.stdout, /SCOPED_EXISTING_ALLOWED:/);
|
|
assert.match(res.stdout, /proceed without blocking/);
|
|
assert.match(res.stdout, /For `init`, `teach`, `shape`, or any request to create a new surface/);
|
|
assert.match(res.stdout, /For a redesign\/rebrand.*old look only as evidence and anti-reference/s);
|
|
assert.doesNotMatch(res.stdout, /WORLD_DISCOVERY_REQUIRED:/);
|
|
});
|
|
|
|
it('prints DESIGN.md even when PRODUCT.md is missing', () => {
|
|
// The skill resumes after init writes PRODUCT.md without rerunning this
|
|
// script, so a DESIGN.md withheld from the no-PRODUCT.md branch is never
|
|
// seen at all. Emit incumbent visual authority on both branches.
|
|
write('DESIGN.md', '# Acme design\n\nUNIQUE_DESIGN_MARKER\n');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^NO_PRODUCT_MD:/);
|
|
assert.match(res.stdout, /# DESIGN\.md\n\n# Acme design/);
|
|
assert.match(res.stdout, /UNIQUE_DESIGN_MARKER/);
|
|
});
|
|
|
|
it('concatenates PRODUCT.md and DESIGN.md with a --- separator', async () => {
|
|
write('PRODUCT.md', '# Acme product\n');
|
|
write('DESIGN.md', '# Acme design\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^# PRODUCT\.md/);
|
|
assert.match(res.stdout, /\n---\n/);
|
|
assert.match(res.stdout, /# DESIGN\.md\n\n# Acme design/);
|
|
assert.equal(res.stdout.includes('WORLD_DISCOVERY_REQUIRED:'), false);
|
|
});
|
|
|
|
it('loads the only persisted surface brief as current task context', () => {
|
|
write('PRODUCT.md', '# Acme product\n');
|
|
write('DESIGN.md', '# Acme design\n');
|
|
write('.impeccable/surfaces/src-pages-pricing-astro.md', `---
|
|
version: 1
|
|
slug: "src-pages-pricing-astro"
|
|
primary_target: "src/pages/pricing.astro"
|
|
related_targets: []
|
|
---
|
|
|
|
# Surface brief: Pricing
|
|
|
|
## Product strategy
|
|
Make plan tradeoffs legible before asking for a trial.
|
|
`);
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# SURFACE BRIEF \(\.impeccable\/surfaces\/src-pages-pricing-astro\.md\)/);
|
|
assert.match(res.stdout, /Make plan tradeoffs legible/);
|
|
assert.match(res.stdout, /"surfaceBriefReason": "only-brief"/);
|
|
});
|
|
|
|
it('selects a surface brief by an exact primary or related target', () => {
|
|
write('PRODUCT.md', '# Acme product\n');
|
|
write('DESIGN.md', '# Acme design\n');
|
|
write('src/pages/pricing.astro', '<main>Pricing</main>\n');
|
|
write('.impeccable/surfaces/src-pages-pricing-astro.md', `---
|
|
version: 1
|
|
slug: "src-pages-pricing-astro"
|
|
primary_target: "src/pages/pricing.astro"
|
|
related_targets: ["src/components/PricingTable.astro"]
|
|
---
|
|
|
|
# Surface brief: Pricing
|
|
|
|
PRICING_STRATEGY_SENTINEL
|
|
`);
|
|
write('.impeccable/surfaces/src-pages-home-astro.md', `---
|
|
version: 1
|
|
slug: "src-pages-home-astro"
|
|
primary_target: "src/pages/home.astro"
|
|
related_targets: []
|
|
---
|
|
|
|
# Surface brief: Home
|
|
|
|
HOME_STRATEGY_SENTINEL
|
|
`);
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'src/pages/pricing.astro'], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /PRICING_STRATEGY_SENTINEL/);
|
|
assert.doesNotMatch(res.stdout, /HOME_STRATEGY_SENTINEL/);
|
|
});
|
|
|
|
it('lists candidates instead of guessing when several surface briefs exist', () => {
|
|
write('PRODUCT.md', '# Acme product\n');
|
|
write('DESIGN.md', '# Acme design\n');
|
|
for (const [slug, target] of [
|
|
['src-pages-pricing-astro', 'src/pages/pricing.astro'],
|
|
['src-pages-home-astro', 'src/pages/home.astro'],
|
|
]) {
|
|
write(`.impeccable/surfaces/${slug}.md`, `---
|
|
version: 1
|
|
slug: "${slug}"
|
|
primary_target: "${target}"
|
|
related_targets: []
|
|
---
|
|
|
|
# Surface brief: ${slug}
|
|
`);
|
|
}
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], {
|
|
cwd: scratch,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /SURFACE_CONTEXT_AVAILABLE:/);
|
|
assert.match(res.stdout, /src\/pages\/pricing\.astro/);
|
|
assert.match(res.stdout, /src\/pages\/home\.astro/);
|
|
assert.doesNotMatch(res.stdout, /# SURFACE BRIEF \(/);
|
|
});
|
|
|
|
it('reads from a fallback dir when cwd is clean', async () => {
|
|
write('.agents/context/PRODUCT.md', '# fallback product\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /^# PRODUCT\.md/);
|
|
assert.match(res.stdout, /# fallback product/);
|
|
});
|
|
|
|
it('ignores a legacy Register field because visitor mode is task-scoped', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Register\n\nbrand\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.doesNotMatch(res.stdout, /REGISTER:/);
|
|
assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/);
|
|
});
|
|
|
|
it('loads the native platform reference for an ios project', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\nios\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: IOS \(reference\/ios\.md\)/);
|
|
assert.match(res.stdout, /Apple Human Interface Guidelines|iOS/i);
|
|
assert.doesNotMatch(res.stdout, /NEXT STEP:.*reference\/ios\.md/);
|
|
});
|
|
|
|
it('loads both native platform references for an adaptive project', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\nadaptive\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: IOS \(reference\/ios\.md\)/);
|
|
assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: ANDROID \(reference\/android\.md\)/);
|
|
});
|
|
|
|
it('appends no native platform directive for a web project', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\nweb\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('This project targets'), false);
|
|
assert.equal(res.stdout.includes('reference/ios.md'), false);
|
|
});
|
|
|
|
it('loads the native platform reference for an android project', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\nandroid\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: ANDROID \(reference\/android\.md\)/);
|
|
assert.match(res.stdout, /Material Design|Android/i);
|
|
});
|
|
|
|
it('warns on an unrecognized platform value instead of silently defaulting to web', async () => {
|
|
// The likeliest misconfiguration is a toolchain name where the target
|
|
// belongs. Silent fallback to web would give web guidance to the exact
|
|
// projects that tried to declare themselves native.
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\nflutter\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /WARNING: PRODUCT\.md's `## Platform` value `flutter` is not recognized/);
|
|
assert.match(res.stdout, /treating the project as `web`/);
|
|
assert.equal(res.stdout.includes('This project targets'), false);
|
|
});
|
|
|
|
it('emits no warning for an empty Platform section', async () => {
|
|
write('PRODUCT.md', '# Acme\n\n## Platform\n\n## Users\n\nAnglers.\n');
|
|
const { spawnSync } = await import('node:child_process');
|
|
const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('WARNING: PRODUCT.md'), false);
|
|
assert.equal(res.stdout.includes('This project targets'), false);
|
|
});
|
|
});
|
|
|
|
describe('context.mjs update check', () => {
|
|
// The script reads its own version from a sibling SKILL.md (resolved via
|
|
// import.meta.url, not cwd). The source tree has no SKILL.md, so we copy the
|
|
// script into a scratch skill dir with a controlled version and run that.
|
|
// Local version is pinned to 1.0.0; "newer" = 2.0.0, "older" = 0.0.1.
|
|
const LOCAL_VERSION = '1.0.0';
|
|
|
|
const cachePath = () => path.join(scratch, 'update-check.json');
|
|
|
|
function setup(cacheObj, { disable = false, host } = {}) {
|
|
const skillScript = stageContextBundle(path.join(scratch, 'skill', 'scripts'));
|
|
fs.writeFileSync(
|
|
path.join(scratch, 'skill', 'SKILL.md'),
|
|
`---\nname: impeccable\nversion: ${LOCAL_VERSION}\n---\n\nbody\n`,
|
|
);
|
|
fs.writeFileSync(cachePath(), JSON.stringify(cacheObj));
|
|
const project = path.join(scratch, 'project');
|
|
fs.mkdirSync(project, { recursive: true });
|
|
fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
|
|
const env = {
|
|
...process.env,
|
|
IMPECCABLE_UPDATE_CACHE: cachePath(),
|
|
IMPECCABLE_NO_UPDATE_CHECK: disable ? '1' : '',
|
|
// This suite asserts on the update directive alone. Staleness findings
|
|
// are a separate directive with their own tests, and leaving the check on
|
|
// would also write notice state into the developer's home dir.
|
|
IMPECCABLE_NO_STALENESS_CHECK: '1',
|
|
...(host ? { IMPECCABLE_UPDATE_HOST: host } : {}),
|
|
};
|
|
return { skillScript, project, env };
|
|
}
|
|
|
|
// A fresh cache (lastCheck = now) skips the network poll, so cache-driven
|
|
// tests stay synchronous and hermetic.
|
|
function run(cacheObj, opts) {
|
|
const { skillScript, project, env } = setup(cacheObj, opts);
|
|
return spawnSync(process.execPath, [skillScript], { cwd: project, encoding: 'utf8', env });
|
|
}
|
|
|
|
// Async variant for the live-fetch tests: the stub server runs in THIS
|
|
// process, so the runner must not block the event loop (spawnSync would
|
|
// deadlock the loopback connection). spawn keeps the loop serving.
|
|
function runAsync(cacheObj, opts) {
|
|
const { skillScript, project, env } = setup(cacheObj, opts);
|
|
return new Promise((resolve) => {
|
|
const proc = spawn(process.execPath, [skillScript], { cwd: project, env });
|
|
let stdout = '';
|
|
proc.stdout.on('data', (d) => (stdout += d.toString()));
|
|
proc.on('exit', (status) => resolve({ status, stdout }));
|
|
});
|
|
}
|
|
|
|
function readCache() {
|
|
return JSON.parse(fs.readFileSync(cachePath(), 'utf8'));
|
|
}
|
|
|
|
it('appends UPDATE_AVAILABLE when the cached latest version is newer', () => {
|
|
const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' });
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /UPDATE_AVAILABLE: A newer Impeccable skill is available/);
|
|
assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/);
|
|
assert.match(res.stdout, /npx impeccable update/);
|
|
// It must come after the real context, never replace it.
|
|
assert.match(res.stdout, /^# PRODUCT\.md/);
|
|
});
|
|
|
|
// The directive used to say "ask once" and "if they agree, run it" while also
|
|
// saying to continue without waiting. Nothing gated the run on an answer that
|
|
// could not arrive, so the command read as the next step. It now forbids
|
|
// running in this turn outright, whatever the answer.
|
|
it('forbids running the update in the same turn, on any answer', () => {
|
|
const { stdout } = run({ lastCheck: Date.now(), latestVersion: '2.0.0' });
|
|
assert.match(stdout, /Do not run `npx impeccable update` in this turn, whatever the user answers/);
|
|
assert.match(stdout, /only after the user has asked for it in their own words/);
|
|
// The conditional that made the command look reachable must be gone.
|
|
assert.equal(/If they agree, run/.test(stdout), false);
|
|
});
|
|
|
|
it('stays silent when the cached latest version is not newer', () => {
|
|
const res = run({ lastCheck: Date.now(), latestVersion: '0.0.1' });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
|
|
});
|
|
|
|
it('does not re-surface a version notified within the last week', () => {
|
|
const res = run({
|
|
lastCheck: Date.now(),
|
|
latestVersion: '2.0.0',
|
|
notifiedVersion: '2.0.0',
|
|
notifiedAt: Date.now(),
|
|
});
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
|
|
});
|
|
|
|
it('respects IMPECCABLE_NO_UPDATE_CHECK', () => {
|
|
const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' }, { disable: true });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
|
|
});
|
|
|
|
// ─── live fetch path (against a localhost stub, never the real site) ──────
|
|
function startStub(body, { status = 200 } = {}) {
|
|
return new Promise((resolve) => {
|
|
const srv = http.createServer((req, res) => {
|
|
res.statusCode = status;
|
|
res.setHeader('content-type', 'application/json');
|
|
res.end(typeof body === 'string' ? body : JSON.stringify(body));
|
|
});
|
|
srv.listen(0, '127.0.0.1', () => resolve({ srv, host: `http://127.0.0.1:${srv.address().port}` }));
|
|
});
|
|
}
|
|
|
|
it('polls /api/version over the network and caches a newer version', async () => {
|
|
const { srv, host } = await startStub({ skills: '2.0.0' });
|
|
try {
|
|
const res = await runAsync({}, { host }); // empty cache forces the poll
|
|
assert.equal(res.status, 0);
|
|
assert.match(res.stdout, /UPDATE_AVAILABLE/);
|
|
assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/);
|
|
const cache = readCache();
|
|
assert.equal(cache.latestVersion, '2.0.0');
|
|
assert.equal(typeof cache.lastCheck, 'number');
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
it('stays silent when the network reports a same-or-older version', async () => {
|
|
const { srv, host } = await startStub({ skills: '1.0.0' });
|
|
try {
|
|
const res = await runAsync({}, { host });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
|
|
// The poll still happened, so lastCheck is stamped to throttle the next.
|
|
assert.equal(typeof readCache().lastCheck, 'number');
|
|
} finally {
|
|
srv.close();
|
|
}
|
|
});
|
|
|
|
it('fails silent and stamps lastCheck when the endpoint is unreachable', async () => {
|
|
// Bind then immediately close to obtain a port nothing is listening on.
|
|
const { srv, host } = await startStub({ skills: '2.0.0' });
|
|
await new Promise((r) => srv.close(r));
|
|
const res = run({}, { host });
|
|
assert.equal(res.status, 0);
|
|
assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
|
|
assert.match(res.stdout, /^# PRODUCT\.md/); // core output is unaffected
|
|
const cache = readCache();
|
|
assert.equal(typeof cache.lastCheck, 'number'); // stamped so we don't re-poll every boot
|
|
assert.equal(cache.latestVersion, undefined); // nothing learned
|
|
});
|
|
});
|