Extends `dispatchKindEntry` in `runtime-artifact-layout.cts` to route agents-kind entries through a converter when the descriptor carries a non-null `converter` field. Adds `stageAgentsForRuntimeWithConverter` to `install-profiles.cts`, expands `VALID_CONVERTER_NAMES` with the 9 agent converter names, and adds a fail-first behavioral test suite (9 tests) proving the new wiring end-to-end. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -546,8 +546,10 @@ function validateFeatureBody(cap) {
|
||||
}
|
||||
|
||||
// ADR-857 phase 5e: Closed ConverterName enum — complete set used across 16 runtime descriptors,
|
||||
// all exported by bin/install.js. Any ArtifactKind with a non-null converter must use one of these.
|
||||
// all exported by bin/install.js (commands/skills) and src/runtime-artifact-conversion.cts (agents).
|
||||
// Any ArtifactKind with a non-null converter must use one of these.
|
||||
const VALID_CONVERTER_NAMES = new Set([
|
||||
// commands / skills converters (pre-existing)
|
||||
'convertClaudeCommandToAntigravitySkill',
|
||||
'convertClaudeCommandToAugmentSkill',
|
||||
'convertClaudeCommandToClineSkill',
|
||||
@@ -563,6 +565,16 @@ const VALID_CONVERTER_NAMES = new Set([
|
||||
'convertClaudeCommandToOpencodeSkill',
|
||||
'convertClaudeCommandToTraeSkill',
|
||||
'convertClaudeCommandToWindsurfSkill',
|
||||
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
||||
'convertClaudeAgentToCopilotAgent',
|
||||
'convertClaudeAgentToAntigravityAgent',
|
||||
'convertClaudeAgentToCursorAgent',
|
||||
'convertClaudeAgentToWindsurfAgent',
|
||||
'convertClaudeAgentToAugmentAgent',
|
||||
'convertClaudeAgentToTraeAgent',
|
||||
'convertClaudeAgentToCodebuddyAgent',
|
||||
'convertClaudeAgentToClineAgent',
|
||||
'convertClaudeAgentToCodexAgent',
|
||||
]);
|
||||
|
||||
// C3: Validate role:runtime body
|
||||
|
||||
@@ -530,6 +530,59 @@ function stageSkillsForRuntimeAsSkills(
|
||||
return stageDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage a converted copy of the agents directory for a given runtime.
|
||||
*
|
||||
* Analogous to `stageCommandsForRuntimeFlat` but for agent `.md` files. Each
|
||||
* source `.md` is passed through `converter` and written as a flat `${name}.md`
|
||||
* file in the staging directory. Agent filenames are kept verbatim (no prefix
|
||||
* added here — the prefix is already embedded in agent stems, e.g. `gsd-planner.md`).
|
||||
*
|
||||
* This is used by the descriptor-driven `dispatchKindEntry` when an `agents` kind
|
||||
* entry carries a non-null converter (ADR-457 / #1173). When `converter` is null,
|
||||
* `agentsKind` falls back to the existing raw-copy path (`stageAgentsForProfile`).
|
||||
*
|
||||
* For the `full` profile (`skills === '*'`), all `.md` files are staged.
|
||||
* For tiered profiles, only agents whose full stem is in `resolvedProfile.agents`
|
||||
* are staged (mirrors `stageAgentsForProfile` behaviour).
|
||||
*
|
||||
* @param srcAgentsDir source agents directory (e.g. agents/)
|
||||
* @param resolvedProfile profile filter from resolveProfile()
|
||||
* @param converter (content: string) → string pure per-file converter
|
||||
*/
|
||||
function stageAgentsForRuntimeWithConverter(
|
||||
srcAgentsDir: string,
|
||||
resolvedProfile: ResolvedProfile,
|
||||
converter: (content: string) => string,
|
||||
): string {
|
||||
if (!fs.existsSync(srcAgentsDir)) return srcAgentsDir;
|
||||
|
||||
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-runtime-agents-'));
|
||||
try {
|
||||
const entries = fs.readdirSync(srcAgentsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.endsWith('.md')) continue;
|
||||
// For tiered profiles, gate by agent stem (full filename without extension).
|
||||
if (resolvedProfile.skills !== '*') {
|
||||
const stem = entry.name.slice(0, -3);
|
||||
if (!(resolvedProfile.agents instanceof Set && resolvedProfile.agents.has(stem))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const content = fs.readFileSync(path.join(srcAgentsDir, entry.name), 'utf8');
|
||||
const converted = converter(content);
|
||||
fs.writeFileSync(path.join(stageDir, entry.name), converted, 'utf8');
|
||||
}
|
||||
} catch (err) {
|
||||
try { fs.rmSync(stageDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
throw err;
|
||||
}
|
||||
STAGED_DIRS.add(stageDir);
|
||||
ensureExitCleanup();
|
||||
return stageDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage converted command files as flat `.md` files.
|
||||
*
|
||||
@@ -757,6 +810,7 @@ export = {
|
||||
mostRestrictiveProfile,
|
||||
stageSkillsForProfile,
|
||||
stageAgentsForProfile,
|
||||
stageAgentsForRuntimeWithConverter,
|
||||
stageSkillsForRuntimeAsSkills,
|
||||
stageCommandsForRuntimeFlat,
|
||||
STAGED_DIRS,
|
||||
|
||||
@@ -21,6 +21,7 @@ import installProfiles = require('./install-profiles.cjs');
|
||||
const {
|
||||
stageSkillsForProfile,
|
||||
stageAgentsForProfile,
|
||||
stageAgentsForRuntimeWithConverter,
|
||||
stageSkillsForRuntimeAsSkills,
|
||||
stageCommandsForRuntimeFlat,
|
||||
} = installProfiles;
|
||||
@@ -192,6 +193,41 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a converted-agents kind descriptor for runtimes whose agent `.md` files
|
||||
* need runtime-specific frontmatter/body conversion (e.g. Copilot, Cursor, Codex).
|
||||
*
|
||||
* Unlike `agentsKind` (which raw-copies source files), this kind applies
|
||||
* `converterName` from Runtime Artifact Conversion exports to each agent file
|
||||
* during staging, writing flat `${name}.md` files to the staged directory.
|
||||
*
|
||||
* Agent filenames are preserved verbatim (the prefix is already embedded in the
|
||||
* agent stem — e.g. `gsd-planner.md`).
|
||||
*
|
||||
* Mirrors the `convertedCommandsKind` pattern (#785).
|
||||
*
|
||||
* @param destSubpath destination subpath within configDir (e.g. 'agents')
|
||||
* @param prefix filename prefix (informational; not applied here)
|
||||
* @param converterName name of converter function in Runtime Artifact Conversion exports
|
||||
* @param configDir runtime config dir (for .gsd-source marker resolution)
|
||||
*/
|
||||
function convertedAgentsKind(
|
||||
destSubpath: string,
|
||||
prefix: string,
|
||||
converterName: string,
|
||||
configDir: string,
|
||||
): ArtifactKind {
|
||||
return {
|
||||
kind: 'agents',
|
||||
destSubpath,
|
||||
prefix,
|
||||
stage: (resolved) => {
|
||||
const converter = conversionExports[converterName] as (content: string) => string;
|
||||
return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
|
||||
return {
|
||||
kind: 'kimi-agents',
|
||||
@@ -401,7 +437,10 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
|
||||
return convertedCommandsKind(destSubpath, prefix, converter, configDir);
|
||||
|
||||
case 'agents':
|
||||
return agentsKind(destSubpath, prefix, configDir);
|
||||
if (converter == null) {
|
||||
return agentsKind(destSubpath, prefix, configDir);
|
||||
}
|
||||
return convertedAgentsKind(destSubpath, prefix, converter, configDir);
|
||||
|
||||
case 'skills':
|
||||
if (converter == null) {
|
||||
|
||||
@@ -3817,13 +3817,14 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
// ─── 25. ADR-857 phase 5e: closed ConverterName enum (Part B) ─────────────────
|
||||
|
||||
describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
|
||||
test('VALID_CONVERTER_NAMES has exactly 15 entries', () => {
|
||||
test('VALID_CONVERTER_NAMES has exactly 24 entries (15 command/skill + 9 agent converters added in #1173)', () => {
|
||||
assert.ok(VALID_CONVERTER_NAMES instanceof Set, 'VALID_CONVERTER_NAMES must be a Set');
|
||||
assert.strictEqual(VALID_CONVERTER_NAMES.size, 15, 'VALID_CONVERTER_NAMES must have exactly 15 entries, got: ' + VALID_CONVERTER_NAMES.size);
|
||||
assert.strictEqual(VALID_CONVERTER_NAMES.size, 24, 'VALID_CONVERTER_NAMES must have exactly 24 entries, got: ' + VALID_CONVERTER_NAMES.size);
|
||||
});
|
||||
|
||||
test('VALID_CONVERTER_NAMES contains all expected converter names', () => {
|
||||
const expected = [
|
||||
// command/skill converters (pre-existing)
|
||||
'convertClaudeCommandToAntigravitySkill',
|
||||
'convertClaudeCommandToAugmentSkill',
|
||||
'convertClaudeCommandToClineSkill',
|
||||
@@ -3839,6 +3840,16 @@ describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
|
||||
'convertClaudeCommandToOpencodeSkill',
|
||||
'convertClaudeCommandToTraeSkill',
|
||||
'convertClaudeCommandToWindsurfSkill',
|
||||
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
||||
'convertClaudeAgentToCopilotAgent',
|
||||
'convertClaudeAgentToAntigravityAgent',
|
||||
'convertClaudeAgentToCursorAgent',
|
||||
'convertClaudeAgentToWindsurfAgent',
|
||||
'convertClaudeAgentToAugmentAgent',
|
||||
'convertClaudeAgentToTraeAgent',
|
||||
'convertClaudeAgentToCodebuddyAgent',
|
||||
'convertClaudeAgentToClineAgent',
|
||||
'convertClaudeAgentToCodexAgent',
|
||||
];
|
||||
for (const name of expected) {
|
||||
assert.ok(VALID_CONVERTER_NAMES.has(name), 'VALID_CONVERTER_NAMES must contain "' + name + '"');
|
||||
|
||||
300
tests/feat-1173-agent-converters-descriptor.test.cjs
Normal file
300
tests/feat-1173-agent-converters-descriptor.test.cjs
Normal file
@@ -0,0 +1,300 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* feat-1173: Descriptor-driven agent converter wiring.
|
||||
*
|
||||
* Verifies that the descriptor-driven install path (dispatchKindEntry) applies
|
||||
* per-runtime agent conversion when the 'agents' kind entry has a non-null
|
||||
* converter — instead of silently raw-copying.
|
||||
*
|
||||
* Behavioral assertions: invoke the staging/dispatch seam, inspect the staged
|
||||
* output files. NOT source-grep.
|
||||
*
|
||||
* TDD flow (REGRESSION-MUST-FAIL-FIRST rule):
|
||||
* Before the fix, dispatchKindEntry ignores the converter for agents kind and
|
||||
* raw-copies. The tests below prove conversion is applied by asserting that
|
||||
* the staged .md contains runtime-specific frontmatter transformations absent
|
||||
* in the raw source.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const {
|
||||
resolveRuntimeArtifactLayoutFromRegistry,
|
||||
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'));
|
||||
|
||||
const {
|
||||
cleanupStagedSkills,
|
||||
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'install-profiles.cjs'));
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ─── fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Minimal Claude agent source with comma-separated tools (Claude format).
|
||||
* Copilot conversion turns tools into a JSON array (CONV-04/05).
|
||||
* Codex conversion adds <codex_agent_role> block.
|
||||
* Cursor/Windsurf/Augment/Trae/Codebuddy/Cline conversion strips color field.
|
||||
*/
|
||||
const CLAUDE_AGENT_SOURCE = `---
|
||||
name: gsd-planner
|
||||
description: A GSD planning agent.
|
||||
tools: Bash, Read, Write
|
||||
color: blue
|
||||
---
|
||||
|
||||
# GSD Planner
|
||||
|
||||
This agent plans GSD phases using ~/.claude/skills.
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create a temp agents directory with a .gsd-source marker pointing back to it
|
||||
* (so findAgentsSourceRoot finds the fixture dir, not the real agents/ dir).
|
||||
* The .gsd-source convention expects the marker to point at a commands/gsd dir;
|
||||
* agents/ is resolved as a sibling of commands/. So we set up:
|
||||
* <tmproot>/
|
||||
* commands/gsd/ (empty, satisfies the sibling check)
|
||||
* agents/
|
||||
* gsd-planner.md
|
||||
* .gsd-source <- points to <tmproot>/commands/gsd
|
||||
*/
|
||||
function makeFixtureRoot(agentFiles) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1173-root-'));
|
||||
const commandsDir = path.join(root, 'commands', 'gsd');
|
||||
const agentsDir = path.join(root, 'agents');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
// .gsd-source marker must point to commands/gsd so that agentsSourceRoot resolves to agents/
|
||||
fs.writeFileSync(path.join(root, '.gsd-source'), commandsDir + '\n', 'utf8');
|
||||
for (const { name, content } of agentFiles) {
|
||||
fs.writeFileSync(path.join(agentsDir, name), content, 'utf8');
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function makeSyntheticRegistry(converterName) {
|
||||
return {
|
||||
runtimes: {
|
||||
testruntime: {
|
||||
runtime: {
|
||||
artifactLayout: {
|
||||
global: [
|
||||
{
|
||||
kind: 'agents',
|
||||
destSubpath: 'agents',
|
||||
prefix: 'gsd-',
|
||||
nesting: 'flat',
|
||||
recursive: false,
|
||||
converter: converterName,
|
||||
},
|
||||
],
|
||||
local: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── stageAgentsForRuntimeWithConverter unit tests ────────────────────────────
|
||||
|
||||
describe('feat-1173: stageAgentsForRuntimeWithConverter', () => {
|
||||
test('is exported from install-profiles', () => {
|
||||
const installProfiles = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'install-profiles.cjs'));
|
||||
assert.strictEqual(
|
||||
typeof installProfiles.stageAgentsForRuntimeWithConverter,
|
||||
'function',
|
||||
'stageAgentsForRuntimeWithConverter must be exported',
|
||||
);
|
||||
});
|
||||
|
||||
test('applies converter to each staged agent file', (t) => {
|
||||
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'install-profiles.cjs'));
|
||||
|
||||
const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1173-agents-'));
|
||||
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), CLAUDE_AGENT_SOURCE, 'utf8');
|
||||
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), CLAUDE_AGENT_SOURCE.replace('gsd-planner', 'gsd-executor'), 'utf8');
|
||||
|
||||
t.after(() => {
|
||||
cleanup(agentsDir);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const calls = [];
|
||||
const converter = (content) => {
|
||||
calls.push(content);
|
||||
return content.replace('~/.claude/', '~/.copilot/');
|
||||
};
|
||||
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = stageAgentsForRuntimeWithConverter(agentsDir, resolvedProfile, converter);
|
||||
|
||||
assert.strictEqual(calls.length, 2, 'converter called for each agent file');
|
||||
const stagedFiles = fs.readdirSync(stagedDir).sort();
|
||||
assert.deepStrictEqual(stagedFiles, ['gsd-executor.md', 'gsd-planner.md']);
|
||||
|
||||
// Converter replaced ~/.claude/ with ~/.copilot/ in all staged files
|
||||
for (const file of stagedFiles) {
|
||||
const content = fs.readFileSync(path.join(stagedDir, file), 'utf8');
|
||||
assert.ok(!content.includes('~/.claude/'), `${file}: converter must have replaced ~/.claude/`);
|
||||
assert.ok(content.includes('~/.copilot/'), `${file}: converter must have injected ~/.copilot/`);
|
||||
}
|
||||
});
|
||||
|
||||
test('non-existent srcAgentsDir returns srcAgentsDir unchanged', () => {
|
||||
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'install-profiles.cjs'));
|
||||
const ghost = path.join(os.tmpdir(), 'gsd-1173-no-exist-' + Date.now());
|
||||
const converter = (c) => c;
|
||||
const result = stageAgentsForRuntimeWithConverter(ghost, { name: 'full', skills: '*', agents: new Set() }, converter);
|
||||
assert.strictEqual(result, ghost, 'must return srcAgentsDir unchanged for non-existent dir');
|
||||
});
|
||||
|
||||
test('only copies .md files (ignores non-.md)', (t) => {
|
||||
const { stageAgentsForRuntimeWithConverter } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'install-profiles.cjs'));
|
||||
const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1173-agents-'));
|
||||
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), CLAUDE_AGENT_SOURCE, 'utf8');
|
||||
fs.writeFileSync(path.join(agentsDir, 'README.txt'), 'not an agent', 'utf8');
|
||||
|
||||
t.after(() => {
|
||||
cleanup(agentsDir);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const converter = (c) => c;
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = stageAgentsForRuntimeWithConverter(agentsDir, resolvedProfile, converter);
|
||||
const stagedFiles = fs.readdirSync(stagedDir);
|
||||
assert.deepStrictEqual(stagedFiles, ['gsd-planner.md'], 'only .md files should be staged');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── dispatchKindEntry wiring tests ──────────────────────────────────────────
|
||||
|
||||
describe('feat-1173: dispatchKindEntry agents converter wiring', () => {
|
||||
test('agents kind with convertClaudeAgentToCopilotAgent converter applies copilot conversion', (t) => {
|
||||
const fixtureRoot = makeFixtureRoot([{ name: 'gsd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
|
||||
t.after(() => {
|
||||
cleanup(fixtureRoot);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const registry = makeSyntheticRegistry('convertClaudeAgentToCopilotAgent');
|
||||
const layout = resolveRuntimeArtifactLayoutFromRegistry(
|
||||
registry, 'testruntime', fixtureRoot, 'global',
|
||||
);
|
||||
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
const agentKind = layout.kinds[0];
|
||||
assert.strictEqual(agentKind.kind, 'agents');
|
||||
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = agentKind.stage(resolvedProfile);
|
||||
|
||||
const stagedFile = path.join(stagedDir, 'gsd-planner.md');
|
||||
assert.ok(fs.existsSync(stagedFile), `staged file must exist: ${stagedFile}`);
|
||||
|
||||
const stagedContent = fs.readFileSync(stagedFile, 'utf8');
|
||||
|
||||
// Copilot CONV-04/05: tools converted from "Bash, Read, Write" to JSON array "['bash', 'read', 'write']"
|
||||
// Raw copy would keep the original comma-separated "tools: Bash, Read, Write" line.
|
||||
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
|
||||
assert.ok(
|
||||
stagedContent.includes("tools: ['") || stagedContent.includes('tools: ['),
|
||||
`Copilot conversion must produce JSON array tools. Got:\n${stagedContent.slice(0, 300)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('agents kind with convertClaudeAgentToCodexAgent converter applies codex conversion', (t) => {
|
||||
const fixtureRoot = makeFixtureRoot([{ name: 'gsd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
|
||||
t.after(() => {
|
||||
cleanup(fixtureRoot);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const registry = makeSyntheticRegistry('convertClaudeAgentToCodexAgent');
|
||||
const layout = resolveRuntimeArtifactLayoutFromRegistry(
|
||||
registry, 'testruntime', fixtureRoot, 'global',
|
||||
);
|
||||
|
||||
const agentKind = layout.kinds[0];
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = agentKind.stage(resolvedProfile);
|
||||
|
||||
const stagedContent = fs.readFileSync(path.join(stagedDir, 'gsd-planner.md'), 'utf8');
|
||||
|
||||
// Codex conversion adds <codex_agent_role> block
|
||||
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
|
||||
assert.ok(
|
||||
stagedContent.includes('<codex_agent_role>'),
|
||||
`Codex conversion must add <codex_agent_role>. Got:\n${stagedContent.slice(0, 300)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('agents kind with convertClaudeAgentToCursorAgent converter applies cursor conversion', (t) => {
|
||||
const fixtureRoot = makeFixtureRoot([{ name: 'gsd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
|
||||
t.after(() => {
|
||||
cleanup(fixtureRoot);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const registry = makeSyntheticRegistry('convertClaudeAgentToCursorAgent');
|
||||
const layout = resolveRuntimeArtifactLayoutFromRegistry(
|
||||
registry, 'testruntime', fixtureRoot, 'global',
|
||||
);
|
||||
|
||||
const agentKind = layout.kinds[0];
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = agentKind.stage(resolvedProfile);
|
||||
|
||||
const stagedContent = fs.readFileSync(path.join(stagedDir, 'gsd-planner.md'), 'utf8');
|
||||
|
||||
// Cursor conversion strips color field and rewrites ~/.claude/ paths
|
||||
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
|
||||
assert.ok(
|
||||
!stagedContent.includes('color:'),
|
||||
`Cursor agent conversion should strip the color field. Got:\n${stagedContent.slice(0, 300)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('agents kind with converter=null still raw-copies (backward compat for claude)', (t) => {
|
||||
const fixtureRoot = makeFixtureRoot([{ name: 'gsd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
|
||||
t.after(() => {
|
||||
cleanup(fixtureRoot);
|
||||
cleanupStagedSkills();
|
||||
});
|
||||
|
||||
const registry = makeSyntheticRegistry(null);
|
||||
const layout = resolveRuntimeArtifactLayoutFromRegistry(
|
||||
registry, 'testruntime', fixtureRoot, 'global',
|
||||
);
|
||||
|
||||
const agentKind = layout.kinds[0];
|
||||
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
|
||||
const stagedDir = agentKind.stage(resolvedProfile);
|
||||
|
||||
// converter=null: stageAgentsForProfile with skills='*' returns srcAgentsDir unchanged
|
||||
// (no staging dir is created; the source dir IS the staged dir, a passthrough)
|
||||
const stagedContent = fs.readFileSync(path.join(stagedDir, 'gsd-planner.md'), 'utf8');
|
||||
assert.strictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter=null must raw-copy the agent content');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── real registry: claude agents kind has converter=null ────────────────────
|
||||
|
||||
describe('feat-1173: real registry claude agents kind has converter=null (backward compat)', () => {
|
||||
test('claude local artifacts layout has agents entry with converter=null', () => {
|
||||
const registry = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
|
||||
const claudeDesc = registry.runtimes?.claude?.runtime?.artifactLayout?.local ?? [];
|
||||
const agentsEntry = claudeDesc.find((e) => e.kind === 'agents');
|
||||
assert.ok(agentsEntry, 'claude local artifactLayout must have an agents entry');
|
||||
assert.strictEqual(agentsEntry.converter, null, 'claude agents entry must have converter=null');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user