v1.91.1.0 fix: harden Impeccable plugin discovery (#2978)

* fix(design-detect): find impeccable installed as a Claude Code plugin

The design-detector probe only ever checked <root>/<SKILL_ROOTS>/skills/impeccable/,
never the Claude Code plugin-cache layout
(<root>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/).
A plugin-installed impeccable was therefore invisible: IMPECCABLE_SKILL stayed
absent, the launcher was never found (so the NOT_CACHED run hint never fired),
and its sibling engine was never considered.

Add a plugin-cache walk alongside the existing SKILL_ROOTS walk, sharing the
same presence/launcher/repo-local-exclusion/sibling-engine logic via an
extracted checkSkillDir() helper so both paths stay behaviorally identical.

Fixes #2838

* refactor(design-detect): consolidate newestSemverDir onto safeReaddir

Both did the identical try/catch-around-readdirSync; newestSemverDir now
reuses the new safeReaddir helper instead of duplicating it.

* fix: harden Impeccable plugin discovery and regression fixtures

* test: supply eval mode to the integrated detector callback adapter

---------

Co-authored-by: Som Samantray <som.samantray@gmail.com>
This commit is contained in:
Garry Tan
2026-09-25 14:32:12 -04:00
committed by GitHub
co-authored by Som Samantray
parent 7b534d3e90
commit 2a113ae7e6
18 changed files with 938 additions and 40 deletions
+12
View File
@@ -37,6 +37,15 @@ import {
LARGE_REPO_FILE_THRESHOLD,
} from "../lib/code-intelligence";
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => execFileSync("git", args, {
cwd: repo, encoding: "utf8", timeout: 10_000,
}).trim();
expect(git("config", "--get", "remote.origin.url")).toBe(url);
git("config", "--local", `url.${url}.insteadOf`, url);
expect(git("remote", "get-url", "origin")).toBe(url);
}
describe("capability matrix", () => {
test("every provider advertises the four required capabilities", () => {
for (const p of [new GbrainProvider(), new SourcebotProvider(), new GraphifyProvider()]) {
@@ -402,6 +411,7 @@ describe("consent unification — deny tier wins (R1)", () => {
const git = (...a: string[]) => execFileSync("git", a, { cwd: repo, timeout: 30_000 });
git("init", "-q", ".");
git("remote", "add", "origin", url);
isolateGitRemote(repo, url);
return repo;
}
const POLICY_BIN = path.join(import.meta.dir, "..", "bin", "gstack-gbrain-repo-policy");
@@ -500,6 +510,7 @@ describe("read-only repo policy blocks write-class CLI index (R2)", () => {
fs.mkdirSync(repo, { recursive: true });
execFileSync("git", ["init", "-q", "."], { cwd: repo });
execFileSync("git", ["remote", "add", "origin", URL], { cwd: repo });
isolateGitRemote(repo, URL);
setProvider("gbrain", env);
setConsent(repo, true, env);
execFileSync(POLICY_BIN, ["set", URL, "read-only"], { env, encoding: "utf-8" });
@@ -702,6 +713,7 @@ describe("CLI search consent gate (gbrain provider, honest refusal message)", ()
fs.mkdirSync(repo, { recursive: true });
execFileSync("git", ["init", "-q", "."], { cwd: repo });
execFileSync("git", ["remote", "add", "origin", URL], { cwd: repo });
isolateGitRemote(repo, URL);
env = { ...process.env, GSTACK_HOME: home, PATH: `${shimDir}:${process.env.PATH}` };
setProvider("gbrain", env);
setRoot("gbrain", repo, env);
+1 -1
View File
@@ -58,7 +58,7 @@ async function exercise(modes: Mode[], retention?: 'directory' | 'run-id' | 'bot
},
};
const args = {
ROOT, fs: localFs, os: { tmpdir: () => scratch }, path, expect, CAPTURE_MS, CAPTURE_LONG_MS,
ROOT, fs: localFs, os: { tmpdir: () => scratch }, path, expect, CAPTURE_MS, CAPTURE_LONG_MS, evalsEnabled: true,
process: { ...process, env: { ...process.env, ...env } }, resolveEvalModel,
getProjectEvalDir: () => artifactRoot,
console: { ...console, log: (...args: any[]) => notices.push(args.join(' ')), error: (...args: any[]) => notices.push(args.join(' ')) },
+1 -1
View File
@@ -29,7 +29,7 @@ describe('AO completed manual DX handoff preserves report freshness',()=>{
expect(E2E_TOUCHFILES[owner]).toContain('test/fixtures/dx-manual-handoff-ao.json');
}
const arrays=[...Object.values(E2E_TOUCHFILES),...Object.values(LLM_JUDGE_TOUCHFILES),GLOBAL_TOUCHFILES];
expect(arrays).toHaveLength(234);
expect(arrays).toHaveLength(235);
for(const values of arrays)for(let i=0;i<values.length;i++)expect(typeof values[i]).toBe('string');
});
test('exact owned report precedes navigation only, with the current Exit gate recognized',()=>{
+2
View File
@@ -8,6 +8,7 @@ describe('fake impeccable engine selection', () => {
'design-html-slop-gate',
'design-review-detector-shim',
'design-review-detector-shim-dom',
'design-review-plugin-handoff',
'review-design-lite',
];
@@ -28,6 +29,7 @@ describe('fake impeccable engine selection', () => {
['design-html-slop-gate', 'periodic'],
['design-review-detector-shim', 'gate'],
['design-review-detector-shim-dom', 'gate'],
['design-review-plugin-handoff', 'gate'],
['review-design-lite', 'periodic'],
]);
});
+31
View File
@@ -25,6 +25,17 @@ const BIN = path.join(ROOT, 'bin', 'gstack-gbrain-repo-policy');
let tmpHome: string;
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
git('config', '--local', `url.${url}.insteadOf`, url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
}
function run(args: string[], opts: { env?: Record<string, string> } = {}) {
const res = spawnSync(BIN, args, {
env: { ...process.env, GSTACK_HOME: tmpHome, ...(opts.env || {}) },
@@ -54,6 +65,25 @@ afterEach(() => {
fs.rmSync(tmpHome, { recursive: true, force: true });
});
test('fixture origin isolation overrides a controlled URL rewrite without changing the stored remote', () => {
const repo = path.join(tmpHome, 'repo');
fs.mkdirSync(repo);
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
const url = 'https://fixture.invalid/org/repo.git';
git('init', '-q');
git('remote', 'add', 'origin', url);
git('config', '--local', 'url.https://mirror.invalid/.insteadOf', 'https://fixture.invalid/');
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
expect(git('remote', 'get-url', 'origin')).toBe('https://mirror.invalid/org/repo.git');
isolateGitRemote(repo, url);
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
});
describe('normalize', () => {
test('strips https:// and .git', () => {
const r = run(['normalize', 'https://github.com/foo/bar.git']);
@@ -293,6 +323,7 @@ describe('gstack-gbrain-sync code stage honors the repo policy (#2140 sync path)
spawnSync('git', args, { cwd: repoDir, encoding: 'utf-8', timeout: 30_000 });
git('init', '-q', '.');
git('remote', 'add', 'origin', REPO_URL);
isolateGitRemote(repoDir, REPO_URL);
fs.writeFileSync(path.join(repoDir, 'README.md'), 'fixture\n');
git('add', '-A');
git('-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'fixture');
+61
View File
@@ -0,0 +1,61 @@
import { test, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import { SENTINEL } from '../lib/design-detect-contract';
const detector = fileURLToPath(new URL('../bin/gstack-design-detect.ts', import.meta.url));
test.each([
{ versions: ['4.3.1', '4.10.0'], selected: '4.10.0', custom: false },
{ versions: ['4.3.1', '4.10.0'], selected: '4.10.0', custom: true },
{ versions: ['4.3.1-beta.1', '4.3.1'], selected: '4.3.1', custom: false },
{ versions: ['99.0.0-', '4.3.1'], selected: '4.3.1', custom: false },
{ versions: ['a1b2c3d4'], selected: 'a1b2c3d4', custom: true },
{ versions: ['unknown'], selected: 'unknown', custom: false },
])('native plugin discovery %j', ({ versions, selected, custom }) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plugin-native-'));
const home = path.join(root, 'home');
const cwd = path.join(root, 'project');
const config = custom ? path.join(root, 'custom config') : path.join(home, '.claude');
for (const dir of [home, cwd, config]) fs.mkdirSync(dir, { recursive: true });
const env: Record<string, string> = {
HOME: home, USERPROFILE: home, PATH: path.dirname(process.execPath),
GSTACK_HOME: path.join(root, 'gstack'), IMPECCABLE_HOME: path.join(root, 'engine-cache'),
};
for (const key of ['SystemRoot', 'SYSTEMROOT', 'TEMP', 'TMP']) if (process.env[key]) env[key] = process.env[key]!;
if (custom) env.CLAUDE_CONFIG_DIR = config;
try {
const launchers = new Map<string, string>();
for (const version of versions) {
const skill = path.join(config, 'plugins', 'cache', 'market', 'impeccable', version, 'skills', 'impeccable');
fs.mkdirSync(path.join(skill, 'scripts'), { recursive: true });
fs.writeFileSync(path.join(skill, 'SKILL.md'), '# fixture\n');
const launcher = path.join(skill, 'scripts', 'impeccable');
fs.writeFileSync(launcher, 'This fixture is not an executable.\n');
launchers.set(version, launcher);
}
if (custom) {
const stale = path.join(home, '.claude', 'skills', 'impeccable');
fs.mkdirSync(stale, { recursive: true });
fs.writeFileSync(path.join(stale, 'SKILL.md'), '# stale default profile\n');
}
const probe = () => spawnSync(process.execPath, ['--no-env-file', 'run', detector, 'probe'], {
cwd, env, encoding: 'utf-8', timeout: 30_000,
});
const found = probe();
expect(found.status).toBe(0);
const expectedLauncher = custom
? path.join(fs.realpathSync(config), path.relative(config, launchers.get(selected)!))
: launchers.get(selected)!;
expect(found.stdout.split('\n')[0]).toBe(`${SENTINEL.NOT_CACHED}: ${expectedLauncher}`);
expect(found.stdout).toContain(`${SENTINEL.SKILL}: present`);
expect(found.stdout).not.toContain(`${SENTINEL.READY}:`);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
const removed = probe();
expect(removed.status).toBe(0);
expect(removed.stdout).toContain(`${SENTINEL.SKILL}: absent`);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
+414
View File
@@ -342,6 +342,420 @@ describe('probe', () => {
});
});
describe('plugin-cache selection and trust', () => {
function fixture(check: (home: string, config: string) => void) {
const home = fs.mkdtempSync(path.join(SANDBOX, 'plugin-home-'));
try { check(home, path.join(home, '.claude')); }
finally { fs.rmSync(home, { recursive: true, force: true }); }
}
function plugin(config: string, version: string, opts: { engine?: boolean; engineVersion?: string; marketplace?: string; name?: string } = {}) {
const skill = path.join(config, 'plugins', 'cache', opts.marketplace ?? 'impeccable', opts.name ?? 'impeccable', version, 'skills', 'impeccable');
const scripts = path.join(skill, 'scripts');
fs.mkdirSync(scripts, { recursive: true });
fs.writeFileSync(path.join(skill, 'SKILL.md'), '# impeccable\n');
const marker = path.join(config, 'launcher-ran');
const launcher = path.join(scripts, 'impeccable');
fs.writeFileSync(launcher, `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\n`);
fs.chmodSync(launcher, 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), `${opts.engineVersion ?? '0.1.3'}\n`);
const engine = path.join(scripts, 'bin', `${process.platform}-${process.arch}`, POSIX ? 'impeccable' : 'impeccable.exe');
if (opts.engine) {
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
}
return { skill, scripts, launcher, engine, marker };
}
test.each([
[['4.3.1', '4.10.0'], '4.10.0'],
[['4.10.0', '4.3.1'], '4.10.0'],
[['4.3.1-beta.1', '4.3.1'], '4.3.1'],
[['4.4.0-beta.2', '4.4.0-beta.10'], '4.4.0-beta.10'],
[['unknown', 'a1b2c3d4'], 'a1b2c3d4'],
[['a1b2c3d4'], 'a1b2c3d4'],
[['unknown'], 'unknown'],
[['unknown', '4.3.1'], '4.3.1'],
[['v4.3.1', '4.3.1'], '4.3.1'],
[['99.0.0-', '4.3.1'], '4.3.1'],
[['99.0.0-beta..1', '4.3.1'], '4.3.1'],
[['99.0.0-01', '4.3.1'], '4.3.1'],
[['99.0.0+build..1', '4.3.1'], '4.3.1'],
[['99.0.0-'], '99.0.0-'],
[['4.3.1+build.2', '4.3.1+build.1'], '4.3.1+build.1'],
] as [string[], string][])('orders plugin versions %j and selects %s', (versions, expected) => {
fixture((home, config) => {
const installs = new Map(versions.map(v => [v, plugin(config, v)]));
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(r.code).toBe(0);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${installs.get(expected)!.launcher}`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
if (!/^[v\d]/.test(expected)) expect(r.out).toContain('opaque version; recency unknown');
for (const install of installs.values()) expect(fs.existsSync(install.marker)).toBe(false);
});
});
test.skipIf(!POSIX)('selected plugin engine and version match; scans produce handoff data without running launchers', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { engine: true, engineVersion: '0.1.3' });
const current = plugin(config, '4.10.0', { engine: true, engineVersion: '0.1.5' });
const env = { HOME: home };
const p = run(['probe'], { env });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(current.engine)}`);
expect(p.out).toContain(`${SENTINEL.ENGINE_UNTESTED}: 0.1.5`);
expect(p.out).not.toContain(SENTINEL.INSTALL_OFFER);
const log = path.join(home, 'engine-args.jsonl');
const scan = run(['scan', 'src/styles.css'], { env: { ...env, IMPECCABLE_FAKE_LOG: log } });
expect(scan.code).toBe(2);
expect(scan.err).toContain(`${SENTINEL.SKILL}: present`);
expect(scan.err).toContain('handoff=/impeccable');
expect(JSON.parse(scan.out).engine).toBe(fs.realpathSync(current.engine));
expect(JSON.parse(scan.out).engineVersion).toBe('0.1.5');
expect(fs.readFileSync(log, 'utf-8').trim().split('\n')).toHaveLength(1);
expect(fs.existsSync(current.marker)).toBe(false);
});
});
test('a newer launcher-only plugin never borrows an older bundled engine', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { engine: true });
const current = plugin(config, '4.10.0');
const r = run(['probe'], { env: { HOME: home } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${current.launcher}`);
expect(r.out).not.toContain(SENTINEL.READY);
});
});
test('custom Claude configuration replaces the default profile for plugins and legacy skills', () => {
fixture((home, config) => {
const ignored = plugin(config, '99.0.0', { engine: true });
const custom = path.join(home, 'custom claude');
const selected = plugin(custom, '4.3.1', { engine: true });
const env = { HOME: home, CLAUDE_CONFIG_DIR: custom };
const r = run(['probe'], { env });
expect(lines(r.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(selected.engine)}`);
expect(r.out).not.toContain(ignored.engine);
fs.rmSync(path.join(custom, 'plugins'), { recursive: true });
expect(run(['probe'], { env }).out).toContain(`${SENTINEL.SKILL}: absent`);
const legacy = path.join(custom, 'skills', 'impeccable');
fs.mkdirSync(path.dirname(legacy), { recursive: true });
fs.cpSync(ignored.skill, legacy, { recursive: true });
const legacyEngine = path.join(legacy, 'scripts', 'bin', `${process.platform}-${process.arch}`, path.basename(ignored.engine));
expect(lines(run(['probe'], { env }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(legacyEngine)}`);
});
});
test.each(['relative', 'missing', 'file', 'project'])('invalid %s custom configuration uses safe default discovery', kind => {
fixture((home, config) => {
const good = plugin(config, '4.3.1');
const file = path.join(home, 'not-directory');
fs.writeFileSync(file, 'x');
const override = { relative: '.claude', missing: path.join(home, 'missing'), file, project: REPO }[kind]!;
const r = run(['probe', '--verbose'], { env: { HOME: home, CLAUDE_CONFIG_DIR: override } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${good.launcher}`);
expect(r.out).toContain('CLAUDE_CONFIG_DIR');
});
});
test('ignores incomplete versions, non-directory entries and directory-shaped skill markers', () => {
fixture((home, config) => {
const good = plugin(config, '4.3.1');
const bad = plugin(config, '99.0.0');
fs.rmSync(path.join(bad.skill, 'SKILL.md'));
fs.mkdirSync(path.join(bad.skill, 'SKILL.md'));
const pluginRoot = path.resolve(good.skill, '../../..');
fs.writeFileSync(path.join(pluginRoot, '100.0.0'), 'not a version directory');
fs.mkdirSync(path.join(pluginRoot, '101.0.0'));
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${good.launcher}`);
expect(r.out).toContain('SKILL.md is not a regular file');
expect(r.out).toContain('ENOENT');
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(config, 'plugins'));
fs.writeFileSync(path.join(config, 'plugins', 'cache'), 'not a directory');
const malformed = run(['probe', '--verbose'], { env: { HOME: home } });
expect(malformed.code).toBe(0);
expect(malformed.out).toContain('ENOTDIR');
expect(malformed.out).toContain(`${SENTINEL.SKILL}: absent`);
});
});
test.skipIf(!POSIX || process.getuid?.() === 0)('unreadable cache is diagnosed without crashing', () => {
fixture((home, config) => {
plugin(config, '4.3.1');
const cache = path.join(config, 'plugins', 'cache');
fs.chmodSync(cache, 0o000);
try {
const r = run(['probe', '--verbose'], { env: { HOME: home } });
expect(r.code).toBe(0);
expect(r.out).toContain('EACCES');
} finally { fs.chmodSync(cache, 0o755); }
});
});
test.skipIf(!POSIX)('project-resolving launcher, engine, cache and config symlinks are never executed', () => {
for (const kind of ['launcher', 'engine', 'cache', 'config']) {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const projectDir = fs.mkdtempSync(path.join(REPO, 'plugin-owned-'));
const marker = path.join(home, 'unsafe-ran');
const env: Record<string, string> = { HOME: home };
try {
const evil = path.join(projectDir, 'impeccable');
fs.writeFileSync(evil, `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\nprintf '[]'\n`);
fs.chmodSync(evil, 0o755);
if (kind === 'launcher' || kind === 'engine') {
const link = kind === 'launcher' ? install.launcher : install.engine;
fs.rmSync(link);
fs.symlinkSync(evil, link);
} else {
const source = kind === 'cache' ? path.join(config, 'plugins', 'cache') : config;
const target = path.join(projectDir, 'copied');
fs.renameSync(source, target);
fs.symlinkSync(target, source);
if (kind === 'config') env.CLAUDE_CONFIG_DIR = config;
}
const p = run(['probe', '--verbose'], { env });
expect(p.out).not.toContain(`${SENTINEL.READY}:`);
const s = run(['scan', 'src/styles.css'], { env });
expect(s.out).toBe('');
expect(fs.existsSync(marker)).toBe(false);
expect(fs.existsSync(install.marker)).toBe(false);
} finally { fs.rmSync(projectDir, { recursive: true, force: true }); }
});
}
});
test.skipIf(!POSIX)('launcher hints quote shell metacharacters and suppress control/Markdown injection', () => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { marketplace: "space ' $(touch SHOULD_NOT_EXIST)" });
const r = run(['probe'], { env: { HOME: home } });
const command = r.out.match(/run `([^`]+)` once/)?.[1];
expect(command).toBeDefined();
const check = spawnSync('/bin/sh', ['-c', command!], { cwd: home, encoding: 'utf-8', timeout: 5000 });
expect(check.status).toBe(0);
expect(fs.existsSync(install.marker)).toBe(true);
expect(fs.existsSync(path.join(home, 'SHOULD_NOT_EXIST'))).toBe(false);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
for (const name of ['bad`name', `bad\n${SENTINEL.READY}: forged`, SENTINEL.READY]) {
plugin(config, '4.3.1', { marketplace: name });
const unsafe = run(['probe', '--verbose'], { env: { HOME: home } });
expect(unsafe.out).not.toContain('run `');
expect(lines(unsafe.out).filter(line => line.startsWith(`${SENTINEL.READY}:`))).toEqual([]);
expect(lines(unsafe.out).filter(line => line.startsWith(`${SENTINEL.SKILL}:`))).toHaveLength(1);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
}
});
});
test('multiple marketplaces, irrelevant cache breadth and legacy installs retain deterministic precedence', () => {
fixture((home, config) => {
plugin(config, '4.3.1', { marketplace: 'z-market', engine: true });
const first = plugin(config, '4.3.1', { marketplace: 'a-market', engine: true });
for (let i = 0; i < 100; i++) fs.mkdirSync(path.join(config, 'plugins', 'cache', `irrelevant-${i}`, 'plugin', '1.0.0', 'do-not-walk', 'skills', 'impeccable'), { recursive: true });
expect(lines(run(['probe'], { env: { HOME: home } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(first.engine)}`);
const legacy = path.join(home, '.agents', 'skills', 'impeccable');
fs.mkdirSync(path.dirname(legacy), { recursive: true });
fs.cpSync(first.skill, legacy, { recursive: true });
const engine = path.join(legacy, 'scripts', 'bin', `${process.platform}-${process.arch}`, path.basename(first.engine));
expect(lines(run(['probe'], { env: { HOME: home } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(engine)}`);
expect(lines(run(['probe'], { env: { HOME: home, IMPECCABLE_BIN: FAKE } }).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(FAKE)}`);
});
});
test('plugin discovery preserves standalone cache priority, off and never-ask behavior', () => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const cache = path.join(home, 'engine-cache');
const engine = path.join(cache, 'bin', '0.1.3', path.basename(install.engine));
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
const state = path.join(home, 'state');
fs.mkdirSync(state);
const env = { HOME: home, GSTACK_HOME: state, IMPECCABLE_HOME: cache };
const p = run(['probe'], { env });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(engine)}`);
expect(p.out).not.toContain(SENTINEL.INSTALL_OFFER);
fs.writeFileSync(path.join(state, 'config.yaml'), 'design_detector: off\n');
const off = run(['probe'], { env });
expect(lines(off.out)[0]).toBe(SENTINEL.DISABLED);
expect(off.out).toContain(`${SENTINEL.SKILL}: present`);
fs.writeFileSync(path.join(state, 'config.yaml'), 'design_detector_install_prompted: true\n');
fs.rmSync(cache, { recursive: true });
fs.rmSync(install.engine);
const nc = run(['probe'], { env });
expect(lines(nc.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${install.launcher}`);
expect(nc.out).not.toContain(SENTINEL.INSTALL_OFFER);
expect(nc.out).not.toContain(SENTINEL.HINT);
fs.rmSync(path.join(config, 'plugins'), { recursive: true });
expect(lines(run(['probe'], { env }).out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
expect(fs.existsSync(install.marker)).toBe(false);
});
});
test.skipIf(!POSIX).each([false, true])('symlinked HOME preserves user installs without trusting arbitrary home scan targets (dotfiles=%s)', dotfiles => {
fixture((home, config) => {
const install = plugin(config, '4.3.1', { engine: true });
const alias = `${home}-alias`;
fs.symlinkSync(home, alias, 'dir');
try {
if (dotfiles) git(home, 'init', '-q');
const log = path.join(home, 'engine-calls');
const opts = { cwd: home, env: { HOME: alias, IMPECCABLE_FAKE_LOG: log } };
expect(lines(run(['probe'], opts).out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(install.engine)}`);
const target = path.join(home, 'private.css');
fs.writeFileSync(target, 'body { color: red; }');
const scan = run(['scan', target], opts);
expect(scan.code).toBe(0);
expect(scan.err).toContain(`${SENTINEL.DETECT_REFUSED}: ${target}`);
expect(scan.err).toContain(SENTINEL.DETECT_NO_TARGETS);
expect(scan.out).toBe('');
expect(fs.existsSync(log)).toBe(false);
expect(fs.existsSync(install.marker)).toBe(false);
} finally { fs.rmSync(alias); }
});
});
test.skipIf(!POSIX).each(['config', 'cache', 'marketplace', 'version'])('repository %s symlinks cannot redirect plugin traversal to a user install', kind => {
fixture((home) => {
const externalConfig = path.join(home, 'external-config');
plugin(externalConfig, '4.3.1', { engine: true });
const config = path.join(REPO, '.claude');
const tail = {
config: '', cache: 'plugins/cache', marketplace: 'plugins/cache/impeccable',
version: 'plugins/cache/impeccable/impeccable/4.3.1',
}[kind]!;
const link = path.join(config, tail);
fs.mkdirSync(path.dirname(link), { recursive: true });
fs.symlinkSync(path.join(externalConfig, tail), link, 'dir');
const log = path.join(home, 'engine-ran');
try {
const env = { HOME: home, IMPECCABLE_FAKE_LOG: log };
const p = run(['probe', '--verbose'], { env });
expect(lines(p.out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
expect(p.out).toContain(`${SENTINEL.SKILL}: absent`);
expect(p.out).not.toContain('plugin selected=');
expect(p.out).toContain('plugin skip');
expect(run(['scan', 'src/styles.css'], { env }).out).toBe('');
expect(fs.existsSync(log)).toBe(false);
} finally { fs.rmSync(config, { recursive: true, force: true }); }
});
});
test('standalone cache shares strict version ordering but never treats opaque names as versions', () => {
fixture((home) => {
const cache = path.join(home, 'cache');
let stable = '';
for (const version of ['0.1.3-rc.1', 'v0.1.3', '99.0.0-', 'unknown']) {
const engine = path.join(cache, 'bin', version, POSIX ? 'impeccable' : 'impeccable.exe');
fs.mkdirSync(path.dirname(engine), { recursive: true });
fs.copyFileSync(FAKE, engine);
fs.chmodSync(engine, 0o755);
if (version === 'v0.1.3') stable = engine;
}
const p = run(['probe'], { env: { HOME: home, IMPECCABLE_HOME: cache } });
expect(lines(p.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(stable)}`);
expect(p.out).not.toContain(SENTINEL.ENGINE_UNTESTED);
fs.rmSync(path.join(cache, 'bin', 'v0.1.3'), { recursive: true });
fs.rmSync(path.join(cache, 'bin', '0.1.3-rc.1'), { recursive: true });
expect(lines(run(['probe'], { env: { HOME: home, IMPECCABLE_HOME: cache } }).out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
});
});
});
describe('plugin-cache impeccable install', () => {
// A Claude Code plugin install places impeccable at
// <HOME>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/impeccable/,
// never at the traditional <HOME>/.claude/skills/impeccable/ the SKILL_ROOTS
// walk expects (regression for github.com/garrytan/gstack/issues/2838).
function pluginCacheDir(root: string, marketplace = 'impeccable', plugin = 'impeccable', version = '4.3.1') {
return path.join(root, '.claude', 'plugins', 'cache', marketplace, plugin, version, 'skills', 'impeccable');
}
test('SKILL.md present, no launcher → IMPECCABLE_SKILL: present, no crash, no READY', () => {
const home = path.join(SANDBOX, 'fake-home');
const skillDir = pluginCacheDir(home);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
try {
const r = run(['probe']);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).not.toContain(SENTINEL.READY);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
test.skipIf(!POSIX)('plugin-cache launcher without engine → NOT_CACHED naming the plugin-cache launcher; with sibling engine → READY + VERSION', () => {
const home = path.join(SANDBOX, 'fake-home');
const skillDir = pluginCacheDir(home);
const scripts = path.join(skillDir, 'scripts');
fs.mkdirSync(scripts, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), '#!/bin/sh\necho "would download"\n');
fs.chmodSync(path.join(scripts, 'impeccable'), 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), '0.1.3\n');
try {
const r = run(['probe']);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${path.join(scripts, 'impeccable')}`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).toContain(`run \`${path.join(scripts, 'impeccable')} detect --help\` once`);
expect(r.out).not.toContain('npx impeccable');
expect(r.out).not.toContain('would download');
const sib = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(sib, { recursive: true });
fs.copyFileSync(FAKE, path.join(sib, 'impeccable'));
fs.chmodSync(path.join(sib, 'impeccable'), 0o755);
const r2 = run(['probe']);
expect(lines(r2.out)[0]).toBe(`${SENTINEL.READY}: ${fs.realpathSync(path.join(sib, 'impeccable'))}`);
expect(r2.out).not.toContain(SENTINEL.ENGINE_UNTESTED);
expect(r2.out).not.toContain(SENTINEL.HINT);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
test.skipIf(!POSIX)('a plugin-cache install committed INSIDE the repository is never executed: skill-present only, launcher never runs', () => {
const skillDir = pluginCacheDir(REPO, 'acme', 'impeccable', '1.0.0');
const scripts = path.join(skillDir, 'scripts');
const sib = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(sib, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# impeccable\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), '#!/bin/sh\necho "would download"\n');
fs.chmodSync(path.join(scripts, 'impeccable'), 0o755);
fs.writeFileSync(path.join(scripts, 'VERSION'), '0.1.3\n');
const marker = path.join(SANDBOX, 'repo-plugin-engine-ran.txt');
fs.writeFileSync(path.join(sib, 'impeccable'), `#!/bin/sh\necho ran > ${JSON.stringify(marker)}\necho "[]"\n`);
fs.chmodSync(path.join(sib, 'impeccable'), 0o755);
try {
const r = run(['probe']);
expect(lines(r.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: repository-local install`);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
expect(r.out).toContain('never runs a repository-local launcher');
expect(fs.existsSync(marker)).toBe(false);
} finally {
fs.rmSync(path.join(REPO, '.claude'), { recursive: true, force: true });
}
});
test('multiple plugin-cache entries (different marketplace/plugin/version) coexist without throwing', () => {
const home = path.join(SANDBOX, 'fake-home');
fs.mkdirSync(pluginCacheDir(home, 'marketA', 'impeccable', '1.0.0'), { recursive: true });
fs.writeFileSync(path.join(pluginCacheDir(home, 'marketA', 'impeccable', '1.0.0'), 'SKILL.md'), '# impeccable\n');
fs.mkdirSync(pluginCacheDir(home, 'marketB', 'impeccable', '2.0.0'), { recursive: true });
fs.writeFileSync(path.join(pluginCacheDir(home, 'marketB', 'impeccable', '2.0.0'), 'SKILL.md'), '# impeccable\n');
try {
const r = run(['probe']);
expect(r.out).toContain(`${SENTINEL.SKILL}: present`);
} finally {
fs.rmSync(path.join(home, '.claude'), { recursive: true, force: true });
}
});
});
describe('scan', () => {
test('not READY → prints the probe lines, exit 0, engine never needed', () => {
const r = run(['scan', 'src/styles.css']);
+14
View File
@@ -28,6 +28,17 @@ function makeTestHome(): string {
return mkdtempSync(join(tmpdir(), "gstack-memory-ingest-"));
}
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync("git", args, { cwd: repo, encoding: "utf8", timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git("config", "--get", "remote.origin.url")).toBe(url);
git("config", "--local", `url.${url}.insteadOf`, url);
expect(git("remote", "get-url", "origin")).toBe(url);
}
function runScript(args: string[], env: Record<string, string> = {}): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync("bun", [SCRIPT, ...args], {
encoding: "utf-8",
@@ -918,6 +929,7 @@ describe("#2394: probe applies the same attribution gate as prepare", () => {
mkdirSync(repo, { recursive: true });
spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(repo, "https://github.com/foo/bar.git");
return repo;
}
@@ -998,6 +1010,7 @@ describe("#2394: probe applies the same attribution gate as prepare", () => {
mkdirSync(attributableCwd, { recursive: true });
spawnSync("git", ["-C", attributableCwd, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", attributableCwd, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(attributableCwd, "https://github.com/foo/bar.git");
const ts = new Date().toISOString();
const cwdLine = `{"type":"user","message":{"role":"user","content":"hello"},"timestamp":"${ts}","cwd":"${attributableCwd.replace(/\\/g, "\\\\")}"}\n`;
@@ -1082,6 +1095,7 @@ describe("#2392: transcript ingest honors per-remote trust policy", () => {
mkdirSync(repo, { recursive: true });
spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 });
spawnSync("git", ["-C", repo, "remote", "add", "origin", remoteUrl], { encoding: "utf-8", timeout: 30_000 });
isolateGitRemote(repo, remoteUrl);
return repo;
}
+2
View File
@@ -1135,6 +1135,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'design-review-detector-shim-dom': ['test/session-runner-stream-lifecycle.test.ts', 'design-review/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'lib/dom-dump-script.ts', 'lib/dom-dump.js', 'bin/gstack-design-detect.ts', 'browse/src/**', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.*', 'test/skill-e2e-design.test.ts',
'scripts/resolvers/testing.ts'
],
'design-review-plugin-handoff': ['test/session-runner-stream-lifecycle.test.ts', 'design-review/**', 'scripts/resolvers/design.ts', 'scripts/resolvers/testing.ts', 'lib/design-catalog.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'test/helpers/hermetic-env.ts', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/fixtures/review-eval-design-slop.html', 'test/skill-e2e-design.test.ts'],
'design-html-slop-gate': ['test/session-runner-stream-lifecycle.test.ts', 'test/gstack-paths.test.ts', 'design-html/**', 'scripts/resolvers/design.ts', 'lib/design-detect-contract.ts', 'bin/gstack-design-detect.ts', 'test/helpers/fake-impeccable.ts', 'test/fixtures/fake-impeccable.ts', 'test/fixtures/impeccable-detect-sample.json', 'test/skill-e2e-design.test.ts'],
// /diagram (diagram-render bundle consumers). Triplet = deterministic
@@ -1677,6 +1678,7 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
'design-review-fix': 'periodic',
'design-review-detector-shim': 'gate', // deterministic sentinels from the fake engine (source mode on a diff)
'design-review-detector-shim-dom': 'gate', // same shim, DOM mode through the browse binary's dump; self-skips when the binary is absent
'design-review-plugin-handoff': 'gate',
'design-html-slop-gate': 'periodic', // one-pass gate behavior is a judgment call on a fake engine's fixed output
// /diagram — triplet is deterministic functional (gstack-render falls back
+15
View File
@@ -49,6 +49,17 @@ const FAKE_AWS_KEY = ['AKIA', '1234567890ABCDEF'].join('');
const CONFIG = path.join(ROOT, 'bin', 'gstack-config');
const POLICY = path.join(ROOT, 'bin', 'gstack-gbrain-repo-policy');
function isolateGitRemote(repo: string, url: string): void {
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
expect(result.status).toBe(0);
return result.stdout.trim();
};
expect(git('config', '--get', 'remote.origin.url')).toBe(url);
git('config', '--local', `url.${url}.insteadOf`, url);
expect(git('remote', 'get-url', 'origin')).toBe(url);
}
const FAKE = `#!/bin/sh
MODE=$(cat "$HOME/mode" 2>/dev/null || echo ok)
printf '%s\\n' "$*" >> "$HOME/calls.log"
@@ -213,6 +224,7 @@ describe('gate on: the mediated hand-off', () => {
const git = (args: string[]) => spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
git(['init', '-q']);
git(['remote', 'add', 'origin', 'https://github.com/example/denied-repo.git']);
isolateGitRemote(repo, 'https://github.com/example/denied-repo.git');
const prompt = JSON.stringify({ prompt: 'hello', cwd: repo });
for (const tier of ['deny', 'read-only']) {
const set = spawnSync('bash', [POLICY, 'set', 'https://github.com/example/denied-repo.git', tier], { env, encoding: 'utf8', timeout: 20_000 });
@@ -693,6 +705,7 @@ describe('deadline and policy failure paths (review coverage)', () => {
const git = (args: string[]) => spawnSync('git', args, { cwd: repo, encoding: 'utf8', timeout: 10_000 });
git(['init', '-q']);
git(['remote', 'add', 'origin', 'https://github.com/example/some-repo.git']);
isolateGitRemote(repo, 'https://github.com/example/some-repo.git');
// a directory where the store file should be: hasRepoPolicyStore() is true, every read fails
const storeDir = path.join(home, '.gstack', 'gbrain-repo-policy.json');
fs.mkdirSync(storeDir, { recursive: true });
@@ -742,6 +755,7 @@ describe('trust-policy lookup outcomes (review coverage, second pass)', () => {
try {
spawnSync('git', ['init', '-q'], { cwd: repo, timeout: 10_000 });
spawnSync('git', ['remote', 'add', 'origin', 'https://github.com/example/other.git'], { cwd: repo, timeout: 10_000 });
isolateGitRemote(repo, 'https://github.com/example/other.git');
expect(runHook(JSON.stringify({ prompt: 'hello', cwd: repo }), {}, repo).stdout).toContain('remembered');
} finally {
fs.rmSync(repo, { recursive: true, force: true });
@@ -755,6 +769,7 @@ describe('trust-policy lookup outcomes (review coverage, second pass)', () => {
for (const [dir, url] of [[denied, 'https://github.com/example/denied.git'], [allowed, 'https://github.com/example/allowed.git']] as const) {
spawnSync('git', ['init', '-q'], { cwd: dir, timeout: 10_000 });
spawnSync('git', ['remote', 'add', 'origin', url], { cwd: dir, timeout: 10_000 });
isolateGitRemote(dir, url);
}
expect(spawnSync('bash', [POLICY, 'set', 'https://github.com/example/denied.git', 'deny'], { env, encoding: 'utf8', timeout: 20_000 }).status).toBe(0);
const r = runHook(JSON.stringify({ prompt: 'hello', cwd: denied }), { GIT_DIR: path.join(allowed, '.git'), GIT_WORK_TREE: allowed }, denied);
+1 -1
View File
@@ -828,7 +828,7 @@ test('stderr lifecycle regression selects runtime consumers without a quality-ma
'outside-voice-codex-to-claude-code', 'outside-voice-claude-code-to-codex', 'outside-plan-disabled-no-fallback', 'ship-coverage-audit', 'review-coverage-audit',
'plan-eng-coverage-audit', 'ship-triage', 'ship-docsync', 'docsync-spawned', 'design-consultation-core',
'design-consultation-existing', 'design-consultation-research', 'design-consultation-preview', 'plan-design-review-no-ui-scope', 'design-review-fix',
'design-review-detector-shim', 'design-review-detector-shim-dom', 'design-html-slop-gate', 'diagram-triplet', 'diagram-authoring-quality',
'design-review-detector-shim', 'design-review-detector-shim-dom', 'design-review-plugin-handoff', 'design-html-slop-gate', 'diagram-triplet', 'diagram-authoring-quality',
'gstack-upgrade-happy-path', 'land-and-deploy-workflow', 'land-and-deploy-first-run', 'land-and-deploy-review-gate', 'canary-workflow',
'benchmark-workflow', 'setup-deploy-workflow', 'autoplan-dual-voice', 'scrape-match-path', 'scrape-prototype-path',
'skillify-happy-path', 'skillify-provenance-refusal', 'skillify-approval-reject', 'journey-ideation', 'journey-plan-eng',
+158
View File
@@ -13,6 +13,7 @@ import {
} from './helpers/e2e-helpers';
import { asideAvailable } from './helpers/aside-available';
import { installFakeImpeccable, DETECT_SAMPLE } from './helpers/fake-impeccable';
import { hermeticChildEnv } from './helpers/hermetic-env';
import { sliceBetween, extractDesignResearchContract } from './helpers/skill-fixture';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
@@ -771,6 +772,163 @@ function makeFakeEngine(): string {
return installFakeImpeccable('skill-e2e-fake-impeccable-').dir;
}
function detectorReportEntries(report: string): string[] {
return report.split(/(?=^[\t ]*(?:#{1,6}\s+|[-*|]\s*|\d+[.)]\s+)?(?:\*\*|`)?FINDING-\d+)/m);
}
if (!evalsEnabled) test('detector report handoffs stay with their entry across inline cross-references', () => {
const report = `### FINDING-001 \`[low-contrast]\` — impact=high — DEFERRED
handoff=\`/impeccable colorize\`
### FINDING-002 \`[ai-color-palette]\` — impact=medium — DEFERRED
The colors also appear in FINDING-001. This is unconfirmed static evidence.
handoff=\`/impeccable colorize\`
### FINDING-003 \`[skipped-heading]\` — impact=medium — DEFERRED
handoff=\`/impeccable typeset\`
`;
const hasPaletteHandoff = (text: string) => detectorReportEntries(text).some(entry => entry.includes('[ai-color-palette]') && entry.includes('/impeccable colorize'));
expect(hasPaletteHandoff(report)).toBe(true);
expect(hasPaletteHandoff(report.replace('handoff=`/impeccable colorize`\n\n### FINDING-003', '### FINDING-003'))).toBe(false);
for (const marker of ['.', ')']) {
const numbered = report.replace(/^### FINDING-(\d+)/gm, (_, number) => `${Number(number)}${marker} **FINDING-${number}**`);
expect(hasPaletteHandoff(numbered)).toBe(true);
expect(hasPaletteHandoff(numbered.replace(`handoff=\`/impeccable colorize\`\n\n3${marker}`, `3${marker}`))).toBe(false);
}
});
function pluginDetectorFixture() {
const fixture = installFakeImpeccable('skill-e2e-plugin-');
const repoDir = path.join(fixture.dir, 'repo');
const home = path.join(fixture.dir, 'home');
const configDir = path.join(fixture.dir, 'custom-claude');
const gstackHome = path.join(fixture.dir, 'gstack');
const impeccableHome = path.join(fixture.dir, 'engine-cache');
for (const dir of [repoDir, home, configDir, gstackHome, impeccableHome]) fs.mkdirSync(dir);
const env = hermeticChildEnv({
HOME: home, USERPROFILE: home, CLAUDE_CONFIG_DIR: configDir,
GSTACK_HOME: gstackHome, IMPECCABLE_HOME: impeccableHome,
CLAUDE_PLUGIN_DATA: '', GSTACK_HEADLESS: '1',
});
for (const key of Object.keys(env)) if (key.startsWith('IMPECCABLE_') && key !== 'IMPECCABLE_HOME') delete env[key];
const engines: Record<string, string> = {};
for (const version of ['4.3.1', '4.10.0']) {
const skillDir = path.join(configDir, 'plugins/cache/fixture-market/impeccable', version, 'skills/impeccable');
const scripts = path.join(skillDir, 'scripts');
const engineDir = path.join(scripts, 'bin', `${process.platform}-${process.arch}`);
fs.mkdirSync(engineDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Impeccable fixture marker\n');
fs.writeFileSync(path.join(scripts, 'impeccable'), `#!/bin/sh\nprintf launcher > '${fixture.dir}/launcher-ran'\nexit 99\n`, { mode: 0o755 });
fs.writeFileSync(path.join(scripts, 'VERSION'), '1.6.0\n');
engines[version] = path.join(engineDir, 'impeccable');
fs.writeFileSync(engines[version], `#!/bin/sh\nIMPECCABLE_FAKE_LOG='${fixture.dir}/${version}.jsonl' exec '${process.execPath}' '${fixture.bin}' "$@"\n`, { mode: 0o755 });
engines[version] = fs.realpathSync(engines[version]);
}
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repoDir, env, encoding: 'utf-8', timeout: 5000 });
if (result.status !== 0) throw new Error(`Plugin fixture git ${args[0]} failed: ${result.stderr}`);
};
git('init', '-b', 'main');
git('config', 'user.email', 'test@test.com');
git('config', 'user.name', 'Test');
fs.writeFileSync(path.join(repoDir, 'index.html'), '<h1>Clean</h1>\n');
git('add', '.');
git('commit', '-m', 'initial');
git('checkout', '-b', 'feature/landing');
fs.copyFileSync(path.join(ROOT, 'test/fixtures/review-eval-design-slop.html'), path.join(repoDir, 'index.html'));
git('add', '.');
git('commit', '-m', 'landing page');
fs.writeFileSync(path.join(repoDir, 'design-review-detector.md'), detectorSkillText([
['**Design detector (optional, deterministic):**', '**Create output directories:**'],
['**Phase 0: mechanical scan**', '## Phases 1-6'],
]));
return { dir: fixture.dir, repoDir, env, engines };
}
if (!evalsEnabled) test('plugin detector fixture discovers the selected engine without an override', () => {
const fixture = pluginDetectorFixture();
try {
expect(fixture.env.IMPECCABLE_BIN).toBeUndefined();
const probe = spawnSync(process.execPath, ['--no-env-file', 'run', path.join(ROOT, 'bin/gstack-design-detect.ts'), 'probe', '--host', 'claude'], {
cwd: fixture.repoDir, env: fixture.env, encoding: 'utf-8', timeout: 10000,
});
expect(probe.status).toBe(0);
expect(probe.stdout).toContain(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`);
expect(probe.stdout).toContain('IMPECCABLE_SKILL: present');
const scan = spawnSync(process.execPath, ['--no-env-file', 'run', path.join(ROOT, 'bin/gstack-design-detect.ts'), 'scan', '--changed', 'main', '--host', 'claude'], {
cwd: fixture.repoDir, env: fixture.env, encoding: 'utf-8', timeout: 10000,
});
expect(scan.status).toBe(2);
expect(scan.stderr).toContain('handoff=/impeccable colorize');
expect(fs.existsSync(path.join(fixture.dir, '4.10.0.jsonl'))).toBe(true);
expect(fs.existsSync(path.join(fixture.dir, '4.3.1.jsonl'))).toBe(false);
expect(fs.existsSync(path.join(fixture.dir, 'launcher-ran'))).toBe(false);
} finally {
fs.rmSync(fixture.dir, { recursive: true, force: true });
}
});
describeIfSelected('Design review plugin discovery E2E', ['design-review-plugin-handoff'], () => {
testConcurrentIfSelected('design-review-plugin-handoff', async () => {
const fixture = pluginDetectorFixture();
try {
const result = await runSkillTest({
prompt: `Load gstack's /design-review workflow by reading design-review-detector.md, the actual Setup detector and Phase 0 excerpt.
Supported actor scope: read that excerpt, run its detector probe and one source-mode scan, then write detector-output.md. This isolated repository is on feature/landing; its base is main. There is no URL.
Do not install or download anything, execute a launcher, change environment variables, browse, ask questions, spawn agents, or edit anything except detector-output.md. Do not read Impeccable skill files. If discovery fails, report that failure and stop; do not repair the environment.
Write the probe's first line and skill-presence line, then one FINDING-NNN entry per DETECT_TOP rule with its [rule-id] and impact. All findings are deferred, unconfirmed static evidence because rendered-page confirmation is outside this actor's scope. Apply the excerpt's deferred-finding reporting requirements. Do not fix source files or claim visual verification.`,
workingDirectory: fixture.repoDir,
maxTurns: 10,
timeout: CAPTURE_MS,
testName: 'design-review-plugin-handoff',
runId,
tools: ['Bash', 'Read', 'Write'],
env: fixture.env,
});
logCost('/design-review plugin handoff', result);
const reportPath = path.join(fixture.repoDir, 'detector-output.md');
const report = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf-8') : '';
const outputs = result.toolCalls.map(call => String(call.output ?? '')).join('\n');
const commands = result.toolCalls.filter(call => call.tool === 'Bash').map(call => String(call.input?.command ?? ''));
const handoffs = [...outputs.matchAll(/\[([\w-]+)\] impact=(\w+)[^\n]*handoff=(\/impeccable \w+)/g)];
const entries = detectorReportEntries(report);
const engineLog = path.join(fixture.dir, '4.10.0.jsonl');
const invocations = fs.existsSync(engineLog) ? fs.readFileSync(engineLog, 'utf-8').trim().split('\n') : [];
const sourceUnchanged = fs.readFileSync(path.join(fixture.repoDir, 'index.html'), 'utf-8') === fs.readFileSync(path.join(ROOT, 'test/fixtures/review-eval-design-slop.html'), 'utf-8');
if (process.env.GSTACK_EVAL_DIR) {
const evidenceDir = path.join(process.env.GSTACK_EVAL_DIR, 'plugin-handoff', `${Date.now()}`);
fs.mkdirSync(evidenceDir, { recursive: true, mode: 0o700 });
for (const [name, text] of Object.entries({
'report.md': report,
'skill-excerpt.md': fs.readFileSync(path.join(fixture.repoDir, 'design-review-detector.md'), 'utf-8'),
'engine-invocations.jsonl': invocations.join('\n'),
})) fs.writeFileSync(path.join(evidenceDir, name), text, { mode: 0o600 });
}
const checks = {
success: result.exitReason === 'success',
selectedEngine: outputs.includes(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`),
skillPresent: outputs.includes('IMPECCABLE_SKILL: present'),
probeReported: report.includes(`IMPECCABLE_READY: ${fixture.engines['4.10.0']}`) && report.includes('IMPECCABLE_SKILL: present'),
probeExecuted: commands.some(command => /gstack-design-detect\.ts probe/.test(command)),
scanExecuted: commands.some(command => /gstack-design-detect\.ts scan --changed main/.test(command)),
noInstallOrOverride: !commands.some(command => /\bnpx\b|gstack-design-detect\.ts install|\b(?:curl|wget|npm install|bun add)\b|IMPECCABLE_BIN\s*=/.test(command)),
oneNewEngineInvocation: invocations.length === 1,
oldEngineNotExecuted: !fs.existsSync(path.join(fixture.dir, '4.3.1.jsonl')),
launcherNotExecuted: !fs.existsSync(path.join(fixture.dir, 'launcher-ran')),
sourceUnchanged,
findingsReported: report.includes('FINDING-001') && report.includes('[ai-color-palette]') && report.includes('[low-contrast]'),
deferred: /deferred/i.test(report),
nonemptyHandoffs: handoffs.length > 0,
perFindingHandoff: handoffs.every(([, rule, impact, command]) => entries.some(entry => entry.includes(`[${rule}]`) && entry.toLowerCase().includes(impact) && entry.includes(command))),
};
recordE2E(evalCollector, '/design-review plugin handoff', 'Design review plugin discovery E2E', result, { passed: Object.values(checks).every(Boolean) });
expect(Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name)).toEqual([]);
} finally {
fs.rmSync(fixture.dir, { recursive: true, force: true });
}
}, CAPTURE_MS);
});
describeIfSelected('Design review detector shim E2E', ['design-review-detector-shim', 'design-review-detector-shim-dom'], () => {
let repoDir: string;
let engineDir: string;