Replace description regex with yaml.parse/stringify in codex.js
Use the yaml library (already a project dependency) instead of a hand-rolled regex to parse and re-serialize frontmatter descriptions, matching the pattern used in manifest-generator.js. Update tests to validate round-trip correctness rather than exact quoting style. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
43672d33c1
commit
7f81518896
|
|
@ -1,12 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* Tests for CodexSetup.transformToSkillFormat
|
* Tests for CodexSetup.transformToSkillFormat
|
||||||
*
|
*
|
||||||
* Demonstrates that the description regex mangles descriptions containing quotes.
|
* Validates that descriptions round-trip correctly through parse/stringify,
|
||||||
|
* producing valid YAML regardless of input quoting style.
|
||||||
*
|
*
|
||||||
* Usage: node test/test-codex-transform.js
|
* Usage: node test/test-codex-transform.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
|
||||||
// ANSI colors
|
// ANSI colors
|
||||||
const colors = {
|
const colors = {
|
||||||
|
|
@ -31,6 +33,17 @@ function assert(condition, testName, detail) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the output frontmatter and return the description value.
|
||||||
|
* Validates the output is well-formed YAML that parses back correctly.
|
||||||
|
*/
|
||||||
|
function parseOutputDescription(output) {
|
||||||
|
const match = output.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (!match) return null;
|
||||||
|
const parsed = yaml.parse(match[1]);
|
||||||
|
return parsed?.description;
|
||||||
|
}
|
||||||
|
|
||||||
// Import the class under test
|
// Import the class under test
|
||||||
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
|
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
|
||||||
|
|
||||||
|
|
@ -38,39 +51,56 @@ const setup = new CodexSetup();
|
||||||
|
|
||||||
console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`);
|
console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`);
|
||||||
|
|
||||||
// --- Passing case: simple description, no quotes ---
|
// --- Simple description, no quotes ---
|
||||||
{
|
{
|
||||||
const input = `---\ndescription: A simple description\n---\n\nBody content here.`;
|
const input = `---\ndescription: A simple description\n---\n\nBody content here.`;
|
||||||
const result = setup.transformToSkillFormat(input, 'my-skill');
|
const result = setup.transformToSkillFormat(input, 'my-skill');
|
||||||
const expected = `---\nname: my-skill\ndescription: 'A simple description'\n---\n\nBody content here.`;
|
const desc = parseOutputDescription(result);
|
||||||
assert(result === expected, 'simple description without quotes', `got: ${JSON.stringify(result)}`);
|
assert(desc === 'A simple description', 'simple description round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||||
|
assert(result.includes('\nBody content here.'), 'body preserved for simple description');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Description with embedded single quotes (from double-quoted YAML input) ---
|
// --- Description with embedded single quotes (from double-quoted YAML input) ---
|
||||||
{
|
{
|
||||||
const input = `---\ndescription: "can't stop won't stop"\n---\n\nBody content here.`;
|
const input = `---\ndescription: "can't stop won't stop"\n---\n\nBody content here.`;
|
||||||
const result = setup.transformToSkillFormat(input, 'my-skill');
|
const result = setup.transformToSkillFormat(input, 'my-skill');
|
||||||
|
const desc = parseOutputDescription(result);
|
||||||
// Output should have properly escaped YAML single-quoted scalar: '' for each '
|
assert(desc === "can't stop won't stop", 'description with apostrophes round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||||
const expected = `---\nname: my-skill\ndescription: 'can''t stop won''t stop'\n---\n\nBody content here.`;
|
assert(result.includes('\nBody content here.'), 'body preserved for quoted description');
|
||||||
assert(result === expected, 'description with embedded single quotes produces valid escaped YAML', `got: ${JSON.stringify(result)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Description with embedded single quote produces valid YAML ---
|
// --- Description with embedded single quote ---
|
||||||
{
|
{
|
||||||
const input = `---\ndescription: "it's a test"\n---\n\nBody.`;
|
const input = `---\ndescription: "it's a test"\n---\n\nBody.`;
|
||||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||||
const expected = `---\nname: test-skill\ndescription: 'it''s a test'\n---\n\nBody.`;
|
const desc = parseOutputDescription(result);
|
||||||
assert(result === expected, 'description with apostrophe produces valid YAML', `got: ${JSON.stringify(result)}`);
|
assert(desc === "it's a test", 'description with apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) ---
|
// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) ---
|
||||||
{
|
{
|
||||||
const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`;
|
const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`;
|
||||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||||
// Input has don''t (YAML-escaped). Should round-trip to don''t in output.
|
const desc = parseOutputDescription(result);
|
||||||
const expected = `---\nname: test-skill\ndescription: 'don''t panic'\n---\n\nBody.`;
|
assert(desc === "don't panic", 'single-quoted escaped apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||||
assert(result === expected, 'single-quoted description with escaped apostrophe round-trips correctly', `got: ${JSON.stringify(result)}`);
|
}
|
||||||
|
|
||||||
|
// --- Verify name is set correctly ---
|
||||||
|
{
|
||||||
|
const input = `---\ndescription: test\n---\n\nBody.`;
|
||||||
|
const result = setup.transformToSkillFormat(input, 'my-custom-skill');
|
||||||
|
const match = result.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
const parsed = yaml.parse(match[1]);
|
||||||
|
assert(parsed.name === 'my-custom-skill', 'name field matches skillName argument', `got name: ${JSON.stringify(parsed.name)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- No frontmatter wraps content ---
|
||||||
|
{
|
||||||
|
const input = 'Just some content without frontmatter.';
|
||||||
|
const result = setup.transformToSkillFormat(input, 'bare-skill');
|
||||||
|
const desc = parseOutputDescription(result);
|
||||||
|
assert(desc === 'bare-skill', 'no-frontmatter fallback uses skillName as description', `got description: ${JSON.stringify(desc)}`);
|
||||||
|
assert(result.includes('Just some content without frontmatter.'), 'body preserved when no frontmatter');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Summary ---
|
// --- Summary ---
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||||
|
const yaml = require('yaml');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -205,28 +206,18 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
const frontmatter = fmMatch[1];
|
const frontmatter = fmMatch[1];
|
||||||
const body = fmMatch[2];
|
const body = fmMatch[2];
|
||||||
|
|
||||||
// Extract description from existing frontmatter, handling quoted and unquoted values
|
// Parse frontmatter with yaml library to handle all quoting variants
|
||||||
const descMatch = frontmatter.match(/^description:\s*(?:'((?:[^']|'')*)'|"((?:[^"\\]|\\.)*)"|(.*))\s*$/m);
|
|
||||||
let description;
|
let description;
|
||||||
if (descMatch) {
|
try {
|
||||||
if (descMatch[1] != null) {
|
const parsed = yaml.parse(frontmatter);
|
||||||
// Single-quoted YAML: unescape '' to '
|
description = parsed?.description || `${skillName} skill`;
|
||||||
description = descMatch[1].replaceAll("''", "'");
|
} catch {
|
||||||
} else if (descMatch[2] == null) {
|
|
||||||
description = descMatch[3];
|
|
||||||
} else {
|
|
||||||
// Double-quoted YAML: unescape \" to "
|
|
||||||
description = descMatch[2].replaceAll(String.raw`\"`, '"');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
description = `${skillName} skill`;
|
description = `${skillName} skill`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape single quotes for YAML single-quoted scalar (a literal ' becomes '')
|
// Build new frontmatter with only skills-spec fields, let yaml handle quoting
|
||||||
const safeDescription = description.replaceAll("'", "''");
|
const newFrontmatter = yaml.stringify({ name: skillName, description }).trimEnd();
|
||||||
|
return `---\n${newFrontmatter}\n---\n${body}`;
|
||||||
// Build new frontmatter with only skills-spec fields
|
|
||||||
return `---\nname: ${skillName}\ndescription: '${safeDescription}'\n---\n${body}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue