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
+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 }); }
});