* test(3317): red — SDK detect-custom-files must scan skills/ Mirrors tests/bug-2942-detect-custom-skills.test.cjs on the SDK side. The SDK's GSD_MANAGED_DIRS array omits 'skills', so user-added skills under <config-dir>/skills/<name>/ are never returned and get destroyed on /gsd-update. New vitest covers: - detects custom skill at skills/<name>/SKILL.md - does not flag manifest-tracked skill as custom - still detects custom files under get-shit-done/workflows/ (regression) - custom_count matches custom_files.length across multiple skills Fails 2/4 against current SDK source. Will pass after fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS) The SDK port of detect-custom-files declared GSD_MANAGED_DIRS without 'skills', while the canonical bin/gsd-tools.cjs port (which the SDK docstring explicitly cites as its source) had the entry. Because update.md prefers gsd-sdk over the CJS shim, real-world users with the SDK installed never had their custom skills detected — the installer's 'Installed N skills to skills/' step then wiped any non-manifest skill without backing it up to gsd-user-files-backup/. Real-world incident: skills/gsd-roadmap/SKILL.md (fully user-owned) destroyed during the 1.40.0 → 1.41.0 update, recoverable only via Time Machine. One-line fix adds 'skills' to the SDK's GSD_MANAGED_DIRS, matching the CJS source the port was supposed to mirror. The 4 vitest cases added in the prior commit now all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: correct changeset pr number --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
/**
|
|
* Regression test for #3317 — SDK detect-custom-files omits `skills/` from
|
|
* GSD_MANAGED_DIRS. Mirrors the CJS-side coverage in
|
|
* `tests/bug-2942-detect-custom-skills.test.cjs`.
|
|
*
|
|
* Without the fix, user-added skills under `<config-dir>/skills/<name>/`
|
|
* are not detected and get silently wiped on `/gsd-update`.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
import { detectCustomFiles } from './detect-custom-files.js';
|
|
|
|
function sha256(content: string): string {
|
|
return createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
async function writeManifest(configDir: string, files: Record<string, string>): Promise<void> {
|
|
const manifest = {
|
|
version: '1.41.1',
|
|
timestamp: new Date().toISOString(),
|
|
files: {} as Record<string, string>,
|
|
};
|
|
for (const [relPath, content] of Object.entries(files)) {
|
|
const fullPath = join(configDir, relPath);
|
|
await mkdir(join(fullPath, '..'), { recursive: true });
|
|
await writeFile(fullPath, content);
|
|
manifest.files[relPath] = sha256(content);
|
|
}
|
|
await writeFile(
|
|
join(configDir, 'gsd-file-manifest.json'),
|
|
JSON.stringify(manifest, null, 2),
|
|
);
|
|
}
|
|
|
|
async function writeCustomFile(configDir: string, relPath: string, content: string): Promise<void> {
|
|
const fullPath = join(configDir, relPath);
|
|
await mkdir(join(fullPath, '..'), { recursive: true });
|
|
await writeFile(fullPath, content);
|
|
}
|
|
|
|
interface DetectResult {
|
|
custom_files: string[];
|
|
custom_count: number;
|
|
manifest_found: boolean;
|
|
}
|
|
|
|
describe('detectCustomFiles — skills/ parity with CJS port (#3317)', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-3317-skills-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects custom skill at skills/<name>/SKILL.md', async () => {
|
|
await writeManifest(tmpDir, {
|
|
'skills/gsd-planner/SKILL.md': '# GSD Planner Skill\n',
|
|
});
|
|
await writeCustomFile(tmpDir, 'skills/test-custom/SKILL.md', '# My Custom Skill\n');
|
|
|
|
const { data } = await detectCustomFiles(['--config-dir', tmpDir], tmpDir);
|
|
const result = data as DetectResult;
|
|
|
|
expect(Array.isArray(result.custom_files)).toBe(true);
|
|
expect(result.custom_files).toContain('skills/test-custom/SKILL.md');
|
|
expect(result.custom_count).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('does not flag GSD-owned skill listed in manifest', async () => {
|
|
await writeManifest(tmpDir, {
|
|
'skills/gsd-planner/SKILL.md': '# GSD Planner Skill\n',
|
|
});
|
|
|
|
const { data } = await detectCustomFiles(['--config-dir', tmpDir], tmpDir);
|
|
const result = data as DetectResult;
|
|
|
|
expect(result.custom_files).not.toContain('skills/gsd-planner/SKILL.md');
|
|
});
|
|
|
|
it('still detects custom files in get-shit-done/workflows/ (regression guard)', async () => {
|
|
await writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/plan-phase.md': '# Plan Phase\n',
|
|
'skills/gsd-planner/SKILL.md': '# GSD Planner Skill\n',
|
|
});
|
|
await writeCustomFile(tmpDir, 'get-shit-done/workflows/custom-workflow.md', '# Custom\n');
|
|
|
|
const { data } = await detectCustomFiles(['--config-dir', tmpDir], tmpDir);
|
|
const result = data as DetectResult;
|
|
|
|
expect(result.custom_files).toContain('get-shit-done/workflows/custom-workflow.md');
|
|
});
|
|
|
|
it('custom_count matches custom_files.length across multiple skills', async () => {
|
|
await writeManifest(tmpDir, {
|
|
'skills/gsd-planner/SKILL.md': '# GSD Planner Skill\n',
|
|
});
|
|
await writeCustomFile(tmpDir, 'skills/test-custom/SKILL.md', '# Custom One\n');
|
|
await writeCustomFile(tmpDir, 'skills/another-custom/SKILL.md', '# Custom Two\n');
|
|
|
|
const { data } = await detectCustomFiles(['--config-dir', tmpDir], tmpDir);
|
|
const result = data as DetectResult;
|
|
|
|
expect(result.custom_count).toBe(result.custom_files.length);
|
|
const skillEntries = result.custom_files.filter(f => f.startsWith('skills/'));
|
|
expect(skillEntries).toHaveLength(2);
|
|
});
|
|
});
|