Fix description regex to handle quotes in transformToSkillFormat
The regex that extracts descriptions from YAML frontmatter did not properly parse quoted values. Descriptions containing apostrophes (e.g. "can't") produced invalid YAML when re-wrapped in single quotes. Replace the naive regex with proper handling of single-quoted, double-quoted, and unquoted YAML values, and escape inner single quotes using YAML '' syntax before re-wrapping. Add tests for transformToSkillFormat covering plain, double-quoted, and single-quoted descriptions with embedded apostrophes.
This commit is contained in:
parent
746bd50d19
commit
26dd80e021
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Tests for CodexSetup.transformToSkillFormat
|
||||
*
|
||||
* Demonstrates that the description regex mangles descriptions containing quotes.
|
||||
*
|
||||
* Usage: node test/test-codex-transform.js
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail) {
|
||||
if (condition) {
|
||||
console.log(` ${colors.green}PASS${colors.reset} ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ${colors.red}FAIL${colors.reset} ${testName}`);
|
||||
if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Import the class under test
|
||||
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
|
||||
|
||||
const setup = new CodexSetup();
|
||||
|
||||
console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`);
|
||||
|
||||
// --- Passing case: simple description, no quotes ---
|
||||
{
|
||||
const input = `---\ndescription: A simple description\n---\n\nBody content here.`;
|
||||
const result = setup.transformToSkillFormat(input, 'my-skill');
|
||||
const expected = `---\nname: my-skill\ndescription: 'A simple description'\n---\n\nBody content here.`;
|
||||
assert(result === expected, 'simple description without quotes', `got: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
// --- 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 result = setup.transformToSkillFormat(input, 'my-skill');
|
||||
|
||||
// Output should have properly escaped YAML single-quoted scalar: '' for each '
|
||||
const expected = `---\nname: my-skill\ndescription: 'can''t stop won''t stop'\n---\n\nBody content here.`;
|
||||
assert(result === expected, 'description with embedded single quotes produces valid escaped YAML', `got: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
// --- Description with embedded single quote produces valid YAML ---
|
||||
{
|
||||
const input = `---\ndescription: "it's a test"\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||
|
||||
// Verify the inner quote is escaped: description: 'it''s a test'
|
||||
const descLine = result.split('\n').find((l) => l.startsWith('description:'));
|
||||
const value = descLine.replace('description: ', '');
|
||||
|
||||
// Check that single quotes inside the value are properly escaped
|
||||
// In YAML single-quoted scalars, a literal ' must be written as ''
|
||||
const innerContent = value.slice(1, -1); // strip outer quotes
|
||||
const hasUnescapedQuote = innerContent.match(/[^']'[^']/);
|
||||
assert(
|
||||
!hasUnescapedQuote,
|
||||
'description with apostrophe produces valid YAML (no unescaped inner quotes)',
|
||||
`description line: ${descLine}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) ---
|
||||
{
|
||||
const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||
// Input has don''t (YAML-escaped). Should round-trip to don''t in output.
|
||||
const expected = `---\nname: test-skill\ndescription: 'don''t panic'\n---\n\nBody.`;
|
||||
assert(result === expected, 'single-quoted description with escaped apostrophe round-trips correctly', `got: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
console.log(`\n${passed} passed, ${failed} failed\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
|
@ -205,12 +205,28 @@ class CodexSetup extends BaseIdeSetup {
|
|||
const frontmatter = fmMatch[1];
|
||||
const body = fmMatch[2];
|
||||
|
||||
// Extract description from existing frontmatter
|
||||
const descMatch = frontmatter.match(/^description:\s*['"]?(.*?)['"]?\s*$/m);
|
||||
const description = descMatch ? descMatch[1] : `${skillName} skill`;
|
||||
// Extract description from existing frontmatter, handling quoted and unquoted values
|
||||
const descMatch = frontmatter.match(/^description:\s*(?:'((?:[^']|'')*)'|"((?:[^"\\]|\\.)*)"|(.*))\s*$/m);
|
||||
let description;
|
||||
if (descMatch) {
|
||||
if (descMatch[1] != null) {
|
||||
// Single-quoted YAML: unescape '' to '
|
||||
description = descMatch[1].replaceAll("''", "'");
|
||||
} else if (descMatch[2] == null) {
|
||||
description = descMatch[3];
|
||||
} else {
|
||||
// Double-quoted YAML: unescape \" to "
|
||||
description = descMatch[2].replaceAll(String.raw`\"`, '"');
|
||||
}
|
||||
} else {
|
||||
description = `${skillName} skill`;
|
||||
}
|
||||
|
||||
// Escape single quotes for YAML single-quoted scalar (a literal ' becomes '')
|
||||
const safeDescription = description.replaceAll("'", "''");
|
||||
|
||||
// Build new frontmatter with only skills-spec fields
|
||||
return `---\nname: ${skillName}\ndescription: '${description}'\n---\n${body}`;
|
||||
return `---\nname: ${skillName}\ndescription: '${safeDescription}'\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue