mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 23:21:53 +02:00
* 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>
62 lines
3.1 KiB
TypeScript
62 lines
3.1 KiB
TypeScript
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 }); }
|
|
});
|