Files
msd-core/tests/runtime-artifact-layout-surface.test.cjs
Tom Boucher 8f2ebbe9bf feat(#1928): remove sunset Gemini CLI runtime, redirect to Antigravity (#1996)
* feat(#1928): remove sunset gemini cli runtime, redirect to antigravity

Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its official successor (already a first-class GSD runtime). Remove the gemini runtime from the enum (16->15), aliases, labels, config-home fragment, install path, converters (convertClaudeToGemini{Markdown,Toml,Agent}, convertSlashCommandsToGeminiMentions), capability descriptor, gemini-extension.json, RULESET.GEMINI.*, and the interactive menu (renumbered, no gap).

--gemini now prints an explicit deprecation notice citing the 2026-06-18 sunset and redirects to --antigravity (no silent alias, per the issue's Hyrum's-Law rejection). Antigravity is preserved throughout: its GEMINI.md contextFileName, .gemini/antigravity config home, the shared convertGeminiToolName/claudeToGeminiTools tool vocabulary, and the 'gemini' hookEvents dialect it declares. GEMINI.md retargeted as Antigravity's context file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): backfill changeset PR number (#1996)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): drop Gemini CLI from issue templates (review nit)

Removes the sunset Gemini CLI runtime from the two GitHub issue-template
runtime lists that the removal PR missed, per @davesienkowski's review nit:
- feature_request.yml: 'Applicable runtimes' checkbox (a user could otherwise
  request a feature for a runtime GSD no longer supports)
- bug_report.yml: 'Runtime' dropdown + the stale ~/.gemini/settings.json
  retrieval-help line

Leaves the post-removal templates fully consistent with the Antigravity redirect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:32:51 -04:00

1113 lines
45 KiB
JavaScript

'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('../gsd-core/bin/lib/surface.cjs');
const { loadSkillsManifest, writeActiveProfile, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { CLUSTERS, allClusteredSkills } = require('../gsd-core/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;
// #1367: claude local uses flat commands/ (not commands/gsd/) — commandsDir is commands/.
const commandsDir = path.join(runtimeConfigDir, 'commands');
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) => {
// #1367: claude local uses flat gsd-<stem>.md files at commands/ (not commands/gsd/<stem>.md).
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);
// After #1367: files are gsd-<stem>.md (not bare stem.md). Strip the gsd- prefix
// to check against the REAL_COMMANDS_DIR (which still uses bare names).
const files = fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
for (const file of files) {
const bareName = file.slice('gsd-'.length); // gsd-help.md → help.md
assert.ok(fs.existsSync(path.join(REAL_COMMANDS_DIR, bareName)), `unexpected file: ${file} (no source: ${bareName})`);
}
const expectedCore = [...resolved.skills].map(stem => `gsd-${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);
// #1367: files are gsd-<stem>.md in flat commands/
const afterStandard = new Set(fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && 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.startsWith('gsd-') && f.endsWith('.md')));
assert.ok(afterCore.size <= afterStandard.size, 'core should have fewer or equal files than standard');
const expectedCore = [...resolvedCore.skills].map(stem => `gsd-${stem}.md`).sort();
assert.deepStrictEqual(
[...afterCore].sort(),
expectedCore,
'afterCore should contain exactly core commands'
);
for (const file of afterCore) {
const bareName = file.slice('gsd-'.length);
assert.ok(
fs.existsSync(path.join(REAL_COMMANDS_DIR, bareName)),
`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);
// #1367: flat gsd-<stem>.md files at commands/ (not commands/gsd/<stem>.md)
assert.ok(
fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
'gsd-help.md should be copied from install source (#1367: flat hyphen layout)'
);
assert.ok(
fs.existsSync(path.join(commandsDir, 'gsd-new-project.md')),
'gsd-new-project.md should be copied from install source (#1367: flat hyphen layout)'
);
});
test('_syncGsdDir skills kind: adds missing skill dirs, removes stale prefix-matched dirs, preserves foreign dirs', (t) => {
const { _syncGsdDir } = require('../gsd-core/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 };
// Build a minimal manifest that includes the GSD-owned stems so that the
// manifest-membership gate (Finding 1 fix) correctly identifies gsd-old-skill
// as GSD-owned and prunes it. Without a manifest the new code conservatively
// preserves all gsd-* dirs it cannot confirm are GSD-owned.
const manifest = new Map([
['help', []],
['update', []],
['old-skill', []], // GSD-owned stale stem — must be pruned when not in staged set
]);
_syncGsdDir(stagedDir, destDir, skillsKind, manifest);
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 (it's in the manifest so it is GSD-owned, but not in staged set)
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);
// #1367: claude local uses flat commands/ (not commands/gsd/)
const commandsDir = path.join(runtimeConfigDir, 'commands');
assert.ok(fs.existsSync(commandsDir), 'commands/ dir should be created even if initially absent');
const files = fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
assert.ok(files.length > 0, 'commands/ should contain staged skill files (gsd-*.md)');
assert.ok(files.includes('gsd-help.md'), 'gsd-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('../gsd-core/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('../gsd-core/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');
});
// Regression guard for #816: applySurface must write gsd-prefixed command files
// (matching installRuntimeArtifacts/_copyStaged behaviour) and must NOT prune
// user-created command files that install would preserve.
//
// Affected runtimes have a FLAT command dir (opencode `command/`, cursor
// `commands/`, augment `commands/`, kilo `command/`) with kind.prefix='gsd-'.
// _copyStaged names files `gsd-<stem>.md` but the buggy _syncGsdDir copies
// them as `<stem>.md` (unprefixed) and also deletes ALL .md files not in the
// staged set, including user files.
test('applySurface writes gsd-prefixed command files matching install and preserves user commands (#816)', (t) => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-surface-816-'));
t.after(() => cleanup(configDir));
writeActiveProfile(configDir, 'standard');
writeSurface(configDir, {
baseProfile: 'standard',
disabledClusters: [],
explicitAdds: [],
explicitRemoves: [],
});
// Determine the command dest dir for opencode: commandsKind destSubpath='command'
const layout = resolveRuntimeArtifactLayout('opencode', configDir, 'global');
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'opencode layout must have a commands kind');
const commandDir = path.join(configDir, commandsKind.destSubpath);
// Pre-seed a user command file BEFORE applySurface — install would preserve it
fs.mkdirSync(commandDir, { recursive: true });
fs.writeFileSync(path.join(commandDir, 'my-user-cmd.md'), '# user custom command\n', 'utf8');
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
applySurface(configDir, layout, manifest, CLUSTERS);
const files = fs.readdirSync(commandDir).filter(f => f.endsWith('.md'));
// (a) At least one gsd-prefixed command file must exist — on buggy code only
// unprefixed files like 'help.md' are written, so this assertion fails.
assert.ok(
files.some(f => f.startsWith('gsd-') && f.endsWith('.md')),
'#816: applySurface must write at least one gsd-prefixed command file ' +
'(e.g. gsd-help.md) to match installRuntimeArtifacts/_copyStaged behaviour. ' +
`Actual files: [${files.join(', ')}]`
);
// (b) Every GSD-owned command file must be prefixed — no bare <stem>.md files
// allowed among GSD-owned output (excluding the user file).
const gsdFiles = files.filter(f => f !== 'my-user-cmd.md');
const unprefixed = gsdFiles.filter(f => !f.startsWith('gsd-'));
assert.deepStrictEqual(
unprefixed,
[],
'#816: all GSD-owned command files must start with gsd- to match install. ' +
`Found unprefixed: [${unprefixed.join(', ')}]`
);
// (c) The pre-seeded user file must survive applySurface — on buggy code the
// commands-kind pruning loop deletes ALL .md files not in the staged set,
// which wipes user files that installRuntimeArtifacts would never touch.
assert.ok(
files.includes('my-user-cmd.md'),
'#816: applySurface must preserve user command file my-user-cmd.md that was ' +
'present before sync — _syncGsdDir must not prune files not owned by GSD. ' +
`Actual files: [${files.join(', ')}]`
);
});
// Parity regression guard for #816: applySurface command-dir filenames must
// match a fresh installRuntimeArtifacts for every command runtime. Guards
// against future drift between _syncGsdDir (surface) and _copyStaged (install)
// command-naming logic.
//
// Matrix: opencode/kilo = flat command/ + prefix gsd-;
// cursor/augment = flat commands/ + prefix gsd-.
// For each runtime we: run install into installDir, run applySurface into
// surfaceDir (same 'standard' profile both sides), then compare sorted .md
// filename sets in the commands dest dir. On a fresh dir (no superseded files)
// both paths must produce identical sets.
test('applySurface command-dir filenames match a fresh install for every command runtime (#816 parity)', async (t) => {
process.env.GSD_TEST_MODE = '1';
const { installRuntimeArtifacts } = require('../bin/install.js');
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
// Build the resolved profile once. Both install and surface sides must use
// the same skill set so any filename difference is purely a naming bug.
const resolvedProfile = resolveProfile({ modes: ['standard'], manifest });
const PARITY_RUNTIMES = ['opencode', 'cursor', 'augment', 'kilo'];
for (const runtime of PARITY_RUNTIMES) {
// Create two independent temp dirs — one for install, one for surface.
const installDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-816-install-${runtime}-`));
const surfaceDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-816-surface-${runtime}-`));
t.after(() => { cleanup(installDir); cleanup(surfaceDir); });
// --- Install path ---
installRuntimeArtifacts(runtime, installDir, 'global', resolvedProfile);
// --- Surface path ---
writeActiveProfile(surfaceDir, 'standard');
writeSurface(surfaceDir, {
baseProfile: 'standard',
disabledClusters: [],
explicitAdds: [],
explicitRemoves: [],
});
const layout = resolveRuntimeArtifactLayout(runtime, surfaceDir, 'global');
applySurface(surfaceDir, layout, manifest, CLUSTERS);
// --- Find commands kind ---
const cmdKind = layout.kinds.find(k => k.kind === 'commands');
if (!cmdKind) {
// Runtime has no commands kind at global scope — skip gracefully.
continue;
}
// --- Compare sorted .md filename sets ---
const installCmdDir = path.join(installDir, cmdKind.destSubpath);
const surfaceCmdDir = path.join(surfaceDir, cmdKind.destSubpath);
const installFiles = fs.existsSync(installCmdDir)
? fs.readdirSync(installCmdDir).filter(f => f.endsWith('.md')).sort()
: [];
const surfaceFiles = fs.existsSync(surfaceCmdDir)
? fs.readdirSync(surfaceCmdDir).filter(f => f.endsWith('.md')).sort()
: [];
assert.deepStrictEqual(
surfaceFiles,
installFiles,
`#816 parity: command filenames for ${runtime} (${cmdKind.destSubpath}) must match a fresh install.\n` +
` install: [${installFiles.slice(0, 5).join(', ')}${installFiles.length > 5 ? '...' : ''}]\n` +
` surface: [${surfaceFiles.slice(0, 5).join(', ')}${surfaceFiles.length > 5 ? '...' : ''}]`
);
}
});
// Regression test for #813: applySurface must apply per-runtime path rewrites
// (applyRuntimeContentRewritesInPlace) just as installRuntimeArtifacts does.
// Without the fix, skill bodies retain the converter's default ~/.claude/ paths
// instead of being rewritten to the install target (pathPrefix).
//
// Both 'cursor' and 'codex' use skillsKind AND have a path-rewrite case in
// _applyRuntimeRewrites — so the regression guard covers both.
for (const runtime of ['cursor', 'codex']) {
test(`applySurface rewrites ${runtime} skill bodies to the install pathPrefix, not the converter default ~/.claude path (#813)`, (t) => {
// Use mkdtempSync under os.tmpdir() — NOT under the user's home dir — so that
// computePathPrefix returns an ABSOLUTE prefix `${configDir}/` for local installs,
// clearly distinguishable from the `~/.claude/` converter default.
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-surface-813-${runtime}-`));
t.after(() => cleanup(configDir));
writeActiveProfile(configDir, 'standard');
writeSurface(configDir, {
baseProfile: 'standard',
disabledClusters: [],
explicitAdds: [],
explicitRemoves: [],
});
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
// skills/gsd-<name>/SKILL.md (destSubpath 'skills', prefix 'gsd-')
// scope='local' ensures computePathPrefix returns an absolute configDir prefix
// rather than the home-relative default (e.g. ~/.cursor/ or ~/.codex/).
const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'local');
applySurface(configDir, layout, manifest, CLUSTERS);
// Collect every SKILL.md body under ${configDir}/skills/
const skillsRoot = path.join(configDir, 'skills');
const skillBodies = [];
if (fs.existsSync(skillsRoot)) {
for (const dirEntry of fs.readdirSync(skillsRoot)) {
const skillMd = path.join(skillsRoot, dirEntry, 'SKILL.md');
if (fs.existsSync(skillMd)) {
skillBodies.push(fs.readFileSync(skillMd, 'utf8'));
}
}
}
// (a) Sanity: at least one SKILL.md must have been staged
assert.ok(
skillBodies.length > 0,
`applySurface must stage at least one ${runtime} SKILL.md under ${skillsRoot}/ but found none`
);
// (b) BUG SYMPTOM (#813): after the rewrite, no body should contain '~/.claude/'
// or '$HOME/.claude/' — both are converter-default forms that must be eliminated.
// This assertion FAILS on unpatched code — applySurface does not call
// applyRuntimeContentRewritesInPlace, so the converter's default ~/.claude/
// paths are left verbatim in the staged files.
const bodiesWithTildeClaude = skillBodies.filter(b => b.includes('~/.claude/') || b.includes('$HOME/.claude/'));
assert.strictEqual(
bodiesWithTildeClaude.length,
0,
`#813 regression: ${bodiesWithTildeClaude.length} ${runtime} SKILL.md(s) still contain '~/.claude/' or '$HOME/.claude/' after applySurface — ` +
`applyRuntimeContentRewritesInPlace was not applied (mirrors installRuntimeArtifacts' rewrite step)`
);
// (c) The rewrite must inject the real install target path, not just remove the tilde.
// This also fails on unpatched code for the same reason.
// NOTE: this assertion depends on the command corpus emitting rewritable
// '~/.claude/'-style paths in at least one skill body. If a future corpus
// change removes all such paths, this assertion will become vacuously true
// (no body will contain configDirPrefix either) — update the test rather than
// treating a silent zero-match as a pass.
// Production derives pathPrefix as `path.resolve(configDir).replace(/\\/g, '/')`
// (mirrors installRuntimeArtifacts), so on Windows the rewritten body uses
// forward slashes. Normalize the expected prefix the same way so this assertion
// is cross-platform (Windows CI leg is not covered by local gsd-test) (#813).
const configDirPrefix = `${path.resolve(configDir).replace(/\\/g, '/')}/`;
const bodiesWithAbsolutePrefix = skillBodies.filter(b => b.includes(configDirPrefix));
assert.ok(
bodiesWithAbsolutePrefix.length > 0,
`#813 regression: no ${runtime} SKILL.md contains the absolute configDir prefix '${configDirPrefix}' — ` +
`the path rewrite was not applied by applySurface. ` +
`(If the command corpus no longer emits any '~/.claude/'-style paths, update this test.)`
);
});
}
});
// ─── 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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
});
// ─── 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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
test('missing file returns null', () => {
const dir = tmpDir('gsd-surface-state-');
try {
const result = readSurface(dir);
assert.strictEqual(result, null);
} finally {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(base);
}
});
});
// ─── 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('accepts parsed gsd-file-manifest JSON objects without crashing (#322)', () => {
const dir = tmpDir('gsd-surface-list-');
try {
fs.writeFileSync(path.join(dir, '.gsd-source'), REAL_COMMANDS_DIR, 'utf8');
writeActiveProfile(dir, 'core');
const diskManifestShape = {
version: '1.0.0',
timestamp: new Date().toISOString(),
mode: 'core',
files: {},
};
const result = listSurface(dir, diskManifestShape, 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');
} finally {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
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 {
cleanup(dir);
}
});
});
// ─── #1615: applySurface must rewrite commands kind (Windsurf workflows) ─────
// Adversarial review of PR #1622 found that applySurface only rewrites 'skills'
// kinds, skipping 'commands'. Windsurf's capability now stages workflow files
// as kind='commands'; without the rewrite, /gsd-surface would write workflow
// bodies containing raw @~/.claude/... references that don't exist on a
// Windsurf install. The same gap affected any runtime with commands kinds.
describe('applySurface — commands kind path rewrite (#1615 adversarial review)', () => {
test('windsurf workflow bodies are rewritten to install target (no raw ~/.claude/)', (t) => {
const base = createTempDir('gsd-surface-cmds-windsurf-');
t.after(() => cleanup(base));
const runtimeConfigDir = base;
// Stage the canonical command body the workflow delegates to.
const canonicalDir = path.join(runtimeConfigDir, 'gsd-core', 'commands', 'gsd');
fs.mkdirSync(canonicalDir, { recursive: true });
fs.writeFileSync(path.join(canonicalDir, 'help.md'),
'---\nname: help\ndescription: Show help\n---\n\nHelp body\n');
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
const layout = resolveRuntimeArtifactLayout('windsurf', runtimeConfigDir, 'local');
// Sanity: layout must have a commands kind (workflows) — pre-condition
// introduced by PR #1622; if a future refactor removes it, this test
// would silently pass without exercising the rewrite path.
const commandsKind = layout.kinds.find((k) => k.kind === 'commands');
assert.ok(commandsKind, 'pre-condition: windsurf layout has a commands kind');
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
// Workflow files should be written to <configDir>/workflows/gsd-*.md
const workflowsDir = path.join(runtimeConfigDir, 'workflows');
const workflowFiles = fs.existsSync(workflowsDir)
? fs.readdirSync(workflowsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'))
: [];
assert.ok(workflowFiles.length > 0,
`expected at least one gsd-*.md workflow under ${workflowsDir}; got [${workflowFiles.join(', ')}]`);
// Every workflow body must reference the install target, NOT the raw
// ~/.claude/ path. This is the regression: pre-fix, the commands kind
// was skipped and raw @~/.claude/... survived into the synced file.
for (const fileName of workflowFiles) {
const workflowPath = path.join(workflowsDir, fileName);
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(
!content.includes('~/.claude/'),
`${fileName} must not contain raw ~/.claude/ after applySurface rewrite (got: ${content.slice(0, 200)})`,
);
assert.ok(
!content.includes('$HOME/.claude/'),
`${fileName} must not contain raw $HOME/.claude/ after applySurface rewrite`,
);
}
});
});