refactor(tests): consolidate Runtime Artifact Layout Module — 12 files → 3 (#3759)
* refactor(tests): consolidate Runtime Artifact Layout Module — 12 files → 3 Closes #3757 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(changeset): add proper frontmatter to 3757 fragment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
8
.changeset/3757-consolidate-runtime-artifact-layout.md
Normal file
8
.changeset/3757-consolidate-runtime-artifact-layout.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3759
|
||||
---
|
||||
|
||||
<!-- docs-exempt: internal test refactor only — no user-facing surface changed -->
|
||||
|
||||
Consolidates the Runtime Artifact Layout Module test cluster (ADR-3660) from 12 files spanning three module names (`surface-*`, `runtime-artifact-layout-*`, `install-profiles-*`) into 3 files, resolving the three-module-name split identified in ADR-3660.
|
||||
@@ -1,121 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for loadSkillsManifest — parses requires: frontmatter from commands/gsd/*.md
|
||||
* and returns a Map<stem, string[]>.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const {
|
||||
loadSkillsManifest,
|
||||
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
|
||||
function createFixtureDir() {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-manifest-fixture-'));
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function writeSkill(dir, stem, frontmatter) {
|
||||
const content = `---\n${frontmatter}\n---\n\n# body\n`;
|
||||
fs.writeFileSync(path.join(dir, `${stem}.md`), content);
|
||||
}
|
||||
|
||||
describe('loadSkillsManifest', () => {
|
||||
test('returns a Map', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m instanceof Map, 'should return a Map');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with no requires: frontmatter maps to empty array', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
writeSkill(dir, 'help', 'name: gsd:help\ndescription: Help text');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('help'), 'help should be in manifest');
|
||||
assert.deepStrictEqual(m.get('help'), []);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: single value maps to array of one', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
writeSkill(dir, 'add-tests', 'name: gsd:add-tests\ndescription: Add tests\nrequires: [phase]');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('add-tests'));
|
||||
assert.deepStrictEqual(m.get('add-tests'), ['phase']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: multiple values maps to full array', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
writeSkill(dir, 'plan-phase', 'name: gsd:plan-phase\ndescription: Plan\nrequires: [discuss-phase, phase, review, update]');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.deepStrictEqual(m.get('plan-phase'), ['discuss-phase', 'phase', 'review', 'update']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores non-.md files in the dir', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
writeSkill(dir, 'help', 'name: gsd:help\ndescription: Help');
|
||||
fs.writeFileSync(path.join(dir, 'README.txt'), 'not a skill');
|
||||
fs.writeFileSync(path.join(dir, 'notes.json'), '{}');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('help'));
|
||||
assert.ok(!m.has('README'));
|
||||
assert.ok(!m.has('notes'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('empty dir returns empty Map', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.strictEqual(m.size, 0);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: empty array maps to empty array', () => {
|
||||
const dir = createFixtureDir();
|
||||
try {
|
||||
writeSkill(dir, 'explore', 'name: gsd:explore\ndescription: Explore\nrequires: []');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.deepStrictEqual(m.get('explore'), []);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('loads real commands/gsd/ directory without throwing', () => {
|
||||
const realDir = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const m = loadSkillsManifest(realDir);
|
||||
assert.ok(m.size >= 60, `expected >=60 skills, got ${m.size}`);
|
||||
// discuss-phase requires [config, phase]
|
||||
const depsDP = m.get('discuss-phase');
|
||||
assert.ok(Array.isArray(depsDP), 'discuss-phase should be in manifest');
|
||||
assert.ok(depsDP.includes('phase'), 'discuss-phase should require phase');
|
||||
assert.ok(depsDP.includes('config'), 'discuss-phase should require config');
|
||||
// help has no requires
|
||||
assert.deepStrictEqual(m.get('help'), []);
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for readActiveProfile / writeActiveProfile marker persistence.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const {
|
||||
readActiveProfile,
|
||||
writeActiveProfile,
|
||||
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
|
||||
describe('readActiveProfile / writeActiveProfile', () => {
|
||||
test('write then read round-trips the profile name', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
writeActiveProfile(dir, 'standard');
|
||||
assert.strictEqual(readActiveProfile(dir), 'standard');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips "core" profile', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
assert.strictEqual(readActiveProfile(dir), 'core');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips composed profiles "core,audit"', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
writeActiveProfile(dir, 'core,audit');
|
||||
assert.strictEqual(readActiveProfile(dir), 'core,audit');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips "full"', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
writeActiveProfile(dir, 'full');
|
||||
assert.strictEqual(readActiveProfile(dir), 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('missing marker file returns null (not throws)', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('non-existent directory returns null (not throws)', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-marker-no-exist-' + Date.now());
|
||||
const result = readActiveProfile(ghost);
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test('corrupt marker content (invalid chars) returns null', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-profile'), 'profile with spaces and !!!\n');
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('empty marker file returns null', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-profile'), '');
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writeActiveProfile creates the directory if it does not exist', () => {
|
||||
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-base-'));
|
||||
const nested = path.join(base, 'skills', '.claude');
|
||||
try {
|
||||
writeActiveProfile(nested, 'standard');
|
||||
assert.ok(fs.existsSync(nested), 'directory should be created');
|
||||
assert.strictEqual(readActiveProfile(nested), 'standard');
|
||||
} finally {
|
||||
fs.rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('overwrites a previously written profile', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-marker-'));
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeActiveProfile(dir, 'full');
|
||||
assert.strictEqual(readActiveProfile(dir), 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,196 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for resolveProfile — computes transitive closure over the requires: graph.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
PROFILES,
|
||||
resolveProfile,
|
||||
loadSkillsManifest,
|
||||
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
describe('PROFILES map', () => {
|
||||
test('PROFILES is frozen', () => {
|
||||
assert.ok(Object.isFrozen(PROFILES));
|
||||
});
|
||||
|
||||
test('PROFILES has core, standard, full keys', () => {
|
||||
assert.ok('core' in PROFILES, 'PROFILES.core missing');
|
||||
assert.ok('standard' in PROFILES, 'PROFILES.standard missing');
|
||||
assert.ok('full' in PROFILES, 'PROFILES.full missing');
|
||||
});
|
||||
|
||||
test('PROFILES.core contains the 7 main-loop skills (including phase)', () => {
|
||||
const core = PROFILES.core;
|
||||
assert.ok(Array.isArray(core), 'core should be an array');
|
||||
const sorted = [...core].sort();
|
||||
assert.deepStrictEqual(sorted, [
|
||||
'discuss-phase',
|
||||
'execute-phase',
|
||||
'help',
|
||||
'new-project',
|
||||
'phase',
|
||||
'plan-phase',
|
||||
'update',
|
||||
]);
|
||||
});
|
||||
|
||||
test('PROFILES.full is the sentinel "*"', () => {
|
||||
assert.strictEqual(PROFILES.full, '*');
|
||||
});
|
||||
|
||||
test('PROFILES.standard contains at least the core skills', () => {
|
||||
const core = new Set(PROFILES.core);
|
||||
const standard = PROFILES.standard;
|
||||
assert.ok(Array.isArray(standard), 'standard should be an array');
|
||||
for (const s of core) {
|
||||
assert.ok(standard.includes(s), `standard should include core skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('PROFILES.standard has at least 10 skills', () => {
|
||||
assert.ok(PROFILES.standard.length >= 10, `standard should have >=10 skills, got ${PROFILES.standard.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProfile', () => {
|
||||
test('defaults to full when called with no args', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ manifest });
|
||||
assert.strictEqual(result.name, 'full');
|
||||
assert.strictEqual(result.skills, '*');
|
||||
});
|
||||
|
||||
test('resolves core profile — returns 7+ skills', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['core'], manifest });
|
||||
assert.strictEqual(result.name, 'core');
|
||||
assert.ok(result.skills instanceof Set, 'skills should be a Set');
|
||||
// core has 7 base skills.
|
||||
assert.ok(result.skills.size >= 7, `core closure should have >=7 skills, got ${result.skills.size}`);
|
||||
// All base skills must be present
|
||||
for (const s of PROFILES.core) {
|
||||
assert.ok(result.skills.has(s), `core closure should include ${s}`);
|
||||
}
|
||||
assert.ok(result.skills.has('phase'), 'core closure must include phase');
|
||||
});
|
||||
|
||||
test('resolves standard profile — returns superset of core', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const coreResult = resolveProfile({ modes: ['core'], manifest });
|
||||
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.strictEqual(stdResult.name, 'standard');
|
||||
assert.ok(stdResult.skills instanceof Set);
|
||||
assert.ok(stdResult.skills.size >= coreResult.skills.size, 'standard should have >= skills than core');
|
||||
// All core closure skills must be in standard
|
||||
for (const s of coreResult.skills) {
|
||||
assert.ok(stdResult.skills.has(s), `standard must include core skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolves full profile — returns sentinel', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['full'], manifest });
|
||||
assert.strictEqual(result.name, 'full');
|
||||
assert.strictEqual(result.skills, '*');
|
||||
});
|
||||
|
||||
test('composable profiles — core,standard union is same as standard', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
||||
const composed = resolveProfile({ modes: ['core', 'standard'], manifest });
|
||||
// name should reflect composed
|
||||
assert.ok(composed.name.includes('core') && composed.name.includes('standard'),
|
||||
`composed name should include both, got: ${composed.name}`);
|
||||
// skills union should equal standard (since core ⊂ standard)
|
||||
for (const s of stdResult.skills) {
|
||||
assert.ok(composed.skills.has(s), `composed should include standard skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('transitive closure: skill that requires phase pulls in phase', () => {
|
||||
// Build a minimal manifest: only discuss-phase requiring phase
|
||||
const manifest = new Map([
|
||||
['discuss-phase', ['phase']],
|
||||
['phase', []],
|
||||
['help', []],
|
||||
]);
|
||||
// Profile with only discuss-phase in base
|
||||
const miniProfiles = { core: ['discuss-phase', 'help'], full: '*', standard: ['discuss-phase', 'help'] };
|
||||
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
||||
assert.ok(result.skills.has('phase'), 'phase should be pulled in via closure from discuss-phase');
|
||||
assert.ok(result.skills.has('discuss-phase'));
|
||||
assert.ok(result.skills.has('help'));
|
||||
});
|
||||
|
||||
test('deep transitive closure works (A→B→C pulls in C)', () => {
|
||||
const manifest = new Map([
|
||||
['a', ['b']],
|
||||
['b', ['c']],
|
||||
['c', []],
|
||||
]);
|
||||
const miniProfiles = { core: ['a'], full: '*', standard: ['a'] };
|
||||
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
||||
assert.ok(result.skills.has('a'));
|
||||
assert.ok(result.skills.has('b'));
|
||||
assert.ok(result.skills.has('c'));
|
||||
});
|
||||
|
||||
test('resolveProfile result has agents Set', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['core'], manifest });
|
||||
assert.ok(result.agents instanceof Set, 'result should have agents Set');
|
||||
});
|
||||
|
||||
test('resolveProfile standard — agents Set is non-empty (plan-phase pulls gsd-planner etc)', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.ok(result.agents instanceof Set, 'agents should be a Set');
|
||||
assert.ok(result.agents.size > 0, `standard profile should have >0 agents, got ${result.agents.size}`);
|
||||
// plan-phase is in standard and calls gsd-planner, gsd-plan-checker, gsd-phase-researcher
|
||||
assert.ok(result.agents.has('gsd-planner'), 'standard should include gsd-planner (called by plan-phase)');
|
||||
assert.ok(result.agents.has('gsd-plan-checker'), 'standard should include gsd-plan-checker (called by plan-phase)');
|
||||
});
|
||||
|
||||
test('resolveProfile full — agents is empty Set (full staging uses srcDir directly)', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['full'], manifest });
|
||||
assert.strictEqual(result.skills, '*');
|
||||
// Full profile: agents Set is empty because stageAgentsForProfile uses srcDir directly
|
||||
assert.ok(result.agents instanceof Set, 'agents should still be a Set for full');
|
||||
});
|
||||
|
||||
test('agents are derived from skill body text — synthetic manifest', () => {
|
||||
// Build a synthetic manifest where plan-phase calls gsd-planner
|
||||
const manifest = new Map([
|
||||
['plan-phase', []],
|
||||
['phase', []],
|
||||
]);
|
||||
// Override with calls_agents map
|
||||
manifest.get('plan-phase'); // ensure it exists
|
||||
// Use the real loadSkillsManifest with REAL dir to verify body parsing works
|
||||
const realManifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const planPhaseAgents = realManifest.get('_calls_agents_plan-phase') ||
|
||||
// calls_agents may be stored as a separate key or on the manifest entry itself
|
||||
[];
|
||||
// This test validates that the real manifest has agent mappings for plan-phase
|
||||
// by checking resolveProfile computes agents correctly
|
||||
const result = resolveProfile({ modes: ['standard'], manifest: realManifest });
|
||||
assert.ok(result.agents.has('gsd-planner'), 'gsd-planner should be derived from plan-phase body');
|
||||
});
|
||||
|
||||
test('agents transitively closed — skill requiring plan-phase also gets its agents', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
// quick requires plan-phase (via requires: field or direct) and also calls gsd-planner directly
|
||||
// new-project requires plan-phase so inherits its agents
|
||||
const result = resolveProfile({ modes: ['standard'], manifest });
|
||||
// Since plan-phase is in standard, and plan-phase calls gsd-planner, gsd-planner must be present
|
||||
assert.ok(result.agents.has('gsd-planner'));
|
||||
});
|
||||
});
|
||||
@@ -1,271 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for stageSkillsForProfile and stageAgentsForProfile.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
stageSkillsForProfile,
|
||||
stageAgentsForProfile,
|
||||
stageSkillsForRuntimeAsSkills,
|
||||
cleanupStagedSkills,
|
||||
resolveProfile,
|
||||
loadSkillsManifest,
|
||||
STAGED_DIRS,
|
||||
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const REAL_AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
function createFixtureSkillsDir() {
|
||||
const tmp = createTempDir('gsd-stage-profile-');
|
||||
for (const name of ['plan-phase', 'execute-phase', 'autonomous', 'progress', 'help', 'phase']) {
|
||||
fs.writeFileSync(path.join(tmp, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function createFixtureAgentsDir() {
|
||||
const tmp = createTempDir('gsd-agents-profile-');
|
||||
for (const name of ['gsd-planner', 'gsd-executor', 'gsd-code-reviewer']) {
|
||||
fs.writeFileSync(path.join(tmp, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
describe('stageSkillsForRuntimeAsSkills', () => {
|
||||
test('is exported as a function', () => {
|
||||
assert.strictEqual(typeof stageSkillsForRuntimeAsSkills, 'function');
|
||||
});
|
||||
|
||||
test('registers stagedDir in STAGED_DIRS after staging', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'alpha.md'), '# alpha\n');
|
||||
cleanupStagedSkills();
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'gsd-');
|
||||
assert.ok(STAGED_DIRS.has(stagedDir), 'stagedDir must be in STAGED_DIRS');
|
||||
});
|
||||
|
||||
test('non-existent srcCommandsDir returns srcCommandsDir unchanged', () => {
|
||||
const ghost = path.join(require('os').tmpdir(), 'gsd-rta-no-exist-' + Date.now());
|
||||
const converter = (content, _skillName) => content;
|
||||
const result = stageSkillsForRuntimeAsSkills(ghost, { skills: '*' }, converter, 'gsd-');
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('empty prefix produces <stem>/SKILL.md without prefix segment', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'phase.md'), '# phase\n');
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, '');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
assert.deepStrictEqual(entries, ['phase']);
|
||||
const content = fs.readFileSync(path.join(stagedDir, 'phase', 'SKILL.md'), 'utf8');
|
||||
assert.strictEqual(content, '# phase\n');
|
||||
});
|
||||
|
||||
test('converter is called with (content, skillName) for each kept skill', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'alpha.md'), '# alpha\n');
|
||||
fs.writeFileSync(path.join(src, 'beta.md'), '# beta\n');
|
||||
const calls = [];
|
||||
const converter = (content, skillName) => {
|
||||
calls.push([content, skillName]);
|
||||
return content;
|
||||
};
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'x-');
|
||||
assert.strictEqual(calls.length, 2);
|
||||
const callMap = Object.fromEntries(calls.map(([c, n]) => [n, c]));
|
||||
assert.strictEqual(callMap['x-alpha'], '# alpha\n');
|
||||
assert.strictEqual(callMap['x-beta'], '# beta\n');
|
||||
});
|
||||
|
||||
test('skills Set filters: only matching stems land in stagedDir', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
for (const name of ['alpha', 'beta', 'phase']) {
|
||||
fs.writeFileSync(path.join(src, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: new Set(['phase']) }, converter, 'gsd-');
|
||||
const entries = fs.readdirSync(stagedDir).sort();
|
||||
assert.deepStrictEqual(entries, ['gsd-phase']);
|
||||
});
|
||||
|
||||
test('skills === "*" stages all md files as <prefix><stem>/SKILL.md', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
for (const name of ['alpha', 'beta', 'gamma']) {
|
||||
fs.writeFileSync(path.join(src, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'gsd-');
|
||||
const entries = fs.readdirSync(stagedDir).sort();
|
||||
assert.deepStrictEqual(entries, ['gsd-alpha', 'gsd-beta', 'gsd-gamma']);
|
||||
for (const name of ['alpha', 'beta', 'gamma']) {
|
||||
const content = fs.readFileSync(path.join(stagedDir, `gsd-${name}`, 'SKILL.md'), 'utf8');
|
||||
assert.strictEqual(content, `# ${name}\n`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('stageSkillsForProfile', () => {
|
||||
test('full profile (skills === "*") returns srcDir unchanged', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
t.after(() => cleanup(src));
|
||||
const result = stageSkillsForProfile(src, { skills: '*', agents: new Set() });
|
||||
assert.strictEqual(result, src);
|
||||
});
|
||||
|
||||
test('profile with Set copies only member files', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const skills = new Set(['plan-phase', 'help', 'phase']);
|
||||
staged = stageSkillsForProfile(src, { skills, agents: new Set() });
|
||||
assert.notStrictEqual(staged, src);
|
||||
const files = fs.readdirSync(staged).sort();
|
||||
assert.deepStrictEqual(files, ['help.md', 'phase.md', 'plan-phase.md']);
|
||||
});
|
||||
|
||||
test('preserves file content byte-for-byte', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
const content = '# plan-phase special content\n\nsome body\n';
|
||||
fs.writeFileSync(path.join(src, 'plan-phase.md'), content);
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const skills = new Set(['plan-phase']);
|
||||
staged = stageSkillsForProfile(src, { skills, agents: new Set() });
|
||||
const copied = fs.readFileSync(path.join(staged, 'plan-phase.md'), 'utf8');
|
||||
assert.strictEqual(copied, content);
|
||||
});
|
||||
|
||||
test('non-existent srcDir returns srcDir unchanged', () => {
|
||||
const ghost = path.join(require('os').tmpdir(), 'gsd-no-exist-' + Date.now());
|
||||
const result = stageSkillsForProfile(ghost, { skills: new Set(['help']), agents: new Set() });
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('empty skills Set produces empty staged dir', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageSkillsForProfile(src, { skills: new Set(), agents: new Set() });
|
||||
const files = fs.readdirSync(staged);
|
||||
assert.deepStrictEqual(files, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stageAgentsForProfile', () => {
|
||||
test('full profile (skills === "*") returns srcDir unchanged', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
t.after(() => cleanup(src));
|
||||
const result = stageAgentsForProfile(src, { skills: '*', agents: new Set() });
|
||||
assert.strictEqual(result, src);
|
||||
});
|
||||
|
||||
test('non-full profile with empty agents Set produces empty staged dir', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageAgentsForProfile(src, { skills: new Set(['help']), agents: new Set() });
|
||||
const files = fs.readdirSync(staged);
|
||||
assert.deepStrictEqual(files, [], 'no agents for non-full profile by default');
|
||||
});
|
||||
|
||||
test('non-full profile with agents Set copies only member agent files', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const agents = new Set(['gsd-planner']);
|
||||
staged = stageAgentsForProfile(src, { skills: new Set(['plan-phase']), agents });
|
||||
const files = fs.readdirSync(staged).sort();
|
||||
assert.deepStrictEqual(files, ['gsd-planner.md']);
|
||||
});
|
||||
|
||||
test('non-existent srcAgentsDir returns srcAgentsDir unchanged', () => {
|
||||
const ghost = path.join(require('os').tmpdir(), 'gsd-agents-no-exist-' + Date.now());
|
||||
const result = stageAgentsForProfile(ghost, { skills: new Set(), agents: new Set() });
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('standard profile — stageAgentsForProfile copies exactly the agents in resolvedProfile.agents', (t) => {
|
||||
// Uses the real agents dir and commands dir
|
||||
if (!fs.existsSync(REAL_AGENTS_DIR) || !fs.existsSync(REAL_COMMANDS_DIR)) return;
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const resolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.ok(resolved.agents instanceof Set && resolved.agents.size > 0,
|
||||
'standard profile must have >0 agents (plan-phase calls gsd-planner etc)');
|
||||
let staged;
|
||||
t.after(() => {
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageAgentsForProfile(REAL_AGENTS_DIR, resolved);
|
||||
const stagedFiles = new Set(
|
||||
fs.readdirSync(staged).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3))
|
||||
);
|
||||
// Every file staged must be in resolved.agents
|
||||
for (const stem of stagedFiles) {
|
||||
assert.ok(resolved.agents.has(stem), `staged agent ${stem} not in resolved.agents`);
|
||||
}
|
||||
// Every agent in resolved.agents that exists in the real dir must be staged
|
||||
for (const agentStem of resolved.agents) {
|
||||
const exists = fs.existsSync(path.join(REAL_AGENTS_DIR, `${agentStem}.md`));
|
||||
if (exists) {
|
||||
assert.ok(stagedFiles.has(agentStem), `resolved agent ${agentStem} missing from staged dir`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('full profile staging returns real agents dir unchanged', () => {
|
||||
if (!fs.existsSync(REAL_AGENTS_DIR)) return;
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const resolved = resolveProfile({ modes: ['full'], manifest });
|
||||
const result = stageAgentsForProfile(REAL_AGENTS_DIR, resolved);
|
||||
assert.strictEqual(result, REAL_AGENTS_DIR);
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Edge-case tests for resolveRuntimeArtifactLayout.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { resolveRuntimeArtifactLayout } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
|
||||
describe('hermes nested layout', () => {
|
||||
test('hermes has destSubpath skills/gsd and empty prefix', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('hermes', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds[0].destSubpath, 'skills/gsd');
|
||||
assert.strictEqual(layout.kinds[0].prefix, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cline empty kinds', () => {
|
||||
test('cline has no kinds', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cline', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini commands layout', () => {
|
||||
test('gemini has one commands kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('gemini', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||
});
|
||||
});
|
||||
|
||||
describe('claude scope=local has commands and agents', () => {
|
||||
test('claude local has both commands and agents kinds', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'local');
|
||||
const kindNames = layout.kinds.map(k => k.kind);
|
||||
assert.ok(kindNames.includes('commands'), 'should have commands kind');
|
||||
assert.ok(kindNames.includes('agents'), 'should have agents kind');
|
||||
});
|
||||
});
|
||||
|
||||
describe('claude scope=global has only skills', () => {
|
||||
test('claude global has only skills kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'global');
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown runtime throws TypeError', () => {
|
||||
test('grok throws TypeError containing "grok"', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('grok', '/tmp/x'),
|
||||
(err) => {
|
||||
assert.ok(err instanceof TypeError);
|
||||
assert.ok(err.message.includes('grok'), 'error message must contain the runtime name');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('xyzunknown throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('xyzunknown', '/tmp/x'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid configDir throws TypeError', () => {
|
||||
test('empty configDir throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', ''),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
test('non-string configDir throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', null),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid scope throws TypeError', () => {
|
||||
test('bad scope throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', '/x', 'invalid'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
});
|
||||
659
tests/runtime-artifact-layout-install-profiles.test.cjs
Normal file
659
tests/runtime-artifact-layout-install-profiles.test.cjs
Normal file
@@ -0,0 +1,659 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Consolidated tests for the Runtime Artifact Layout Module — install-profiles parity (ADR-3660).
|
||||
*
|
||||
* Covers:
|
||||
* - stageSkillsForProfile / stageAgentsForProfile / stageSkillsForRuntimeAsSkills
|
||||
* - resolveProfile — transitive closure + PROFILES map
|
||||
* - loadSkillsManifest — frontmatter parsing
|
||||
* - readActiveProfile / writeActiveProfile — marker persistence
|
||||
*
|
||||
* Sources consolidated (4 files deleted):
|
||||
* tests/install-profiles-stage.test.cjs
|
||||
* tests/install-profiles-resolve.test.cjs
|
||||
* tests/install-profiles-manifest.test.cjs
|
||||
* tests/install-profiles-marker.test.cjs
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const {
|
||||
stageSkillsForProfile,
|
||||
stageAgentsForProfile,
|
||||
stageSkillsForRuntimeAsSkills,
|
||||
cleanupStagedSkills,
|
||||
resolveProfile,
|
||||
loadSkillsManifest,
|
||||
readActiveProfile,
|
||||
writeActiveProfile,
|
||||
PROFILES,
|
||||
STAGED_DIRS,
|
||||
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const REAL_AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function tmpDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix || 'gsd-ip-'));
|
||||
}
|
||||
|
||||
function createFixtureSkillsDir() {
|
||||
const tmp = createTempDir('gsd-stage-profile-');
|
||||
for (const name of ['plan-phase', 'execute-phase', 'autonomous', 'progress', 'help', 'phase']) {
|
||||
fs.writeFileSync(path.join(tmp, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function createFixtureAgentsDir() {
|
||||
const tmp = createTempDir('gsd-agents-profile-');
|
||||
for (const name of ['gsd-planner', 'gsd-executor', 'gsd-code-reviewer']) {
|
||||
fs.writeFileSync(path.join(tmp, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function writeSkill(dir, stem, frontmatter) {
|
||||
const content = `---\n${frontmatter}\n---\n\n# body\n`;
|
||||
fs.writeFileSync(path.join(dir, `${stem}.md`), content);
|
||||
}
|
||||
|
||||
// ─── stageSkillsForRuntimeAsSkills ──────────────────────────────────────────
|
||||
|
||||
describe('stageSkillsForRuntimeAsSkills', () => {
|
||||
test('is exported as a function', () => {
|
||||
assert.strictEqual(typeof stageSkillsForRuntimeAsSkills, 'function');
|
||||
});
|
||||
|
||||
test('registers stagedDir in STAGED_DIRS after staging', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'alpha.md'), '# alpha\n');
|
||||
cleanupStagedSkills();
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'gsd-');
|
||||
assert.ok(STAGED_DIRS.has(stagedDir), 'stagedDir must be in STAGED_DIRS');
|
||||
});
|
||||
|
||||
test('non-existent srcCommandsDir returns srcCommandsDir unchanged', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-rta-no-exist-' + Date.now());
|
||||
const converter = (content, _skillName) => content;
|
||||
const result = stageSkillsForRuntimeAsSkills(ghost, { skills: '*' }, converter, 'gsd-');
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('empty prefix produces <stem>/SKILL.md without prefix segment', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'phase.md'), '# phase\n');
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, '');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
assert.deepStrictEqual(entries, ['phase']);
|
||||
const content = fs.readFileSync(path.join(stagedDir, 'phase', 'SKILL.md'), 'utf8');
|
||||
assert.strictEqual(content, '# phase\n');
|
||||
});
|
||||
|
||||
test('converter is called with (content, skillName) for each kept skill', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
fs.writeFileSync(path.join(src, 'alpha.md'), '# alpha\n');
|
||||
fs.writeFileSync(path.join(src, 'beta.md'), '# beta\n');
|
||||
const calls = [];
|
||||
const converter = (content, skillName) => {
|
||||
calls.push([content, skillName]);
|
||||
return content;
|
||||
};
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'x-');
|
||||
assert.strictEqual(calls.length, 2);
|
||||
const callMap = Object.fromEntries(calls.map(([c, n]) => [n, c]));
|
||||
assert.strictEqual(callMap['x-alpha'], '# alpha\n');
|
||||
assert.strictEqual(callMap['x-beta'], '# beta\n');
|
||||
});
|
||||
|
||||
test('skills Set filters: only matching stems land in stagedDir', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
for (const name of ['alpha', 'beta', 'phase']) {
|
||||
fs.writeFileSync(path.join(src, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: new Set(['phase']) }, converter, 'gsd-');
|
||||
const entries = fs.readdirSync(stagedDir).sort();
|
||||
assert.deepStrictEqual(entries, ['gsd-phase']);
|
||||
});
|
||||
|
||||
test('skills === "*" stages all md files as <prefix><stem>/SKILL.md', (t) => {
|
||||
const src = createTempDir('gsd-rta-src-');
|
||||
let stagedDir;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (stagedDir) cleanupStagedSkills();
|
||||
});
|
||||
for (const name of ['alpha', 'beta', 'gamma']) {
|
||||
fs.writeFileSync(path.join(src, `${name}.md`), `# ${name}\n`);
|
||||
}
|
||||
const converter = (content, _skillName) => content;
|
||||
stagedDir = stageSkillsForRuntimeAsSkills(src, { skills: '*' }, converter, 'gsd-');
|
||||
const entries = fs.readdirSync(stagedDir).sort();
|
||||
assert.deepStrictEqual(entries, ['gsd-alpha', 'gsd-beta', 'gsd-gamma']);
|
||||
for (const name of ['alpha', 'beta', 'gamma']) {
|
||||
const content = fs.readFileSync(path.join(stagedDir, `gsd-${name}`, 'SKILL.md'), 'utf8');
|
||||
assert.strictEqual(content, `# ${name}\n`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── stageSkillsForProfile ───────────────────────────────────────────────────
|
||||
|
||||
describe('stageSkillsForProfile', () => {
|
||||
test('full profile (skills === "*") returns srcDir unchanged', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
t.after(() => cleanup(src));
|
||||
const result = stageSkillsForProfile(src, { skills: '*', agents: new Set() });
|
||||
assert.strictEqual(result, src);
|
||||
});
|
||||
|
||||
test('profile with Set copies only member files', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const skills = new Set(['plan-phase', 'help', 'phase']);
|
||||
staged = stageSkillsForProfile(src, { skills, agents: new Set() });
|
||||
assert.notStrictEqual(staged, src);
|
||||
const files = fs.readdirSync(staged).sort();
|
||||
assert.deepStrictEqual(files, ['help.md', 'phase.md', 'plan-phase.md']);
|
||||
});
|
||||
|
||||
test('preserves file content byte-for-byte', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
const content = '# plan-phase special content\n\nsome body\n';
|
||||
fs.writeFileSync(path.join(src, 'plan-phase.md'), content);
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const skills = new Set(['plan-phase']);
|
||||
staged = stageSkillsForProfile(src, { skills, agents: new Set() });
|
||||
const copied = fs.readFileSync(path.join(staged, 'plan-phase.md'), 'utf8');
|
||||
assert.strictEqual(copied, content);
|
||||
});
|
||||
|
||||
test('non-existent srcDir returns srcDir unchanged', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-no-exist-' + Date.now());
|
||||
const result = stageSkillsForProfile(ghost, { skills: new Set(['help']), agents: new Set() });
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('empty skills Set produces empty staged dir', (t) => {
|
||||
const src = createFixtureSkillsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageSkillsForProfile(src, { skills: new Set(), agents: new Set() });
|
||||
const files = fs.readdirSync(staged);
|
||||
assert.deepStrictEqual(files, []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── stageAgentsForProfile ───────────────────────────────────────────────────
|
||||
|
||||
describe('stageAgentsForProfile', () => {
|
||||
test('full profile (skills === "*") returns srcDir unchanged', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
t.after(() => cleanup(src));
|
||||
const result = stageAgentsForProfile(src, { skills: '*', agents: new Set() });
|
||||
assert.strictEqual(result, src);
|
||||
});
|
||||
|
||||
test('non-full profile with empty agents Set produces empty staged dir', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageAgentsForProfile(src, { skills: new Set(['help']), agents: new Set() });
|
||||
const files = fs.readdirSync(staged);
|
||||
assert.deepStrictEqual(files, [], 'no agents for non-full profile by default');
|
||||
});
|
||||
|
||||
test('non-full profile with agents Set copies only member agent files', (t) => {
|
||||
const src = createFixtureAgentsDir();
|
||||
let staged;
|
||||
t.after(() => {
|
||||
cleanup(src);
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
const agents = new Set(['gsd-planner']);
|
||||
staged = stageAgentsForProfile(src, { skills: new Set(['plan-phase']), agents });
|
||||
const files = fs.readdirSync(staged).sort();
|
||||
assert.deepStrictEqual(files, ['gsd-planner.md']);
|
||||
});
|
||||
|
||||
test('non-existent srcAgentsDir returns srcAgentsDir unchanged', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-agents-no-exist-' + Date.now());
|
||||
const result = stageAgentsForProfile(ghost, { skills: new Set(), agents: new Set() });
|
||||
assert.strictEqual(result, ghost);
|
||||
});
|
||||
|
||||
test('standard profile — stageAgentsForProfile copies exactly the agents in resolvedProfile.agents', (t) => {
|
||||
if (!fs.existsSync(REAL_AGENTS_DIR) || !fs.existsSync(REAL_COMMANDS_DIR)) return;
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const resolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.ok(resolved.agents instanceof Set && resolved.agents.size > 0,
|
||||
'standard profile must have >0 agents (plan-phase calls gsd-planner etc)');
|
||||
let staged;
|
||||
t.after(() => {
|
||||
if (staged) cleanupStagedSkills();
|
||||
});
|
||||
staged = stageAgentsForProfile(REAL_AGENTS_DIR, resolved);
|
||||
const stagedFiles = new Set(
|
||||
fs.readdirSync(staged).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3))
|
||||
);
|
||||
for (const stem of stagedFiles) {
|
||||
assert.ok(resolved.agents.has(stem), `staged agent ${stem} not in resolved.agents`);
|
||||
}
|
||||
for (const agentStem of resolved.agents) {
|
||||
const exists = fs.existsSync(path.join(REAL_AGENTS_DIR, `${agentStem}.md`));
|
||||
if (exists) {
|
||||
assert.ok(stagedFiles.has(agentStem), `resolved agent ${agentStem} missing from staged dir`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('full profile staging returns real agents dir unchanged', () => {
|
||||
if (!fs.existsSync(REAL_AGENTS_DIR)) return;
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const resolved = resolveProfile({ modes: ['full'], manifest });
|
||||
const result = stageAgentsForProfile(REAL_AGENTS_DIR, resolved);
|
||||
assert.strictEqual(result, REAL_AGENTS_DIR);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PROFILES map + resolveProfile ──────────────────────────────────────────
|
||||
|
||||
describe('PROFILES map', () => {
|
||||
test('PROFILES is frozen', () => {
|
||||
assert.ok(Object.isFrozen(PROFILES));
|
||||
});
|
||||
|
||||
test('PROFILES has core, standard, full keys', () => {
|
||||
assert.ok('core' in PROFILES, 'PROFILES.core missing');
|
||||
assert.ok('standard' in PROFILES, 'PROFILES.standard missing');
|
||||
assert.ok('full' in PROFILES, 'PROFILES.full missing');
|
||||
});
|
||||
|
||||
test('PROFILES.core contains the 7 main-loop skills (including phase)', () => {
|
||||
const core = PROFILES.core;
|
||||
assert.ok(Array.isArray(core), 'core should be an array');
|
||||
const sorted = [...core].sort();
|
||||
assert.deepStrictEqual(sorted, [
|
||||
'discuss-phase',
|
||||
'execute-phase',
|
||||
'help',
|
||||
'new-project',
|
||||
'phase',
|
||||
'plan-phase',
|
||||
'update',
|
||||
]);
|
||||
});
|
||||
|
||||
test('PROFILES.full is the sentinel "*"', () => {
|
||||
assert.strictEqual(PROFILES.full, '*');
|
||||
});
|
||||
|
||||
test('PROFILES.standard contains at least the core skills', () => {
|
||||
const core = new Set(PROFILES.core);
|
||||
const standard = PROFILES.standard;
|
||||
assert.ok(Array.isArray(standard), 'standard should be an array');
|
||||
for (const s of core) {
|
||||
assert.ok(standard.includes(s), `standard should include core skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('PROFILES.standard has at least 10 skills', () => {
|
||||
assert.ok(PROFILES.standard.length >= 10, `standard should have >=10 skills, got ${PROFILES.standard.length}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProfile', () => {
|
||||
test('defaults to full when called with no modes arg', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ manifest });
|
||||
assert.strictEqual(result.name, 'full');
|
||||
assert.strictEqual(result.skills, '*');
|
||||
});
|
||||
|
||||
test('resolves core profile — returns 7+ skills, all base stems present', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['core'], manifest });
|
||||
assert.strictEqual(result.name, 'core');
|
||||
assert.ok(result.skills instanceof Set, 'skills should be a Set');
|
||||
assert.ok(result.skills.size >= 7, `core closure should have >=7 skills, got ${result.skills.size}`);
|
||||
for (const s of PROFILES.core) {
|
||||
assert.ok(result.skills.has(s), `core closure should include ${s}`);
|
||||
}
|
||||
assert.ok(result.skills.has('phase'), 'core closure must include phase');
|
||||
});
|
||||
|
||||
test('resolves standard profile — superset of core', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const coreResult = resolveProfile({ modes: ['core'], manifest });
|
||||
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.strictEqual(stdResult.name, 'standard');
|
||||
assert.ok(stdResult.skills instanceof Set);
|
||||
assert.ok(stdResult.skills.size >= coreResult.skills.size, 'standard should have >= skills than core');
|
||||
for (const s of coreResult.skills) {
|
||||
assert.ok(stdResult.skills.has(s), `standard must include core skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolves full profile — returns sentinel', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['full'], manifest });
|
||||
assert.strictEqual(result.name, 'full');
|
||||
assert.strictEqual(result.skills, '*');
|
||||
});
|
||||
|
||||
test('composable profiles — core,standard union is same as standard', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
||||
const composed = resolveProfile({ modes: ['core', 'standard'], manifest });
|
||||
assert.ok(composed.name.includes('core') && composed.name.includes('standard'),
|
||||
`composed name should include both, got: ${composed.name}`);
|
||||
for (const s of stdResult.skills) {
|
||||
assert.ok(composed.skills.has(s), `composed should include standard skill: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('transitive closure: skill that requires phase pulls in phase', () => {
|
||||
const manifest = new Map([
|
||||
['discuss-phase', ['phase']],
|
||||
['phase', []],
|
||||
['help', []],
|
||||
]);
|
||||
const miniProfiles = { core: ['discuss-phase', 'help'], full: '*', standard: ['discuss-phase', 'help'] };
|
||||
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
||||
assert.ok(result.skills.has('phase'), 'phase should be pulled in via closure from discuss-phase');
|
||||
assert.ok(result.skills.has('discuss-phase'));
|
||||
assert.ok(result.skills.has('help'));
|
||||
});
|
||||
|
||||
test('deep transitive closure works (A→B→C pulls in C)', () => {
|
||||
const manifest = new Map([
|
||||
['a', ['b']],
|
||||
['b', ['c']],
|
||||
['c', []],
|
||||
]);
|
||||
const miniProfiles = { core: ['a'], full: '*', standard: ['a'] };
|
||||
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
||||
assert.ok(result.skills.has('a'));
|
||||
assert.ok(result.skills.has('b'));
|
||||
assert.ok(result.skills.has('c'));
|
||||
});
|
||||
|
||||
test('result has agents Set', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['core'], manifest });
|
||||
assert.ok(result.agents instanceof Set, 'result should have agents Set');
|
||||
});
|
||||
|
||||
test('standard — agents Set is non-empty; gsd-planner and gsd-plan-checker present', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.ok(result.agents instanceof Set, 'agents should be a Set');
|
||||
assert.ok(result.agents.size > 0, `standard profile should have >0 agents, got ${result.agents.size}`);
|
||||
assert.ok(result.agents.has('gsd-planner'), 'standard should include gsd-planner');
|
||||
assert.ok(result.agents.has('gsd-plan-checker'), 'standard should include gsd-plan-checker');
|
||||
});
|
||||
|
||||
test('full — agents is a Set (full staging uses srcDir directly)', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['full'], manifest });
|
||||
assert.strictEqual(result.skills, '*');
|
||||
assert.ok(result.agents instanceof Set, 'agents should still be a Set for full');
|
||||
});
|
||||
|
||||
test('agents are derived from real manifest body text (gsd-planner from plan-phase)', () => {
|
||||
const realManifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['standard'], manifest: realManifest });
|
||||
assert.ok(result.agents.has('gsd-planner'), 'gsd-planner should be derived from plan-phase body');
|
||||
});
|
||||
|
||||
test('agents transitively closed — plan-phase in standard brings its agents', () => {
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = resolveProfile({ modes: ['standard'], manifest });
|
||||
assert.ok(result.agents.has('gsd-planner'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── loadSkillsManifest ──────────────────────────────────────────────────────
|
||||
|
||||
describe('loadSkillsManifest', () => {
|
||||
test('returns a Map', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m instanceof Map, 'should return a Map');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with no requires: frontmatter maps to empty array', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
writeSkill(dir, 'help', 'name: gsd:help\ndescription: Help text');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('help'), 'help should be in manifest');
|
||||
assert.deepStrictEqual(m.get('help'), []);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: single value maps to array of one', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
writeSkill(dir, 'add-tests', 'name: gsd:add-tests\ndescription: Add tests\nrequires: [phase]');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('add-tests'));
|
||||
assert.deepStrictEqual(m.get('add-tests'), ['phase']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: multiple values maps to full array', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
writeSkill(dir, 'plan-phase', 'name: gsd:plan-phase\ndescription: Plan\nrequires: [discuss-phase, phase, review, update]');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.deepStrictEqual(m.get('plan-phase'), ['discuss-phase', 'phase', 'review', 'update']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores non-.md files in the dir', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
writeSkill(dir, 'help', 'name: gsd:help\ndescription: Help');
|
||||
fs.writeFileSync(path.join(dir, 'README.txt'), 'not a skill');
|
||||
fs.writeFileSync(path.join(dir, 'notes.json'), '{}');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.ok(m.has('help'));
|
||||
assert.ok(!m.has('README'));
|
||||
assert.ok(!m.has('notes'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('empty dir returns empty Map', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.strictEqual(m.size, 0);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('skill with requires: empty array maps to empty array', () => {
|
||||
const dir = tmpDir('gsd-manifest-fixture-');
|
||||
try {
|
||||
writeSkill(dir, 'explore', 'name: gsd:explore\ndescription: Explore\nrequires: []');
|
||||
const m = loadSkillsManifest(dir);
|
||||
assert.deepStrictEqual(m.get('explore'), []);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('loads real commands/gsd/ directory: >=60 skills, discuss-phase deps correct, help has no requires', () => {
|
||||
const m = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
// Use prefix/set assertion, not a hardcoded count — avoids stale-count anti-pattern
|
||||
assert.ok(m.size >= 60, `expected >=60 skills, got ${m.size}`);
|
||||
const depsDP = m.get('discuss-phase');
|
||||
assert.ok(Array.isArray(depsDP), 'discuss-phase should be in manifest');
|
||||
assert.ok(depsDP.includes('phase'), 'discuss-phase should require phase');
|
||||
assert.ok(depsDP.includes('config'), 'discuss-phase should require config');
|
||||
assert.deepStrictEqual(m.get('help'), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── readActiveProfile / writeActiveProfile ──────────────────────────────────
|
||||
|
||||
describe('readActiveProfile / writeActiveProfile', () => {
|
||||
test('write then read round-trips the profile name', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'standard');
|
||||
assert.strictEqual(readActiveProfile(dir), 'standard');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips "core" profile', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
assert.strictEqual(readActiveProfile(dir), 'core');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips composed profiles "core,audit"', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core,audit');
|
||||
assert.strictEqual(readActiveProfile(dir), 'core,audit');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips "full"', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'full');
|
||||
assert.strictEqual(readActiveProfile(dir), 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('missing marker file returns null (not throws)', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('non-existent directory returns null (not throws)', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-marker-no-exist-' + Date.now());
|
||||
const result = readActiveProfile(ghost);
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test('corrupt marker content (invalid chars) returns null', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-profile'), 'profile with spaces and !!!\n');
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('empty marker file returns null', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-profile'), '');
|
||||
const result = readActiveProfile(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writeActiveProfile creates the directory if it does not exist', () => {
|
||||
const base = tmpDir('gsd-marker-base-');
|
||||
const nested = path.join(base, 'skills', '.claude');
|
||||
try {
|
||||
writeActiveProfile(nested, 'standard');
|
||||
assert.ok(fs.existsSync(nested), 'directory should be created');
|
||||
assert.strictEqual(readActiveProfile(nested), 'standard');
|
||||
} finally {
|
||||
fs.rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('overwrites a previously written profile', () => {
|
||||
const dir = tmpDir('gsd-marker-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeActiveProfile(dir, 'full');
|
||||
assert.strictEqual(readActiveProfile(dir), 'full');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for resolveRuntimeArtifactLayout kind.stage() invocations.
|
||||
* Verifies each kind type produces the expected staged directory structure.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { resolveRuntimeArtifactLayout } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// A small resolved profile selecting known real skills
|
||||
const CORE_SKILLS = new Set(['help', 'phase', 'new-project']);
|
||||
const CORE_AGENTS = new Set(['gsd-planner']);
|
||||
const PROFILE_CORE = { skills: CORE_SKILLS, agents: CORE_AGENTS };
|
||||
const PROFILE_FULL = { skills: '*', agents: new Set() };
|
||||
|
||||
const FAKE_DIR = '/tmp/fake-config-dir-stage';
|
||||
|
||||
describe('commands kind — stage (gemini)', () => {
|
||||
test('stage returns a directory containing only the selected skill .md files', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('gemini', FAKE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'should have a commands kind');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_CORE);
|
||||
// stagedDir is managed by stageSkillsForProfile — just verify, no manual cleanup needed
|
||||
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
// All staged files must come from the selected skill set
|
||||
for (const entry of entries) {
|
||||
const stem = entry.slice(0, -3);
|
||||
assert.ok(CORE_SKILLS.has(stem), `unexpected skill staged: ${stem}`);
|
||||
}
|
||||
// At least one file must be present (help.md exists in real commands/gsd)
|
||||
assert.ok(entries.length >= 1, 'at least one skill file should be staged');
|
||||
});
|
||||
});
|
||||
|
||||
describe('agents kind — stage (claude local)', () => {
|
||||
test('stage returns a directory containing only the selected agent .md files', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'local');
|
||||
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
|
||||
assert.ok(agentsKind, 'should have an agents kind');
|
||||
|
||||
const stagedDir = agentsKind.stage(PROFILE_CORE);
|
||||
// stagedDir may be empty (agents Set from core profile may be empty set)
|
||||
// But it must be a valid directory
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
assert.ok(fs.statSync(stagedDir).isDirectory(), 'stagedDir must be a directory');
|
||||
});
|
||||
});
|
||||
|
||||
describe('skills kind — stage (claude global)', () => {
|
||||
test('stage returns a directory containing gsd-<stem>/SKILL.md entries', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global');
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'should have a skills kind');
|
||||
|
||||
const stagedDir = skillsKind.stage(PROFILE_CORE);
|
||||
// managed by stageSkillsForRuntimeAsSkills — no manual cleanup needed
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
// Each entry should be a directory named gsd-<stem>
|
||||
for (const entry of entries) {
|
||||
assert.ok(entry.startsWith('gsd-'), `entry should start with gsd-: ${entry}`);
|
||||
const skillMd = path.join(stagedDir, entry, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`);
|
||||
}
|
||||
assert.ok(entries.length >= 1, 'at least one skill dir should be staged');
|
||||
});
|
||||
});
|
||||
|
||||
describe('skills kind — stage with full profile', () => {
|
||||
test('stage with skills="*" stages all commands/gsd/*.md as skills', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global');
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'should have a skills kind');
|
||||
|
||||
const stagedDir = skillsKind.stage(PROFILE_FULL);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
// Full profile: should have many skills
|
||||
assert.ok(entries.length > 10, `full profile should have many skills, got ${entries.length}`);
|
||||
for (const entry of entries) {
|
||||
assert.ok(entry.startsWith('gsd-'), `entry should start with gsd-: ${entry}`);
|
||||
const skillMd = path.join(stagedDir, entry, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('opencode commands kind — stage', () => {
|
||||
test('opencode stage returns directory with .md files for selected skills', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('opencode', FAKE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'should have a commands kind');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_CORE);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
for (const entry of entries) {
|
||||
const stem = entry.slice(0, -3);
|
||||
assert.ok(CORE_SKILLS.has(stem), `unexpected skill staged: ${stem}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
795
tests/runtime-artifact-layout-surface.test.cjs
Normal file
795
tests/runtime-artifact-layout-surface.test.cjs
Normal file
@@ -0,0 +1,795 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Runtime Artifact Layout Module (ADR-3660) — surface seam.
|
||||
* Consolidated from: surface-apply, surface-resolve, surface-state,
|
||||
* surface-clusters, surface-list (5 files deleted).
|
||||
* See also: runtime-artifact-layout.test.cjs, runtime-artifact-layout-install-profiles.test.cjs
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { writeSurface, readSurface, resolveSurface, listSurface, applySurface } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
const { loadSkillsManifest, writeActiveProfile, resolveProfile } = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { resolveRuntimeArtifactLayout } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
const { CLUSTERS, allClusteredSkills } = require('../get-shit-done/bin/lib/clusters.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function tmpDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix || 'gsd-ral-surf-'));
|
||||
}
|
||||
|
||||
function createFixtureRuntime() {
|
||||
const base = createTempDir('gsd-surface-apply-');
|
||||
const runtimeConfigDir = base;
|
||||
const commandsDir = path.join(runtimeConfigDir, 'commands', 'gsd');
|
||||
const agentsDir = path.join(runtimeConfigDir, 'agents');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
return { base, runtimeConfigDir, commandsDir, agentsDir };
|
||||
}
|
||||
|
||||
function realManifest() {
|
||||
return loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
}
|
||||
|
||||
function readFrontmatterDescription(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
if (lines[0].trim() !== '---') return '';
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '---') break;
|
||||
const sep = line.indexOf(':');
|
||||
if (sep === -1) continue;
|
||||
const key = line.slice(0, sep).trim();
|
||||
if (key !== 'description') continue;
|
||||
return line.slice(sep + 1).trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ─── applySurface ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('applySurface', () => {
|
||||
test('core profile: only core skills appear in commandsDir', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
const resolved = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
assert.ok(fs.existsSync(path.join(REAL_COMMANDS_DIR, file)), `unexpected file: ${file}`);
|
||||
}
|
||||
const expectedCore = [...resolved.skills].map(stem => `${stem}.md`).sort();
|
||||
assert.deepStrictEqual(
|
||||
[...files].sort(),
|
||||
expectedCore,
|
||||
'commandsDir should contain exactly core commands'
|
||||
);
|
||||
});
|
||||
|
||||
test('removes superseded files when profile shrinks', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
writeActiveProfile(runtimeConfigDir, 'standard');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const afterStandard = new Set(fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')));
|
||||
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const resolvedCore = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const afterCore = new Set(fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')));
|
||||
|
||||
assert.ok(afterCore.size <= afterStandard.size, 'core should have fewer or equal files than standard');
|
||||
|
||||
const expectedCore = [...resolvedCore.skills].map(stem => `${stem}.md`).sort();
|
||||
assert.deepStrictEqual(
|
||||
[...afterCore].sort(),
|
||||
expectedCore,
|
||||
'afterCore should contain exactly core commands'
|
||||
);
|
||||
|
||||
for (const file of afterCore) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(REAL_COMMANDS_DIR, file)),
|
||||
`file in commandsDir not a real skill: ${file}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('leaves non-gsd .md files alone in agentsDir', (t) => {
|
||||
const { base, runtimeConfigDir, agentsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
const foreignAgent = path.join(agentsDir, 'my-custom-agent.md');
|
||||
fs.writeFileSync(foreignAgent, '# custom agent\n', 'utf8');
|
||||
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(fs.existsSync(foreignAgent), 'non-gsd agent file should not be touched');
|
||||
});
|
||||
|
||||
test('adds missing skill files from install source', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(commandsDir, 'help.md')),
|
||||
'help.md should be copied from install source'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(commandsDir, 'new-project.md')),
|
||||
'new-project.md should be copied from install source'
|
||||
);
|
||||
});
|
||||
|
||||
test('_syncGsdDir skills kind: adds missing skill dirs, removes stale prefix-matched dirs, preserves foreign dirs', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
const base = createTempDir('gsd-surface-skills-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
const stem1 = 'gsd-help';
|
||||
const stem2 = 'gsd-update';
|
||||
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
||||
fs.mkdirSync(path.join(stagedDir, stem2), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem2, 'SKILL.md'), '# update\n', 'utf8');
|
||||
|
||||
const staleDir = path.join(destDir, 'gsd-old-skill');
|
||||
fs.mkdirSync(staleDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleDir, 'SKILL.md'), '# old\n', 'utf8');
|
||||
|
||||
const foreignDir = path.join(destDir, 'my-custom-skill');
|
||||
fs.mkdirSync(foreignDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(foreignDir, 'SKILL.md'), '# custom\n', 'utf8');
|
||||
|
||||
const skillsKind = { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', stage: () => stagedDir };
|
||||
_syncGsdDir(stagedDir, destDir, skillsKind);
|
||||
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'gsd-help/SKILL.md should be copied');
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem2, 'SKILL.md')), 'gsd-update/SKILL.md should be copied');
|
||||
assert.ok(!fs.existsSync(staleDir), 'stale gsd-old-skill dir should be removed');
|
||||
assert.ok(fs.existsSync(foreignDir), 'my-custom-skill dir should be preserved');
|
||||
});
|
||||
|
||||
test('applySurface recreates missing destination directories', (t) => {
|
||||
const base = createTempDir('gsd-surface-missing-dest-');
|
||||
t.after(() => cleanup(base));
|
||||
const runtimeConfigDir = base;
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const commandsDir = path.join(runtimeConfigDir, 'commands', 'gsd');
|
||||
assert.ok(fs.existsSync(commandsDir), 'commands/gsd dir should be created even if initially absent');
|
||||
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
||||
assert.ok(files.length > 0, 'commands/gsd should contain staged skill files');
|
||||
assert.ok(files.includes('help.md'), 'help.md should be present after applySurface on missing dest');
|
||||
});
|
||||
|
||||
test('Hermes profile shrink: stale GSD skill dirs are removed; user skills preserved', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
const base = createTempDir('gsd-surface-hermes-shrink-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
fs.mkdirSync(path.join(stagedDir, 'gsd-executor'), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
||||
|
||||
fs.mkdirSync(path.join(destDir, 'gsd-executor'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
||||
fs.mkdirSync(path.join(destDir, 'gsd-planner'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'gsd-planner', 'SKILL.md'), '# planner\n', 'utf8');
|
||||
fs.mkdirSync(path.join(destDir, 'user-skill'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'user-skill', 'SKILL.md'), '# user\n', 'utf8');
|
||||
|
||||
const manifest = new Map([
|
||||
['gsd-executor', []],
|
||||
['gsd-planner', []],
|
||||
]);
|
||||
|
||||
const hermesKind = { kind: 'skills', destSubpath: 'skills/gsd', prefix: '', stage: () => stagedDir };
|
||||
_syncGsdDir(stagedDir, destDir, hermesKind, manifest);
|
||||
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(destDir, 'gsd-executor', 'SKILL.md')),
|
||||
'gsd-executor should be kept (in staged set)'
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(destDir, 'gsd-planner')),
|
||||
'gsd-planner should be removed (in manifest but not in staged set — stale GSD skill)'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(destDir, 'user-skill', 'SKILL.md')),
|
||||
'user-skill should be preserved (not in manifest — user-owned)'
|
||||
);
|
||||
});
|
||||
|
||||
test('_syncGsdDir skills kind (hermes): preserves non-GSD user dir under skills/gsd/ when kindPrefix is empty', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
const base = createTempDir('gsd-surface-hermes-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
const stem1 = 'help';
|
||||
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
||||
|
||||
const userDir = path.join(destDir, 'user-custom-skill');
|
||||
fs.mkdirSync(userDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(userDir, 'SKILL.md'), '# user custom\n', 'utf8');
|
||||
|
||||
const hermesKind = { kind: 'skills', destSubpath: 'skills/gsd', prefix: '', stage: () => stagedDir };
|
||||
_syncGsdDir(stagedDir, destDir, hermesKind);
|
||||
|
||||
assert.ok(fs.existsSync(userDir), 'user-custom-skill dir must be preserved when kindPrefix is empty (Hermes)');
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'GSD help/SKILL.md must be copied');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveSurface ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('resolveSurface', () => {
|
||||
test('no surface state + core base profile → identical to resolveProfile core', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = realManifest();
|
||||
const surfaceResolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
const profileResolved = resolveProfile({ modes: ['core'], manifest });
|
||||
|
||||
assert.ok(surfaceResolved.skills instanceof Set);
|
||||
assert.ok(profileResolved.skills instanceof Set);
|
||||
assert.deepStrictEqual(
|
||||
[...surfaceResolved.skills].sort(),
|
||||
[...profileResolved.skills].sort(),
|
||||
'surface with no state should equal profile resolution'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('standard base + disabledClusters:["utility"] removes utility skills', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'standard');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
for (const stem of CLUSTERS.utility) {
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
if (standardResolved.skills.has(stem)) {
|
||||
assert.ok(
|
||||
!resolved.skills.has(stem),
|
||||
`"${stem}" should be removed by disabling utility cluster`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('explicitAdds:["sketch"] adds sketch to a core install', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: ['sketch'],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
assert.ok(resolved.skills.has('sketch'), 'sketch must be in resolved skills');
|
||||
|
||||
const sketchRequires = manifest.get('sketch') || [];
|
||||
for (const dep of sketchRequires) {
|
||||
assert.ok(resolved.skills.has(dep), `transitive dep "${dep}" of sketch must be present`);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('explicitRemoves removes individual skill stems', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: ['progress'],
|
||||
});
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(!resolved.skills.has('progress'), '"progress" must be removed by explicitRemoves');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('result is a Set<string> with name property and agents Set', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
assert.ok(typeof resolved.name === 'string');
|
||||
assert.ok(resolved.agents instanceof Set);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('surface with baseProfile overrides .gsd-profile marker', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
|
||||
assert.deepStrictEqual(
|
||||
[...resolved.skills].sort(),
|
||||
[...standardResolved.skills].sort(),
|
||||
'surface baseProfile takes precedence over marker'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('disabled cluster + explicitAdds can re-add specific skills from disabled cluster', () => {
|
||||
const dir = tmpDir('gsd-surface-resolve-');
|
||||
try {
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['workspace_state'],
|
||||
explicitAdds: ['capture'],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills.has('capture'), '"capture" must be present via explicitAdds');
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
for (const stem of CLUSTERS.workspace_state) {
|
||||
if (stem === 'capture') continue;
|
||||
if (standardResolved.skills.has(stem)) {
|
||||
assert.ok(
|
||||
!resolved.skills.has(stem),
|
||||
`"${stem}" should be removed (workspace_state disabled, not explicitly re-added)`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── readSurface / writeSurface ──────────────────────────────────────────────
|
||||
|
||||
describe('readSurface / writeSurface', () => {
|
||||
test('round-trips a complete surface state', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: ['sketch'],
|
||||
explicitRemoves: [],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
const read = readSurface(dir);
|
||||
assert.deepStrictEqual(read, state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips empty arrays', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
assert.deepStrictEqual(readSurface(dir), state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips composed base profile', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'core,audit',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: ['health'],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
assert.deepStrictEqual(readSurface(dir), state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('missing file returns null', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('non-existent directory returns null', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-surface-no-exist-' + Date.now());
|
||||
const result = readSurface(ghost);
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test('corrupt JSON returns null', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-surface.json'), '{not valid json', 'utf8');
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('JSON missing baseProfile field returns null', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.gsd-surface.json'),
|
||||
JSON.stringify({ disabledClusters: [], explicitAdds: [], explicitRemoves: [] }),
|
||||
'utf8'
|
||||
);
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('JSON with non-array disabledClusters returns null', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.gsd-surface.json'),
|
||||
JSON.stringify({ baseProfile: 'standard', disabledClusters: 'utility', explicitAdds: [], explicitRemoves: [] }),
|
||||
'utf8'
|
||||
);
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('atomic write: result file is never a partial tmp file', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
const state = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
||||
writeSurface(dir, state);
|
||||
const files = fs.readdirSync(dir);
|
||||
const tmpFiles = files.filter(f => f.includes('.tmp.'));
|
||||
assert.deepStrictEqual(tmpFiles, [], 'no tmp files should remain after write');
|
||||
assert.ok(files.includes('.gsd-surface.json'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('second write overwrites first', () => {
|
||||
const dir = tmpDir('gsd-surface-state-');
|
||||
try {
|
||||
writeSurface(dir, { baseProfile: 'core', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
||||
writeSurface(dir, { baseProfile: 'standard', disabledClusters: ['utility'], explicitAdds: [], explicitRemoves: [] });
|
||||
const read = readSurface(dir);
|
||||
assert.strictEqual(read.baseProfile, 'standard');
|
||||
assert.deepStrictEqual(read.disabledClusters, ['utility']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writeSurface creates directory if it does not exist', () => {
|
||||
const base = tmpDir('gsd-surface-state-');
|
||||
const nested = path.join(base, 'skills', 'subdir');
|
||||
try {
|
||||
writeSurface(nested, { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
||||
assert.ok(fs.existsSync(nested));
|
||||
assert.ok(readSurface(nested) !== null);
|
||||
} finally {
|
||||
fs.rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── CLUSTERS data structure ─────────────────────────────────────────────────
|
||||
|
||||
describe('CLUSTERS data structure', () => {
|
||||
test('no cluster is empty', () => {
|
||||
for (const [name, members] of Object.entries(CLUSTERS)) {
|
||||
assert.ok(members.length > 0, `cluster ${name} must not be empty`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every cluster member is a real skill stem in commands/gsd/', () => {
|
||||
const entries = fs.readdirSync(REAL_COMMANDS_DIR, { withFileTypes: true });
|
||||
const realStems = new Set(
|
||||
entries
|
||||
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
||||
.map(e => e.name.slice(0, -3))
|
||||
);
|
||||
const mismatches = [];
|
||||
for (const [cluster, members] of Object.entries(CLUSTERS)) {
|
||||
for (const stem of members) {
|
||||
if (!realStems.has(stem)) {
|
||||
mismatches.push(`${cluster}: "${stem}" not found in commands/gsd/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(mismatches, [], `Cluster members missing from disk:\n${mismatches.join('\n')}`);
|
||||
});
|
||||
|
||||
test('union of all clusters covers every skill in commands/gsd/', () => {
|
||||
const entries = fs.readdirSync(REAL_COMMANDS_DIR, { withFileTypes: true });
|
||||
const realStems = new Set(
|
||||
entries
|
||||
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
||||
.map(e => e.name.slice(0, -3))
|
||||
);
|
||||
const clustered = allClusteredSkills();
|
||||
const uncategorized = [];
|
||||
for (const stem of realStems) {
|
||||
if (!clustered.has(stem)) uncategorized.push(stem);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
uncategorized,
|
||||
[],
|
||||
`Uncategorized skills (not in any cluster):\n${uncategorized.sort().join('\n')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('CLUSTERS is frozen (immutable)', () => {
|
||||
assert.ok(Object.isFrozen(CLUSTERS), 'CLUSTERS must be frozen');
|
||||
for (const [name, members] of Object.entries(CLUSTERS)) {
|
||||
assert.ok(Object.isFrozen(members), `CLUSTERS.${name} must be frozen`);
|
||||
}
|
||||
});
|
||||
|
||||
test('cluster names contain the expected set from research memo §3.2', () => {
|
||||
const expectedClusterNames = new Set([
|
||||
'core_loop',
|
||||
'audit_review',
|
||||
'milestone',
|
||||
'research_ideate',
|
||||
'workspace_state',
|
||||
'docs',
|
||||
'ui',
|
||||
'ai_eval',
|
||||
'ns_meta',
|
||||
'utility',
|
||||
]);
|
||||
const actualClusterNames = new Set(Object.keys(CLUSTERS));
|
||||
for (const name of expectedClusterNames) {
|
||||
assert.ok(actualClusterNames.has(name), `expected cluster "${name}" missing from CLUSTERS`);
|
||||
}
|
||||
});
|
||||
|
||||
test('allClusteredSkills returns a Set containing all cluster members', () => {
|
||||
const result = allClusteredSkills();
|
||||
assert.ok(result instanceof Set, 'allClusteredSkills() must return a Set');
|
||||
for (const members of Object.values(CLUSTERS)) {
|
||||
for (const stem of members) {
|
||||
assert.ok(result.has(stem), `allClusteredSkills() missing "${stem}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── listSurface ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('listSurface', () => {
|
||||
test('returns { enabled, disabled, tokenCost } structure', () => {
|
||||
const dir = tmpDir('gsd-surface-list-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(Array.isArray(result.enabled), 'enabled must be array');
|
||||
assert.ok(Array.isArray(result.disabled), 'disabled must be array');
|
||||
assert.ok(typeof result.tokenCost === 'number', 'tokenCost must be number');
|
||||
assert.ok(result.tokenCost >= 0, 'tokenCost must be non-negative');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('core profile: enabled has fewer skills than full; enabled + disabled = total stems', () => {
|
||||
const dir = tmpDir('gsd-surface-list-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const coreList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
const totalStems = [...manifest.keys()].filter(k => !k.startsWith('_calls_agents_')).length;
|
||||
assert.ok(
|
||||
coreList.enabled.length < totalStems,
|
||||
'core should enable fewer skills than total'
|
||||
);
|
||||
assert.ok(coreList.disabled.length > 0, 'core should have some disabled skills');
|
||||
assert.ok(coreList.enabled.length + coreList.disabled.length === totalStems,
|
||||
'enabled + disabled must equal total stems');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('disabling utility cluster reduces enabled count', () => {
|
||||
const dir = tmpDir('gsd-surface-list-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
|
||||
const beforeList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const afterList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(afterList.enabled.length <= beforeList.enabled.length,
|
||||
'disabling utility cluster should not increase enabled count');
|
||||
assert.ok(afterList.tokenCost <= beforeList.tokenCost,
|
||||
'disabling a cluster should not increase token cost');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('tokenCost is sum of description char lengths ÷ 4 for enabled skills', () => {
|
||||
const dir = tmpDir('gsd-surface-list-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
let expected = 0;
|
||||
for (const stem of result.enabled) {
|
||||
const filePath = path.join(REAL_COMMANDS_DIR, `${stem}.md`);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
const markdown = fs.readFileSync(filePath, 'utf8');
|
||||
const description = readFrontmatterDescription(markdown);
|
||||
if (description) expected += Math.ceil(description.length / 4);
|
||||
}
|
||||
|
||||
assert.strictEqual(result.tokenCost, expected, 'tokenCost must equal sum of description lengths ÷ 4');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('enabled and disabled arrays are sorted', () => {
|
||||
const dir = tmpDir('gsd-surface-list-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.deepStrictEqual(result.enabled, [...result.enabled].sort());
|
||||
assert.deepStrictEqual(result.disabled, [...result.disabled].sort());
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,33 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for resolveRuntimeArtifactLayout — structural shape per runtime.
|
||||
* Consolidated tests for the Runtime Artifact Layout Module (ADR-3660) — layout seam.
|
||||
*
|
||||
* Covers:
|
||||
* - resolveRuntimeArtifactLayout — structural shape per runtime
|
||||
* - resolveRuntimeArtifactLayout edge-cases (error paths, invalid input)
|
||||
* - kind.stage() invocations per kind type
|
||||
*
|
||||
* Sources consolidated (3 files deleted):
|
||||
* tests/runtime-artifact-layout-resolve.test.cjs
|
||||
* tests/runtime-artifact-layout-edge-cases.test.cjs
|
||||
* tests/runtime-artifact-layout-stage.test.cjs
|
||||
*
|
||||
* See also:
|
||||
* runtime-artifact-layout-surface.test.cjs — surface seam
|
||||
* runtime-artifact-layout-install-profiles.test.cjs — install-profiles seam
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { resolveRuntimeArtifactLayout } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
|
||||
const FAKE_DIR = '/tmp/fake-config-dir';
|
||||
|
||||
// ─── resolveRuntimeArtifactLayout — structural shape ────────────────────────
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — claude local', () => {
|
||||
test('returns correct layout for claude scope=local', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'local');
|
||||
@@ -217,3 +235,162 @@ describe('resolveRuntimeArtifactLayout — kilo', () => {
|
||||
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveRuntimeArtifactLayout — edge-cases ──────────────────────────────
|
||||
|
||||
describe('resolveRuntimeArtifactLayout edge-cases', () => {
|
||||
test('hermes has destSubpath skills/gsd and empty prefix', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('hermes', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds[0].destSubpath, 'skills/gsd');
|
||||
assert.strictEqual(layout.kinds[0].prefix, '');
|
||||
});
|
||||
|
||||
test('cline has no kinds', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cline', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds.length, 0);
|
||||
});
|
||||
|
||||
test('gemini has one commands kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('gemini', '/tmp/x');
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'commands');
|
||||
});
|
||||
|
||||
test('claude local has both commands and agents kinds', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'local');
|
||||
const kindNames = layout.kinds.map(k => k.kind);
|
||||
assert.ok(kindNames.includes('commands'), 'should have commands kind');
|
||||
assert.ok(kindNames.includes('agents'), 'should have agents kind');
|
||||
});
|
||||
|
||||
test('claude global has only skills kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'global');
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
});
|
||||
|
||||
test('unknown runtime grok throws TypeError containing runtime name', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('grok', '/tmp/x'),
|
||||
(err) => {
|
||||
assert.ok(err instanceof TypeError);
|
||||
assert.ok(err.message.includes('grok'), 'error message must contain the runtime name');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown runtime xyzunknown throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('xyzunknown', '/tmp/x'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
test('empty configDir throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', ''),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
test('non-string configDir throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', null),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
test('bad scope throws TypeError', () => {
|
||||
assert.throws(
|
||||
() => resolveRuntimeArtifactLayout('claude', '/x', 'invalid'),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── kind.stage() invocations ────────────────────────────────────────────────
|
||||
|
||||
const CORE_SKILLS = new Set(['help', 'phase', 'new-project']);
|
||||
const CORE_AGENTS = new Set(['gsd-planner']);
|
||||
const PROFILE_CORE = { skills: CORE_SKILLS, agents: CORE_AGENTS };
|
||||
const PROFILE_FULL = { skills: '*', agents: new Set() };
|
||||
const FAKE_STAGE_DIR = '/tmp/fake-config-dir-stage';
|
||||
|
||||
describe('stage — commands kind (gemini)', () => {
|
||||
test('stage returns a directory containing only the selected skill .md files', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('gemini', FAKE_STAGE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'should have a commands kind');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_CORE);
|
||||
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
for (const entry of entries) {
|
||||
const stem = entry.slice(0, -3);
|
||||
assert.ok(CORE_SKILLS.has(stem), `unexpected skill staged: ${stem}`);
|
||||
}
|
||||
assert.ok(entries.length >= 1, 'at least one skill file should be staged');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage — agents kind (claude local)', () => {
|
||||
test('stage returns a valid directory for the agents kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'local');
|
||||
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
|
||||
assert.ok(agentsKind, 'should have an agents kind');
|
||||
|
||||
const stagedDir = agentsKind.stage(PROFILE_CORE);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
assert.ok(fs.statSync(stagedDir).isDirectory(), 'stagedDir must be a directory');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage — skills kind (claude global)', () => {
|
||||
test('stage returns a directory containing gsd-<stem>/SKILL.md entries', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'global');
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'should have a skills kind');
|
||||
|
||||
const stagedDir = skillsKind.stage(PROFILE_CORE);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
for (const entry of entries) {
|
||||
assert.ok(entry.startsWith('gsd-'), `entry should start with gsd-: ${entry}`);
|
||||
const skillMd = path.join(stagedDir, entry, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`);
|
||||
}
|
||||
assert.ok(entries.length >= 1, 'at least one skill dir should be staged');
|
||||
});
|
||||
|
||||
test('stage with skills="*" stages all commands/gsd/*.md as skills', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', FAKE_STAGE_DIR, 'global');
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'should have a skills kind');
|
||||
|
||||
const stagedDir = skillsKind.stage(PROFILE_FULL);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir);
|
||||
assert.ok(entries.length > 10, `full profile should have many skills, got ${entries.length}`);
|
||||
for (const entry of entries) {
|
||||
assert.ok(entry.startsWith('gsd-'), `entry should start with gsd-: ${entry}`);
|
||||
const skillMd = path.join(stagedDir, entry, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage — opencode commands kind', () => {
|
||||
test('opencode stage returns directory with .md files for selected skills', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('opencode', FAKE_STAGE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'should have a commands kind');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_CORE);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
for (const entry of entries) {
|
||||
const stem = entry.slice(0, -3);
|
||||
assert.ok(CORE_SKILLS.has(stem), `unexpected skill staged: ${stem}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,316 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for applySurface — file sync behavior.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { writeSurface, applySurface } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
const { loadSkillsManifest, writeActiveProfile } = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { CLUSTERS } = require('../get-shit-done/bin/lib/clusters.cjs');
|
||||
const { resolveRuntimeArtifactLayout } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const REAL_AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
/**
|
||||
* Create a minimal fixture install dir structure for claude/local layout.
|
||||
* runtimeConfigDir is the layout configDir.
|
||||
* commandsDir = runtimeConfigDir/commands/gsd
|
||||
* agentsDir = runtimeConfigDir/agents
|
||||
*/
|
||||
function createFixtureRuntime() {
|
||||
const base = createTempDir('gsd-surface-apply-');
|
||||
const runtimeConfigDir = base;
|
||||
const commandsDir = path.join(runtimeConfigDir, 'commands', 'gsd');
|
||||
const agentsDir = path.join(runtimeConfigDir, 'agents');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
return { base, runtimeConfigDir, commandsDir, agentsDir };
|
||||
}
|
||||
|
||||
describe('applySurface', () => {
|
||||
test('core profile: only core skills appear in commandsDir', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir, agentsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
const resolved = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
||||
// Every file should be a real stem we know about
|
||||
for (const file of files) {
|
||||
assert.ok(fs.existsSync(path.join(REAL_COMMANDS_DIR, file)), `unexpected file: ${file}`);
|
||||
}
|
||||
// Core profile should materialize exactly the resolved core command set.
|
||||
const expectedCore = [...resolved.skills].map(stem => `${stem}.md`).sort();
|
||||
assert.deepStrictEqual(
|
||||
[...files].sort(),
|
||||
expectedCore,
|
||||
'commandsDir should contain exactly core commands'
|
||||
);
|
||||
});
|
||||
|
||||
test('removes superseded files when profile shrinks', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir, agentsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
// Start with standard: put some skill files in commandsDir
|
||||
writeActiveProfile(runtimeConfigDir, 'standard');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const afterStandard = new Set(fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')));
|
||||
|
||||
// Now switch to core: skills not in core should be removed
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const resolvedCore = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
const afterCore = new Set(fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')));
|
||||
|
||||
// core should be a subset of standard
|
||||
assert.ok(afterCore.size <= afterStandard.size, 'core should have fewer or equal files than standard');
|
||||
|
||||
// Core profile should materialize exactly the resolved core command set.
|
||||
const expectedCore = [...resolvedCore.skills].map(stem => `${stem}.md`).sort();
|
||||
assert.deepStrictEqual(
|
||||
[...afterCore].sort(),
|
||||
expectedCore,
|
||||
'afterCore should contain exactly core commands'
|
||||
);
|
||||
|
||||
// All files should still map to known real skills.
|
||||
for (const file of afterCore) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(REAL_COMMANDS_DIR, file)),
|
||||
`file in commandsDir not a real skill: ${file}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('leaves non-gsd .md files alone in agentsDir', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir, agentsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
// Place a non-gsd agent file in agentsDir
|
||||
const foreignAgent = path.join(agentsDir, 'my-custom-agent.md');
|
||||
fs.writeFileSync(foreignAgent, '# custom agent\n', 'utf8');
|
||||
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
// Non-gsd file should still be there
|
||||
assert.ok(fs.existsSync(foreignAgent), 'non-gsd agent file should not be touched');
|
||||
});
|
||||
|
||||
test('adds missing skill files from install source', (t) => {
|
||||
const { base, runtimeConfigDir, commandsDir, agentsDir } = createFixtureRuntime();
|
||||
t.after(() => cleanup(base));
|
||||
// commandsDir starts empty
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
// Core skills should now be present
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(commandsDir, 'help.md')),
|
||||
'help.md should be copied from install source'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(commandsDir, 'new-project.md')),
|
||||
'new-project.md should be copied from install source'
|
||||
);
|
||||
});
|
||||
|
||||
test('_syncGsdDir skills kind: adds missing skill dirs, removes stale prefix-matched dirs, preserves foreign dirs', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
const { stageSkillsForRuntimeAsSkills } = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { findInstallSourceRoot } = require('../get-shit-done/bin/lib/runtime-artifact-layout.cjs');
|
||||
// Minimal converter that produces SKILL.md with given stem
|
||||
function converter(stem, content) {
|
||||
return [
|
||||
'---',
|
||||
`name: ${stem}`,
|
||||
'---',
|
||||
content,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const base = createTempDir('gsd-surface-skills-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Build a staged dir manually: gsd-help/SKILL.md and gsd-update/SKILL.md
|
||||
const stem1 = 'gsd-help';
|
||||
const stem2 = 'gsd-update';
|
||||
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
||||
fs.mkdirSync(path.join(stagedDir, stem2), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem2, 'SKILL.md'), '# update\n', 'utf8');
|
||||
|
||||
// In destDir: stale gsd- dir + foreign user dir
|
||||
const staleDir = path.join(destDir, 'gsd-old-skill');
|
||||
fs.mkdirSync(staleDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleDir, 'SKILL.md'), '# old\n', 'utf8');
|
||||
|
||||
const foreignDir = path.join(destDir, 'my-custom-skill');
|
||||
fs.mkdirSync(foreignDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(foreignDir, 'SKILL.md'), '# custom\n', 'utf8');
|
||||
|
||||
const skillsKind = { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', stage: () => stagedDir };
|
||||
|
||||
_syncGsdDir(stagedDir, destDir, skillsKind);
|
||||
|
||||
// staged dirs copied
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'gsd-help/SKILL.md should be copied');
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem2, 'SKILL.md')), 'gsd-update/SKILL.md should be copied');
|
||||
|
||||
// stale gsd- dir removed
|
||||
assert.ok(!fs.existsSync(staleDir), 'stale gsd-old-skill dir should be removed');
|
||||
|
||||
// foreign dir preserved
|
||||
assert.ok(fs.existsSync(foreignDir), 'my-custom-skill dir should be preserved');
|
||||
});
|
||||
|
||||
test('applySurface recreates missing destination directories', (t) => {
|
||||
// Fixture: layout configDir exists but the dest subdirectory for the kinds does NOT.
|
||||
const base = createTempDir('gsd-surface-missing-dest-');
|
||||
t.after(() => cleanup(base));
|
||||
const runtimeConfigDir = base;
|
||||
// Do NOT pre-create commands/gsd or agents — they are intentionally absent.
|
||||
writeActiveProfile(runtimeConfigDir, 'core');
|
||||
writeSurface(runtimeConfigDir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
||||
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
||||
|
||||
// commands/gsd must have been created and populated
|
||||
const commandsDir = path.join(runtimeConfigDir, 'commands', 'gsd');
|
||||
assert.ok(fs.existsSync(commandsDir), 'commands/gsd dir should be created even if initially absent');
|
||||
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
||||
assert.ok(files.length > 0, 'commands/gsd should contain staged skill files');
|
||||
assert.ok(files.includes('help.md'), 'help.md should be present after applySurface on missing dest');
|
||||
});
|
||||
|
||||
test('Hermes profile shrink: stale GSD skill dirs are removed; user skills preserved', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
const base = createTempDir('gsd-surface-hermes-shrink-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Staged: only gsd-executor (profile shrunk — gsd-planner no longer in profile)
|
||||
fs.mkdirSync(path.join(stagedDir, 'gsd-executor'), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
||||
|
||||
// Dest already has: gsd-executor (keep), gsd-planner (stale GSD), user-skill (user-owned)
|
||||
fs.mkdirSync(path.join(destDir, 'gsd-executor'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
||||
fs.mkdirSync(path.join(destDir, 'gsd-planner'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'gsd-planner', 'SKILL.md'), '# planner\n', 'utf8');
|
||||
fs.mkdirSync(path.join(destDir, 'user-skill'), { recursive: true });
|
||||
fs.writeFileSync(path.join(destDir, 'user-skill', 'SKILL.md'), '# user\n', 'utf8');
|
||||
|
||||
// Manifest contains gsd-executor and gsd-planner as canonical GSD skills.
|
||||
// user-skill is NOT in manifest (user-owned).
|
||||
const manifest = new Map([
|
||||
['gsd-executor', []],
|
||||
['gsd-planner', []],
|
||||
]);
|
||||
|
||||
// Hermes kind: empty prefix, destSubpath = skills/gsd
|
||||
const hermesKind = { kind: 'skills', destSubpath: 'skills/gsd', prefix: '', stage: () => stagedDir };
|
||||
|
||||
_syncGsdDir(stagedDir, destDir, hermesKind, manifest);
|
||||
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(destDir, 'gsd-executor', 'SKILL.md')),
|
||||
'gsd-executor should be kept (in staged set)'
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(destDir, 'gsd-planner')),
|
||||
'gsd-planner should be removed (in manifest but not in staged set — stale GSD skill)'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(destDir, 'user-skill', 'SKILL.md')),
|
||||
'user-skill should be preserved (not in manifest — user-owned)'
|
||||
);
|
||||
});
|
||||
|
||||
test('_syncGsdDir skills kind (hermes): preserves non-GSD user dir under skills/gsd/ when kindPrefix is empty', (t) => {
|
||||
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
const base = createTempDir('gsd-surface-hermes-');
|
||||
t.after(() => cleanup(base));
|
||||
const stagedDir = path.join(base, 'staged');
|
||||
const destDir = path.join(base, 'dest');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Staged contains a GSD skill named 'help' (no prefix under hermes skills/gsd/)
|
||||
const stem1 = 'help';
|
||||
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
||||
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
||||
|
||||
// Dest also has a user-owned custom skill dir (no gsd- prefix — Hermes namespace)
|
||||
const userDir = path.join(destDir, 'user-custom-skill');
|
||||
fs.mkdirSync(userDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(userDir, 'SKILL.md'), '# user custom\n', 'utf8');
|
||||
|
||||
// kindPrefix === '' simulates Hermes (destSubpath = skills/gsd, prefix = '')
|
||||
const hermesKind = { kind: 'skills', destSubpath: 'skills/gsd', prefix: '', stage: () => stagedDir };
|
||||
|
||||
_syncGsdDir(stagedDir, destDir, hermesKind);
|
||||
|
||||
// The user's custom skill dir must be preserved — it's not in staged but should not be removed
|
||||
// (Fix 4: when kindPrefix === '', skip the startsWith guard and preserve ALL non-staged dirs)
|
||||
assert.ok(fs.existsSync(userDir), 'user-custom-skill dir must be preserved when kindPrefix is empty (Hermes)');
|
||||
|
||||
// The staged skill must still be copied
|
||||
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'GSD help/SKILL.md must be copied');
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for CLUSTERS data structure integrity.
|
||||
* Verifies every cluster member is a real skill stem and all skills are covered.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { CLUSTERS, allClusteredSkills } = require('../get-shit-done/bin/lib/clusters.cjs');
|
||||
|
||||
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
function realSkillStems() {
|
||||
const entries = fs.readdirSync(COMMANDS_DIR, { withFileTypes: true });
|
||||
return new Set(
|
||||
entries
|
||||
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
||||
.map(e => e.name.slice(0, -3))
|
||||
);
|
||||
}
|
||||
|
||||
describe('CLUSTERS data structure', () => {
|
||||
test('no cluster is empty', () => {
|
||||
for (const [name, members] of Object.entries(CLUSTERS)) {
|
||||
assert.ok(members.length > 0, `cluster ${name} must not be empty`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every cluster member is a real skill stem in commands/gsd/', () => {
|
||||
const realStems = realSkillStems();
|
||||
const mismatches = [];
|
||||
for (const [cluster, members] of Object.entries(CLUSTERS)) {
|
||||
for (const stem of members) {
|
||||
if (!realStems.has(stem)) {
|
||||
mismatches.push(`${cluster}: "${stem}" not found in commands/gsd/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(mismatches, [], `Cluster members missing from disk:\n${mismatches.join('\n')}`);
|
||||
});
|
||||
|
||||
test('union of all clusters covers every skill in commands/gsd/', () => {
|
||||
const realStems = realSkillStems();
|
||||
const clustered = allClusteredSkills();
|
||||
const uncategorized = [];
|
||||
for (const stem of realStems) {
|
||||
if (!clustered.has(stem)) uncategorized.push(stem);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
uncategorized,
|
||||
[],
|
||||
`Uncategorized skills (not in any cluster):\n${uncategorized.sort().join('\n')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('CLUSTERS is frozen (immutable)', () => {
|
||||
assert.ok(Object.isFrozen(CLUSTERS), 'CLUSTERS must be frozen');
|
||||
for (const [name, members] of Object.entries(CLUSTERS)) {
|
||||
assert.ok(Object.isFrozen(members), `CLUSTERS.${name} must be frozen`);
|
||||
}
|
||||
});
|
||||
|
||||
test('cluster names match expected set from research memo §3.2', () => {
|
||||
const expectedClusterNames = new Set([
|
||||
'core_loop',
|
||||
'audit_review',
|
||||
'milestone',
|
||||
'research_ideate',
|
||||
'workspace_state',
|
||||
'docs',
|
||||
'ui',
|
||||
'ai_eval',
|
||||
'ns_meta',
|
||||
'utility',
|
||||
]);
|
||||
const actualClusterNames = new Set(Object.keys(CLUSTERS));
|
||||
for (const name of expectedClusterNames) {
|
||||
assert.ok(actualClusterNames.has(name), `expected cluster "${name}" missing from CLUSTERS`);
|
||||
}
|
||||
});
|
||||
|
||||
test('allClusteredSkills returns a Set containing all cluster members', () => {
|
||||
const result = allClusteredSkills();
|
||||
assert.ok(result instanceof Set, 'allClusteredSkills() must return a Set');
|
||||
for (const members of Object.values(CLUSTERS)) {
|
||||
for (const stem of members) {
|
||||
assert.ok(result.has(stem), `allClusteredSkills() missing "${stem}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for listSurface — enabled/disabled/tokenCost output.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { writeSurface, listSurface } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
const { loadSkillsManifest, writeActiveProfile } = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { CLUSTERS } = require('../get-shit-done/bin/lib/clusters.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
function tmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-surface-list-'));
|
||||
}
|
||||
|
||||
function readFrontmatterDescription(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
if (lines[0].trim() !== '---') return '';
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '---') break;
|
||||
const sep = line.indexOf(':');
|
||||
if (sep === -1) continue;
|
||||
const key = line.slice(0, sep).trim();
|
||||
if (key !== 'description') continue;
|
||||
return line.slice(sep + 1).trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
describe('listSurface', () => {
|
||||
test('returns { enabled, disabled, tokenCost } structure', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
// Write source marker so listSurface can find descriptions
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(Array.isArray(result.enabled), 'enabled must be array');
|
||||
assert.ok(Array.isArray(result.disabled), 'disabled must be array');
|
||||
assert.ok(typeof result.tokenCost === 'number', 'tokenCost must be number');
|
||||
assert.ok(result.tokenCost >= 0, 'tokenCost must be non-negative');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('core profile: enabled has fewer skills than full', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const coreList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
// Core should have fewer enabled skills than total
|
||||
const totalStems = [...manifest.keys()].filter(k => !k.startsWith('_calls_agents_')).length;
|
||||
assert.ok(
|
||||
coreList.enabled.length < totalStems,
|
||||
'core should enable fewer skills than total'
|
||||
);
|
||||
assert.ok(coreList.disabled.length > 0, 'core should have some disabled skills');
|
||||
assert.ok(coreList.enabled.length + coreList.disabled.length === totalStems,
|
||||
'enabled + disabled must equal total stems');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('disabling utility cluster reduces enabled count', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
|
||||
const beforeList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const afterList = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(afterList.enabled.length <= beforeList.enabled.length,
|
||||
'disabling utility cluster should not increase enabled count');
|
||||
assert.ok(afterList.tokenCost <= beforeList.tokenCost,
|
||||
'disabling a cluster should not increase token cost');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('tokenCost is sum of description char lengths ÷ 4 for enabled skills', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
// Manually compute expected token cost for enabled skills
|
||||
let expected = 0;
|
||||
for (const stem of result.enabled) {
|
||||
const filePath = path.join(REAL_COMMANDS_DIR, `${stem}.md`);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
const markdown = fs.readFileSync(filePath, 'utf8');
|
||||
const description = readFrontmatterDescription(markdown);
|
||||
if (description) expected += Math.ceil(description.length / 4);
|
||||
}
|
||||
|
||||
assert.strictEqual(result.tokenCost, expected, 'tokenCost must equal sum of description lengths ÷ 4');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('enabled and disabled arrays are sorted', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
const result = listSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.deepStrictEqual(result.enabled, [...result.enabled].sort());
|
||||
assert.deepStrictEqual(result.disabled, [...result.disabled].sort());
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,194 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for resolveSurface — profile + cluster + explicit combinations.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { readSurface, writeSurface, resolveSurface } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
const { resolveProfile, loadSkillsManifest, writeActiveProfile } = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
||||
const { CLUSTERS } = require('../get-shit-done/bin/lib/clusters.cjs');
|
||||
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
|
||||
function tmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-surface-resolve-'));
|
||||
}
|
||||
|
||||
function realManifest() {
|
||||
return loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
}
|
||||
|
||||
describe('resolveSurface', () => {
|
||||
test('no surface state + core base profile → identical to resolveProfile core', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = realManifest();
|
||||
const surfaceResolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
const profileResolved = resolveProfile({ modes: ['core'], manifest });
|
||||
|
||||
// Both should have same skill sets
|
||||
assert.ok(surfaceResolved.skills instanceof Set);
|
||||
assert.ok(profileResolved.skills instanceof Set);
|
||||
assert.deepStrictEqual(
|
||||
[...surfaceResolved.skills].sort(),
|
||||
[...profileResolved.skills].sort(),
|
||||
'surface with no state should equal profile resolution'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('standard base + disabledClusters:["utility"] removes utility skills', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeActiveProfile(dir, 'standard');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
// Utility cluster members should not be in the result
|
||||
for (const stem of CLUSTERS.utility) {
|
||||
// Only check stems that were actually in the standard profile
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
if (standardResolved.skills.has(stem)) {
|
||||
assert.ok(
|
||||
!resolved.skills.has(stem),
|
||||
`"${stem}" should be removed by disabling utility cluster`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('explicitAdds:["sketch"] adds sketch to a core install', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: ['sketch'],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
assert.ok(resolved.skills.has('sketch'), 'sketch must be in resolved skills');
|
||||
|
||||
// Transitive requires of sketch should also be present
|
||||
const sketchRequires = manifest.get('sketch') || [];
|
||||
for (const dep of sketchRequires) {
|
||||
assert.ok(resolved.skills.has(dep), `transitive dep "${dep}" of sketch must be present`);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('explicitRemoves removes individual skill stems', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: ['progress'],
|
||||
});
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
// standard includes 'progress', so removing it should take it out
|
||||
assert.ok(!resolved.skills.has('progress'), '"progress" must be removed by explicitRemoves');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('result is a Set<string> with name property', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
assert.ok(resolved.skills instanceof Set);
|
||||
assert.ok(typeof resolved.name === 'string');
|
||||
assert.ok(resolved.agents instanceof Set);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('surface with baseProfile overrides .gsd-profile marker', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeActiveProfile(dir, 'core');
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
|
||||
// Should use standard profile from surface state, not core from marker
|
||||
assert.deepStrictEqual(
|
||||
[...resolved.skills].sort(),
|
||||
[...standardResolved.skills].sort(),
|
||||
'surface baseProfile takes precedence over marker'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('disabled cluster + explicitAdds can re-add specific skills from disabled cluster', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeSurface(dir, {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['workspace_state'],
|
||||
explicitAdds: ['capture'],
|
||||
explicitRemoves: [],
|
||||
});
|
||||
writeActiveProfile(dir, 'standard');
|
||||
const manifest = realManifest();
|
||||
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
||||
|
||||
// workspace_state is disabled, but capture is explicitly re-added
|
||||
assert.ok(resolved.skills.has('capture'), '"capture" must be present via explicitAdds');
|
||||
// Other workspace_state members that were in standard should be gone
|
||||
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
||||
for (const stem of CLUSTERS.workspace_state) {
|
||||
if (stem === 'capture') continue;
|
||||
if (standardResolved.skills.has(stem)) {
|
||||
assert.ok(
|
||||
!resolved.skills.has(stem),
|
||||
`"${stem}" should be removed (workspace_state disabled, not explicitly re-added)`
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Tests for readSurface / writeSurface — state IO round-trips.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { readSurface, writeSurface } = require('../get-shit-done/bin/lib/surface.cjs');
|
||||
|
||||
function tmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-surface-state-'));
|
||||
}
|
||||
|
||||
describe('readSurface / writeSurface', () => {
|
||||
test('round-trips a complete surface state', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'standard',
|
||||
disabledClusters: ['utility'],
|
||||
explicitAdds: ['sketch'],
|
||||
explicitRemoves: [],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
const read = readSurface(dir);
|
||||
assert.deepStrictEqual(read, state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips empty arrays', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'core',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: [],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
assert.deepStrictEqual(readSurface(dir), state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('round-trips composed base profile', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
const state = {
|
||||
baseProfile: 'core,audit',
|
||||
disabledClusters: [],
|
||||
explicitAdds: [],
|
||||
explicitRemoves: ['health'],
|
||||
};
|
||||
writeSurface(dir, state);
|
||||
assert.deepStrictEqual(readSurface(dir), state);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('missing file returns null', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('non-existent directory returns null', () => {
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-surface-no-exist-' + Date.now());
|
||||
const result = readSurface(ghost);
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test('corrupt JSON returns null', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, '.gsd-surface.json'), '{not valid json', 'utf8');
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('JSON missing baseProfile field returns null', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.gsd-surface.json'),
|
||||
JSON.stringify({ disabledClusters: [], explicitAdds: [], explicitRemoves: [] }),
|
||||
'utf8'
|
||||
);
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('JSON with non-array disabledClusters returns null', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.gsd-surface.json'),
|
||||
JSON.stringify({ baseProfile: 'standard', disabledClusters: 'utility', explicitAdds: [], explicitRemoves: [] }),
|
||||
'utf8'
|
||||
);
|
||||
const result = readSurface(dir);
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('atomic write: result file is never a partial tmp file', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
const state = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
||||
writeSurface(dir, state);
|
||||
// No .tmp.* files should remain
|
||||
const files = fs.readdirSync(dir);
|
||||
const tmpFiles = files.filter(f => f.includes('.tmp.'));
|
||||
assert.deepStrictEqual(tmpFiles, [], 'no tmp files should remain after write');
|
||||
// The canonical file exists
|
||||
assert.ok(files.includes('.gsd-surface.json'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('second write overwrites first', () => {
|
||||
const dir = tmpDir();
|
||||
try {
|
||||
writeSurface(dir, { baseProfile: 'core', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
||||
writeSurface(dir, { baseProfile: 'standard', disabledClusters: ['utility'], explicitAdds: [], explicitRemoves: [] });
|
||||
const read = readSurface(dir);
|
||||
assert.strictEqual(read.baseProfile, 'standard');
|
||||
assert.deepStrictEqual(read.disabledClusters, ['utility']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writeSurface creates directory if it does not exist', () => {
|
||||
const base = tmpDir();
|
||||
const nested = path.join(base, 'skills', 'subdir');
|
||||
try {
|
||||
writeSurface(nested, { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
||||
assert.ok(fs.existsSync(nested));
|
||||
assert.ok(readSurface(nested) !== null);
|
||||
} finally {
|
||||
fs.rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user