* feat(#784): emit native skills for OpenCode + Kilo runtimes OpenCode and Kilo share a config schema and both discover on-demand skills from skills/<name>/SKILL.md. The installer previously emitted only flat commands (command/) and file-based agents (agents/) for these runtimes. Add a shared OpenCode-family skill writer that stages each GSD command as a spec-compliant SKILL.md (name matching the directory, description 1-1024 chars), wired through the runtime artifact layout so uninstall cleans skills/ automatically. Skills respect the active install profile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#784): correct skill body paths + preserve user dev-preferences Address adversarial-review findings: - Add opencode/kilo cases to _applyRuntimeRewrites so staged SKILL.md bodies are re-pointed from the converter's hardcoded default config dir to the actual install target (fixes --local / --config-dir installs; commands/agents already did this by applying pathPrefix pre-conversion). - Preserve user-owned skills/gsd-dev-preferences across reinstall in installOpencodeFamilySkills (snapshot+restore around the gsd-* prune), matching installRuntimeArtifacts. - Export installOpencodeFamilySkills and add regression tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#784): guarantee command/skill body parity, fix kilo-alt double-rewrite Follow-up adversarial-review found the post-conversion path rewrite could double-rewrite custom Kilo dirs (kilo -> kilo-alt -> kilo-alt-alt) because the kilo pathPrefix is a $HOME (non-absolute) superset of the hardcoded default base. Restructure so OpenCode/Kilo skills mirror copyFlattenedCommands exactly: stage raw commands, apply pathPrefix BEFORE conversion via a new shared applyOpencodeFamilyPathPrefix() helper (now used by both the command and skill writers), then convert. This guarantees byte-for-byte command/ skill body parity for global, --local, and --config-dir installs and removes the prefix-overlap hazard. Drop the fragile _applyRuntimeRewrites opencode/ kilo case. Strengthen the path regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(#784): derive opencode/kilo skills from the same staged command set Pass the installer's _stageSkills() output directly to installOpencodeFamilySkills instead of re-staging via the layout, so the command/ and skills/ surfaces always cover the identical profile-resolved set — including the --minimal/--core-only alias path, which stages differently from a plain --profile=core. Verified: minimal install now emits 8 commands and 8 skills. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#784): set changeset PR number to 810 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#784): fully escape backslashes in test helper (CodeQL js/incomplete-string-escaping) Replace the dot-only escape `replace(/[.]/g, '\\.')` with a complete regex-escape pattern `replace(/[\\.*+?^${}()|[\]]/g, '\\$&')` so all regex metacharacters (including backslash itself) in `defaultBase` are safely escaped before interpolation into `new RegExp(...)`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
416 lines
18 KiB
JavaScript
416 lines
18 KiB
JavaScript
/**
|
|
* Runtime Converter Tests — OpenCode + Kilo + Gemini
|
|
*
|
|
* Tests for small runtime-specific conversion functions from install.js.
|
|
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
|
|
*
|
|
* OpenCode/Kilo: flat-runtime frontmatter converters (agent + command modes)
|
|
* model: inherit is NOT added (runtime uses its configured default model)
|
|
* but mode: subagent IS added (required by both runtimes' agents).
|
|
* Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping)
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
const {
|
|
convertClaudeToOpencodeFrontmatter,
|
|
convertClaudeToKiloFrontmatter,
|
|
convertClaudeToGeminiAgent,
|
|
convertClaudeCommandToOpencodeSkill,
|
|
convertClaudeCommandToKiloSkill,
|
|
neutralizeAgentReferences,
|
|
} = require('../bin/install.js');
|
|
|
|
// Sample Claude agent frontmatter (matches actual GSD agent format)
|
|
const SAMPLE_AGENT = `---
|
|
name: gsd-executor
|
|
description: Executes GSD plans with atomic commits
|
|
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
color: yellow
|
|
skills:
|
|
- gsd-executor-workflow
|
|
# hooks:
|
|
# PostToolUse:
|
|
# - matcher: "Write|Edit"
|
|
# hooks:
|
|
# - type: command
|
|
# command: "npx eslint --fix $FILE 2>/dev/null || true"
|
|
---
|
|
|
|
<role>
|
|
You are a GSD plan executor.
|
|
</role>`;
|
|
|
|
// Sample Claude command frontmatter (for comparison — commands work differently)
|
|
const SAMPLE_COMMAND = `---
|
|
name: gsd-execute-phase
|
|
description: Execute all plans in a phase
|
|
allowed-tools:
|
|
- Read
|
|
- Write
|
|
- Bash
|
|
---
|
|
|
|
Execute the phase plan.`;
|
|
|
|
const flatRuntimeSuites = [
|
|
{
|
|
label: 'OpenCode',
|
|
convert: convertClaudeToOpencodeFrontmatter,
|
|
configDir: '.config/opencode',
|
|
},
|
|
{
|
|
label: 'Kilo',
|
|
convert: convertClaudeToKiloFrontmatter,
|
|
configDir: '.config/kilo',
|
|
},
|
|
];
|
|
|
|
for (const { label, convert, configDir } of flatRuntimeSuites) {
|
|
describe(`${label} agent conversion (isAgent: true)`, () => {
|
|
test('keeps name: field for agents', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
|
});
|
|
|
|
test('does not add model: inherit', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added');
|
|
});
|
|
|
|
test('adds mode: subagent', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
|
|
});
|
|
|
|
test('strips tools: field', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
|
|
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
|
|
|
|
if (label === 'Kilo') {
|
|
assert.ok(frontmatter.includes('permission:'), 'Kilo agents should emit permission block');
|
|
assert.ok(frontmatter.includes('read: allow'), 'Read should map to read: allow');
|
|
assert.ok(frontmatter.includes('edit: allow'), 'Write/Edit should map to edit: allow');
|
|
assert.ok(frontmatter.includes('bash: allow'), 'Bash should map to bash: allow');
|
|
assert.ok(frontmatter.includes('grep: allow'), 'Grep should map to grep: allow');
|
|
assert.ok(frontmatter.includes('glob: allow'), 'Glob should map to glob: allow');
|
|
assert.ok(frontmatter.includes('task: deny'), 'unspecified permissions should be denied');
|
|
} else {
|
|
assert.ok(!frontmatter.includes('permission:'), 'OpenCode agents should not emit permission block');
|
|
}
|
|
});
|
|
|
|
test('strips skills: array', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
|
|
assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped');
|
|
});
|
|
|
|
test('strips color: field', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
|
|
});
|
|
|
|
test('strips commented hooks block', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
|
|
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
|
|
});
|
|
|
|
test('keeps description: field', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept');
|
|
});
|
|
|
|
test('preserves body content', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
assert.ok(result.includes('<role>'), 'body should be preserved');
|
|
assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact');
|
|
});
|
|
|
|
test('applies body text replacements', () => {
|
|
const agentWithClaudePaths = `---
|
|
name: test-agent
|
|
description: Test
|
|
tools: Read
|
|
---
|
|
|
|
Read ~/.claude/agent-memory/ for context.
|
|
Use $HOME/.claude/skills/ for reference.
|
|
Check .claude/skills/ and .claude/agents/ locally.
|
|
Use ./.claude/hooks/gsd-statusline.js during local testing.
|
|
Fallback skills live in .agents/skills/.`;
|
|
|
|
const result = convert(agentWithClaudePaths, { isAgent: true });
|
|
assert.ok(result.includes(`~/${configDir}/agent-memory/`), '~/.claude should be replaced');
|
|
assert.ok(result.includes(`$HOME/${configDir}/skills/`), '$HOME/.claude should be replaced');
|
|
|
|
if (label === 'Kilo') {
|
|
assert.ok(result.includes('.kilo/skills/'), '.claude/skills should be replaced for Kilo');
|
|
assert.ok(result.includes('.kilo/agents/'), '.claude/agents should be replaced for Kilo');
|
|
assert.ok(result.includes('./.kilo/hooks/'), './.claude should be replaced for Kilo');
|
|
assert.ok(result.includes('Fallback skills live in .kilo/skills/.'), '.agents/skills should be rewritten to Kilo skills dir');
|
|
assert.ok(!result.includes('.kilo/skill/'), 'singular Kilo skill dir should not be emitted');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe(`${label} command conversion (isAgent: false, default)`, () => {
|
|
test('strips name: field for commands', () => {
|
|
const result = convert(SAMPLE_COMMAND);
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
|
|
});
|
|
|
|
test('does not add model: or mode: for commands', () => {
|
|
const result = convert(SAMPLE_COMMAND);
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
|
|
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
|
|
});
|
|
|
|
test('keeps description: for commands', () => {
|
|
const result = convert(SAMPLE_COMMAND);
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(frontmatter.includes('description:'), 'description should be kept');
|
|
});
|
|
});
|
|
|
|
// ─── #2256: model_overrides support for OpenCode/Kilo agents ────────────────
|
|
// Only test OpenCode — Kilo uses the same converter but model override injection
|
|
// is wired only for OpenCode at the call site in install().
|
|
if (label === 'OpenCode') {
|
|
describe('OpenCode agent model override (modelOverride option) (#2256)', () => {
|
|
test('adds model: field when modelOverride is provided', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'gpt-5.3-codex' });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(frontmatter.includes('model: gpt-5.3-codex'), 'model: field must be added with override value');
|
|
});
|
|
|
|
test('does not add model: field when modelOverride is null', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when no override');
|
|
});
|
|
|
|
test('does not add model: field when modelOverride is omitted', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('model:'), 'model: field must be absent when option omitted');
|
|
});
|
|
|
|
test('model: field appears after mode: subagent', () => {
|
|
const result = convert(SAMPLE_AGENT, { isAgent: true, modelOverride: 'o4-mini' });
|
|
const frontmatter = result.split('---')[1];
|
|
const modeIdx = frontmatter.indexOf('mode: subagent');
|
|
const modelIdx = frontmatter.indexOf('model: o4-mini');
|
|
assert.ok(modeIdx !== -1, 'mode: subagent must be present');
|
|
assert.ok(modelIdx !== -1, 'model: field must be present');
|
|
assert.ok(modelIdx > modeIdx, 'model: must appear after mode: subagent');
|
|
});
|
|
|
|
test('model override does not affect command conversion', () => {
|
|
// modelOverride has no effect when isAgent is false (commands)
|
|
const result = convert(SAMPLE_COMMAND, { modelOverride: 'gpt-5.4' });
|
|
const frontmatter = result.split('---')[1];
|
|
assert.ok(!frontmatter.includes('model:'), 'model: must not appear in command output');
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Gemini CLI agent conversion (merged from gemini-config.test.cjs)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('convertClaudeToGeminiAgent', () => {
|
|
test('drops unsupported skills frontmatter while keeping converted tools', () => {
|
|
const input = `---
|
|
name: gsd-codebase-mapper
|
|
description: Explores codebase and writes structured analysis documents.
|
|
tools: Read, Bash, Grep, Glob, Write
|
|
color: cyan
|
|
skills:
|
|
- gsd-mapper-workflow
|
|
---
|
|
|
|
<role>
|
|
Use \${PHASE} in shell examples.
|
|
</role>`;
|
|
|
|
const result = convertClaudeToGeminiAgent(input);
|
|
const frontmatter = result.split('---')[1] || '';
|
|
|
|
assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name');
|
|
assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description');
|
|
assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array');
|
|
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
|
|
assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command');
|
|
assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content');
|
|
assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob');
|
|
assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file');
|
|
assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field');
|
|
assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field');
|
|
assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items');
|
|
assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini');
|
|
assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern');
|
|
});
|
|
|
|
test('excludes Claude-only agent interaction tools from Gemini frontmatter', () => {
|
|
const input = `---
|
|
name: gsd-debug-session-manager
|
|
description: Manages debug sessions.
|
|
tools: Read, Task, Agent, AskUserQuestion
|
|
---
|
|
|
|
<role>
|
|
Coordinate debugger agents.
|
|
Offer choices via AskUserQuestion when user input is needed.
|
|
</role>`;
|
|
|
|
const result = convertClaudeToGeminiAgent(input);
|
|
const frontmatter = result.split('---')[1] || '';
|
|
|
|
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
|
|
assert.ok(!frontmatter.includes(' - ask_user'), 'does not emit invalid Gemini ask_user tool');
|
|
assert.ok(!frontmatter.includes(' - task'), 'does not emit invalid Gemini task tool');
|
|
assert.ok(!frontmatter.includes(' - agent'), 'does not emit invalid Gemini agent tool');
|
|
assert.ok(!frontmatter.includes('Task'), 'does not preserve Claude-only Task tool');
|
|
assert.ok(!frontmatter.includes('Agent'), 'does not preserve Claude-only Agent tool');
|
|
assert.ok(!frontmatter.includes('AskUserQuestion'), 'does not preserve Claude-only AskUserQuestion tool');
|
|
assert.ok(!result.includes('AskUserQuestion'), 'does not leave Claude-only tool references in the body');
|
|
assert.ok(result.includes('conversational prompting'), 'uses runtime-neutral body wording for user prompts');
|
|
});
|
|
});
|
|
|
|
// ─── neutralizeAgentReferences (#766) ─────────────────────────────────────────
|
|
|
|
describe('neutralizeAgentReferences', () => {
|
|
test('replaces standalone Claude with "the agent"', () => {
|
|
const input = 'Claude handles these decisions. Claude should read the file.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(!result.includes('Claude handles'), 'standalone Claude replaced');
|
|
assert.ok(result.includes('the agent handles'), 'replaced with "the agent"');
|
|
});
|
|
|
|
test('preserves Claude Code (product name)', () => {
|
|
const input = 'This is a Claude Code bug. Use Claude Code settings.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(result.includes('Claude Code bug'), 'Claude Code preserved');
|
|
assert.ok(result.includes('Claude Code settings'), 'Claude Code preserved');
|
|
});
|
|
|
|
test('preserves Claude model names', () => {
|
|
const input = 'Use Claude Opus for planning. Claude Sonnet for execution. Claude Haiku for research.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(result.includes('Claude Opus'), 'Opus preserved');
|
|
assert.ok(result.includes('Claude Sonnet'), 'Sonnet preserved');
|
|
assert.ok(result.includes('Claude Haiku'), 'Haiku preserved');
|
|
});
|
|
|
|
test('replaces CLAUDE.md with runtime instruction file', () => {
|
|
const input = 'Read CLAUDE.md for project instructions. Check ./CLAUDE.md if exists.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(result.includes('AGENTS.md'), 'CLAUDE.md -> AGENTS.md');
|
|
assert.ok(!result.includes('CLAUDE.md'), 'no CLAUDE.md remains');
|
|
});
|
|
|
|
test('uses different instruction file per runtime', () => {
|
|
const input = 'Read CLAUDE.md for instructions.';
|
|
assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md'));
|
|
assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md'));
|
|
assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md'));
|
|
});
|
|
|
|
test('removes AGENTS.md load-blocking instruction', () => {
|
|
const input = 'Do NOT load full `AGENTS.md` files — they contain agent definitions.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(!result.includes('Do NOT load full'), 'blocking instruction removed');
|
|
});
|
|
|
|
test('preserves claude- prefixes (CSS classes, package names)', () => {
|
|
const input = 'The claude-ctx session and claude-code package.';
|
|
const result = neutralizeAgentReferences(input, 'AGENTS.md');
|
|
assert.ok(result.includes('claude-ctx'), 'claude- prefix preserved');
|
|
assert.ok(result.includes('claude-code'), 'claude-code preserved');
|
|
});
|
|
});
|
|
|
|
// ─── OpenCode-family skill converters (SKILL.md) — #784 ──────────────────────
|
|
|
|
const SKILL_SAMPLE_COMMAND = `---
|
|
description: Show available GSD commands and usage guide
|
|
argument-hint: "[topic]"
|
|
allowed-tools:
|
|
- Read
|
|
- Bash
|
|
---
|
|
|
|
Run \`/gsd:help\` to see the guide. AskUserQuestion when unsure.
|
|
`;
|
|
|
|
const SKILL_BETA_COMMAND = `---
|
|
description: "[BETA] Offload plan phase to the cloud and import back."
|
|
---
|
|
|
|
Body for /gsd:ultraplan-phase.
|
|
`;
|
|
|
|
describe('convertClaudeCommandToOpencodeSkill / convertClaudeCommandToKiloSkill (#784)', () => {
|
|
const cases = [
|
|
{ label: 'opencode', convert: convertClaudeCommandToOpencodeSkill },
|
|
{ label: 'kilo', convert: convertClaudeCommandToKiloSkill },
|
|
];
|
|
|
|
for (const { label, convert } of cases) {
|
|
describe(`${label} skill conversion`, () => {
|
|
test('emits SKILL.md frontmatter with name matching the skill dir', () => {
|
|
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
|
assert.ok(out.startsWith('---\n'), 'opens with frontmatter');
|
|
assert.match(out, /^name: gsd-help$/m, 'name equals the skill name');
|
|
});
|
|
|
|
test('preserves the description from the source command', () => {
|
|
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
|
assert.match(out, /^description: "Show available GSD commands and usage guide"$/m);
|
|
});
|
|
|
|
test('drops the command tools/permission block (skills inherit perms)', () => {
|
|
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
|
const fmEnd = out.indexOf('\n---', 4);
|
|
const fm = out.slice(0, fmEnd);
|
|
assert.ok(!/tools:/.test(fm), 'no tools block in skill frontmatter');
|
|
assert.ok(!/permission:/.test(fm), 'no permission block in skill frontmatter');
|
|
});
|
|
|
|
test('rewrites /gsd: colon refs to hyphen form in the body', () => {
|
|
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
|
assert.ok(!/\/gsd:/.test(out), 'no /gsd: colon refs remain');
|
|
assert.match(out, /\/gsd-help/, 'colon ref rewritten to hyphen form');
|
|
});
|
|
|
|
test('quotes descriptions with leading YAML flow indicators ([BETA])', () => {
|
|
const out = convert(SKILL_BETA_COMMAND, 'gsd-ultraplan-phase');
|
|
assert.match(out, /^description: "\[BETA\] /m, 'leading [BETA] safely quoted');
|
|
});
|
|
|
|
test('falls back to a synthetic description when none present', () => {
|
|
const out = convert('Body only, no frontmatter.', 'gsd-mystery');
|
|
assert.match(out, /^name: gsd-mystery$/m);
|
|
assert.match(out, /^description: "Run GSD workflow gsd-mystery\."$/m);
|
|
});
|
|
});
|
|
}
|
|
});
|