Files
krow_backend/scripts/cases.mjs
2026-08-24 13:06:29 +05:30

269 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Adversarial case corpus for Phase 4D parser conformance.
*
* Each case is raw bytes as an author could actually produce them. Nothing here
* is normalized on the way in: the point is what the two parsers do with the
* awkward form, so the awkward form is what is stored.
*/
const SKILL = [
'---',
'id: sample-skill',
'name: Sample Skill',
'description: A sample skill.',
'pages:',
' - candidates',
'status: active',
'actions:',
' - navigate_to_candidates',
'---',
'',
'# Sample Skill',
'',
'## Purpose',
'',
'- Read the pipeline.',
'',
'## Capabilities',
'',
'- Summarize candidates.',
'',
].join('\n');
const AGENT = [
'---',
'id: sample-agent',
'name: Sample Agent',
'description: A sample agent.',
'icon: users',
'status: published',
'version: 2',
'reasoning: balanced',
'trigger: Use on candidates.',
'pages:',
' - candidates',
'skills:',
' - candidate-search',
'starters:',
' - label: Who is waiting?',
' prompt: Who is waiting on a decision?',
'permissions:',
' owner: demo@krow.app',
' access: all',
'---',
'',
'# Sample Agent',
'',
'## Instructions',
'',
'Answer about candidates.',
'',
'## Purpose',
'',
'- Report the pipeline.',
'',
].join('\n');
/** A skill body, with the frontmatter lines replaced wholesale. */
const skillWith = (fmLines) => ['---', ...fmLines, '---', '', '# Sample Skill', '', '## Purpose', '', '- Read the pipeline.', ''].join('\n');
const agentWith = (fmLines) => ['---', ...fmLines, '---', '', '# Sample Agent', '', '## Instructions', '', 'Answer.', ''].join('\n');
const BASE_SKILL_FM = [
'id: sample-skill',
'name: Sample Skill',
'description: A sample skill.',
'pages:',
' - candidates',
];
const BASE_AGENT_FM = [
'id: sample-agent',
'name: Sample Agent',
'description: A sample agent.',
'pages:',
' - candidates',
];
/** BASE_SKILL_FM with one line appended. */
const skillPlus = (...extra) => skillWith([...BASE_SKILL_FM, ...extra]);
const agentPlus = (...extra) => agentWith([...BASE_AGENT_FM, ...extra]);
export const CASES = [
/* ── Baselines ────────────────────────────────────────────────────────── */
{ name: 'baseline-skill', kind: 'skill', raw: SKILL },
{ name: 'baseline-agent', kind: 'agent', raw: AGENT },
/* ── Byte-level shape ─────────────────────────────────────────────────── */
{ name: 'utf8-bom', kind: 'skill', raw: '' + SKILL },
{ name: 'utf8-bom-agent', kind: 'agent', raw: '' + AGENT },
{ name: 'crlf', kind: 'skill', raw: SKILL.replace(/\n/g, '\r\n') },
{ name: 'crlf-agent', kind: 'agent', raw: AGENT.replace(/\n/g, '\r\n') },
{ name: 'cr-only', kind: 'skill', raw: SKILL.replace(/\n/g, '\r') },
{ name: 'bom-crlf-blankline', kind: 'skill', raw: '\r\n' + SKILL.replace(/\n/g, '\r\n') },
{ name: 'leading-blank-line', kind: 'skill', raw: '\n' + SKILL },
{ name: 'multiple-leading-blank-lines', kind: 'skill', raw: '\n\n\n' + SKILL },
{ name: 'leading-spaces-then-blank-lines', kind: 'skill', raw: ' \n \n' + SKILL },
{ name: 'no-trailing-newline', kind: 'skill', raw: SKILL.replace(/\n+$/, '') },
{ name: 'many-trailing-newlines', kind: 'skill', raw: SKILL + '\n\n\n' },
{ name: 'trailing-spaces-on-values', kind: 'skill', raw: skillWith(BASE_SKILL_FM.map((l) => l + ' ')) },
{ name: 'trailing-ws-after-open-fence', kind: 'skill', raw: SKILL.replace(/^---/, '--- ') },
{ name: 'trailing-tab-after-close-fence', kind: 'skill', raw: SKILL.replace(/\n---\n/, '\n---\t\n') },
{ name: 'frontmatter-only-no-body', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---' },
{ name: 'frontmatter-only-trailing-newline', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---\n' },
/* ── Scalars ──────────────────────────────────────────────────────────── */
{ name: 'double-quoted-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Sample Skill"', 'description: "A sample."', 'pages:', ' - candidates']) },
{ name: 'single-quoted-scalar', kind: 'skill', raw: skillWith(["id: sample-skill", "name: 'Sample Skill'", "description: 'A sample.'", 'pages:', ' - candidates']) },
{ name: 'colon-in-quoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Sample: Skill"', 'description: "Note: read this."', 'pages:', ' - candidates']) },
{ name: 'colon-in-unquoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: Note: read this.', 'pages:', ' - candidates']) },
{ name: 'hash-in-quoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Shift #1"', 'description: "Tag #ops"', 'pages:', ' - candidates']) },
{ name: 'hash-unquoted-trailing-comment', kind: 'skill', raw: skillPlus('category: ops # a trailing comment') },
{ name: 'hash-unquoted-midword', kind: 'skill', raw: skillPlus('category: ops#1') },
{ name: 'doubled-quote-escape', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "She said ""go"""', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'full-line-comment', kind: 'skill', raw: skillWith(['# a comment line', ...BASE_SKILL_FM]) },
{ name: 'empty-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description:', 'pages:', ' - candidates']) },
{ name: 'tilde-scalar', kind: 'skill', raw: skillPlus('category: ~') },
{ name: 'null-scalar', kind: 'skill', raw: skillPlus('category: null') },
{ name: 'boolean-scalar', kind: 'agent', raw: agentPlus('webSearch: true') },
{ name: 'integer-scalar', kind: 'agent', raw: agentPlus('version: 3') },
{ name: 'float-scalar', kind: 'agent', raw: agentPlus('version: 1.5') },
{ name: 'negative-integer', kind: 'agent', raw: agentPlus('version: -2') },
/* ── Collections ──────────────────────────────────────────────────────── */
{ name: 'empty-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', 'actions:']) },
{ name: 'inline-flow-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages: [candidates]']) },
{ name: 'inline-flow-map', kind: 'agent', raw: agentPlus('permissions: {owner: a, access: all}') },
{ name: 'sequence-of-mappings', kind: 'agent', raw: AGENT },
{ name: 'nested-mapping', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' owner: demo@krow.app', ' access: specific', ' people:', ' - user: a@b.c', ' role: editor']) },
{ name: 'dash-alone-nested-block', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' -', ' label: Hello', ' prompt: Hello there']) },
{ name: 'tab-indented-sequence', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', '\t- candidates']) },
{ name: 'four-space-indent', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'ragged-indent', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', ' - positions']) },
/* ── Multiline / unsupported YAML ─────────────────────────────────────── */
{ name: 'block-scalar-literal', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: |', ' Line one.', ' Line two.', 'pages:', ' - candidates']) },
{ name: 'block-scalar-folded', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: >', ' Line one.', 'pages:', ' - candidates']) },
{ name: 'anchor-and-alias', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: &n Sample Skill', 'description: *n', 'pages:', ' - candidates']) },
{ name: 'multi-document', kind: 'skill', raw: skillWith([...BASE_SKILL_FM, '---', 'id: second']) },
/* ── Keys ─────────────────────────────────────────────────────────────── */
{ name: 'duplicate-key', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: First Name', 'name: Second Name', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'duplicate-key-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', 'pages:', ' - positions']) },
{ name: 'unsupported-frontmatter-field', kind: 'skill', raw: skillPlus('unknownField: whatever') },
{ name: 'unsupported-field-agent', kind: 'agent', raw: agentPlus('nonsense: 1') },
{ name: 'key-with-space', kind: 'skill', raw: skillWith(['id: sample-skill', 'my key: value', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'uppercase-key', kind: 'skill', raw: skillPlus('Category: Ops') },
/* ── Malformed ────────────────────────────────────────────────────────── */
{ name: 'malformed-yaml-bare-line', kind: 'skill', raw: skillWith(['id: sample-skill', 'this is not a pair', 'name: Sample Skill', 'pages:', ' - candidates']) },
{ name: 'malformed-open-fence-two-dashes', kind: 'skill', raw: SKILL.replace(/^---/, '--') },
{ name: 'malformed-open-fence-four-dashes', kind: 'skill', raw: SKILL.replace(/^---/, '----') },
{ name: 'malformed-open-fence-indented', kind: 'skill', raw: ' ' + SKILL },
{ name: 'malformed-open-fence-text-after', kind: 'skill', raw: SKILL.replace(/^---/, '---yaml') },
{ name: 'malformed-close-fence-two-dashes', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n--\n\n# Body\n' },
{ name: 'malformed-close-fence-missing', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n\n# Body\n' },
{ name: 'malformed-close-fence-four-dashes', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n----\n\n# Body\n' },
{ name: 'empty-fence-pair', kind: 'skill', raw: '---\n---\n\n# Body\n' },
{ name: 'empty-fence-with-blank', kind: 'skill', raw: '---\n\n---\n\n# Body\n' },
{ name: 'missing-frontmatter', kind: 'skill', raw: '# Sample Skill\n\n## Purpose\n\n- Read the pipeline.\n' },
{ name: 'empty-markdown', kind: 'skill', raw: '' },
{ name: 'whitespace-only-markdown', kind: 'skill', raw: ' \n\n\t\n' },
{ name: 'frontmatter-is-a-sequence', kind: 'skill', raw: '---\n- one\n- two\n---\n\n# Body\n' },
/* ── Field-level validity ─────────────────────────────────────────────── */
{ name: 'invalid-definition-id-uppercase', kind: 'skill', raw: skillWith(['id: Sample_Skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-definition-id-leading-dash', kind: 'skill', raw: skillWith(['id: -sample', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-definition-id-underscore', kind: 'skill', raw: skillWith(['id: sample_skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-id-derives-from-name', kind: 'skill', raw: skillWith(['name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-name', kind: 'skill', raw: skillWith(['id: sample-skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-pages', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.']) },
{ name: 'unknown-page', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - nowhere']) },
{ name: 'page-alias-uppercase', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - Candidates']) },
{ name: 'page-alias-underscore', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - talent_pool']) },
{ name: 'invalid-status-skill', kind: 'skill', raw: skillPlus('status: bogus') },
{ name: 'inactive-status-skill', kind: 'skill', raw: skillPlus('status: inactive') },
{ name: 'invalid-status-agent', kind: 'agent', raw: agentPlus('status: bogus') },
{ name: 'invalid-reasoning-agent', kind: 'agent', raw: agentPlus('reasoning: turbo') },
{ name: 'invalid-icon-agent', kind: 'agent', raw: agentPlus('icon: rocket') },
{ name: 'invalid-version-agent', kind: 'agent', raw: agentPlus('version: zero') },
{ name: 'invalid-field-type-pages-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages: candidates']) },
{ name: 'invalid-field-type-pages-scalar-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages: candidates']) },
{ name: 'invalid-field-type-name-list', kind: 'skill', raw: skillWith(['id: sample-skill', 'name:', ' - a', ' - b', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-permissions-access', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' access: nobody']) },
{ name: 'self-subagent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'subagents:', ' - sample-agent']) },
/* ── Size ─────────────────────────────────────────────────────────────── */
{ name: 'oversized-markdown', kind: 'skill', raw: skillPlus() + '\n' + 'x'.repeat(65600) },
{ name: 'at-size-bound', kind: 'skill', raw: (() => { const b = skillPlus(); return b + 'y'.repeat(65536 - b.length); })() },
/* ── Gaps closed after the first oracle run ───────────────────────────── */
{ name: 'missing-name-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-pages-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.']) },
{ name: 'unknown-page-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - nowhere']) },
{ name: 'oversized-agent', kind: 'agent', raw: agentPlus() + '\n' + 'x'.repeat(65600) },
{ name: 'visibility-field-personal', kind: 'skill', raw: skillPlus('visibility: personal') },
{ name: 'visibility-field-invalid', kind: 'skill', raw: skillPlus('visibility: nobody') },
{ name: 'blank-page-entry-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates', ' - ""']) },
{ name: 'duplicate-page-entry-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates', ' - candidates']) },
{ name: 'id-with-trailing-space', kind: 'skill', raw: skillWith(['id: sample-skill ', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-quoted', kind: 'skill', raw: skillWith(['id: "sample-skill"', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'starters-plain-strings-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' - Who is waiting?', ' - Where are the gaps?']) },
{ name: 'starter-missing-label-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' - prompt: Only a prompt']) },
{ name: 'knowledge-note-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A note', ' body: The body of the note.']) },
{ name: 'knowledge-bad-kind-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A note', ' kind: rumour', ' body: x']) },
{ name: 'knowledge-link-without-url-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A link', ' kind: link']) },
{ name: 'skills-scalar-coerced-agent', kind: 'agent', raw: agentPlus('skills: candidate-search') },
{ name: 'skills-blank-entry-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'skills:', ' - candidate-search', ' - ""']) },
{ name: 'web-search-snake-case-agent', kind: 'agent', raw: agentPlus('web_search: true') },
{ name: 'web-search-string-true-agent', kind: 'agent', raw: agentPlus('webSearch: "true"') },
{ name: 'version-quoted-integer-agent', kind: 'agent', raw: agentPlus('version: "3"') },
{ name: 'version-zero-agent', kind: 'agent', raw: agentPlus('version: 0') },
{ name: 'version-empty-agent', kind: 'agent', raw: agentPlus('version:') },
{ name: 'crlf-inside-frontmatter-only', kind: 'skill', raw: (() => { const i = SKILL.indexOf('\n---\n', 3); return SKILL.slice(0, i).replace(/\n/g, '\r\n') + SKILL.slice(i); })() },
{ name: 'body-whitespace-only-after-fence', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---\n \n\t\n' },
{ name: 'description-whitespace-only', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: " "', 'pages:', ' - candidates']) },
{ name: 'permissions-null-agent', kind: 'agent', raw: agentPlus('permissions:') },
{ name: 'permissions-people-bad-role-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' access: specific', ' people:', ' - user: a@b.c', ' role: overlord']) },
/* ── Gaps closed after the second oracle run ──────────────────────────────
*
* Two rules had no case that could tell a correct implementation from a
* plausible wrong one, so both were being asserted by accident:
*
* The id fallback chain. Every earlier case either declares `id:` or
* declares a `name:` to slug, so the THIRD link — the default path, which
* is the literal `custom` on both sides — was never reached by a case that
* is otherwise valid. `id-omitted-unnamed` is that case: the frontend
* accepts it as the skill `custom`, and a backend deriving no id would
* refuse it on save.
*
* Text coercion. `name`, `description`, `category`, `trigger` and the
* entries of `pages` are text columns in migration 000005, and a definition
* may write any of them as a list, a number or a boolean. The frontend
* keeps the raw value on the record and stringifies it at each use; the
* backend has to choose the string at the boundary. These cases pin which
* string it chooses.
*/
{ name: 'id-omitted-unnamed', kind: 'skill', raw: skillWith(['description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-named', kind: 'skill', raw: skillWith(['name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-unnamed-agent', kind: 'agent', raw: agentWith(['description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-named-agent', kind: 'agent', raw: agentWith(['name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-name-unsluggable', kind: 'skill', raw: skillWith(['name: "!!!"', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'name-numeric', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: 42', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'name-boolean', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: true', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'description-list', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description:', ' - one', ' - two', 'pages:', ' - candidates']) },
{ name: 'description-numeric', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: 7', 'pages:', ' - candidates']) },
{ name: 'pages-numeric-entry', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', ' - 5']) },
{ name: 'pages-mapping-entry', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - page: candidates']) },
{ name: 'trigger-list-agent', kind: 'agent', raw: agentPlus('trigger:', ' - one', ' - two') },
{ name: 'name-list-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name:', ' - a', ' - b', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'category-numeric', kind: 'skill', raw: skillPlus('category: 3') },
/* The other backend-only bound. agent_definitions.version is a PostgreSQL
`integer`; the frontend accepts any whole number of 1 or more, so a version
above 2^31-1 is one the editor takes and the database cannot store. It had
no case at all, which left the rule asserted by nobody. */
{ name: 'version-above-int32-agent', kind: 'agent', raw: agentPlus('version: 3000000000') },
{ name: 'version-at-int32-agent', kind: 'agent', raw: agentPlus('version: 2147483647') },
];