Files
krow_talent_app/scripts/skill-check.mjs

1421 lines
62 KiB
JavaScript
Raw 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.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Skill pipeline check — Owliver skills and Board (UI) skills.
*
* Runs the *real* module graph through Vite, so `import.meta.glob`, the `@/`
* alias and the Markdown loading all behave exactly as they do in the app. That
* matters: the reliability problem this script exists for was never in the
* Markdown, it was in what the pipeline did with a definition it could only
* partly read, and a mock of that pipeline would have reproduced none of it.
*
* node scripts/skill-check.mjs # against source, as the dev server sees it
* node scripts/skill-check.mjs --dist # also assert the built bundle carries every definition
*
* Exits non-zero on failure, so it can gate a build.
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'vite';
const ROOT = process.cwd();
const results = [];
const record = (name, pass, detail = '') => {
results.push({ name, pass, detail });
const mark = pass ? ' ok ' : ' FAIL ';
console.log(`[${mark}] ${name}${detail ? ` — ${detail}` : ''}`);
};
const server = await createServer({
root: ROOT,
server: { middlewareMode: true },
appType: 'custom',
logLevel: 'error',
});
const reg = await server.ssrLoadModule('/src/lib/skills/registry.js');
const placement = await server.ssrLoadModule('/src/components/ai-assistant/placement.js');
const contextFor = (pageKey) => Object.entries(placement.PLACEMENT_ROUTES)
.find(([route]) => reg.pageKeyForRoute(route) === pageKey)?.[1] ?? null;
/* ── 1. Discovery ─────────────────────────────────────────────────────────── */
console.log('\n── Discovery ──');
const onDisk = readdirSync(join(ROOT, 'src/skills'))
.flatMap((dir) => readdirSync(join(ROOT, 'src/skills', dir)).map((f) => `${dir}/${f}`))
.filter((f) => f.endsWith('.md'));
record(
'every .md under src/skills registers',
reg.SKILLS.length === onDisk.length,
`${reg.SKILLS.length} registered / ${onDisk.length} files`
);
record(
'no duplicate skill ids',
new Set(reg.SKILLS.map((s) => s.id)).size === reg.SKILLS.length,
`${new Set(reg.SKILLS.map((s) => s.id)).size} unique ids`
);
record(
'every skill declares at least one page',
reg.SKILLS.every((s) => s.pages.length > 0)
);
/* Both managed lists are non-empty — the Workspace → Skills tabs. */
const uiFacet = reg.skillsWithFacet(reg.SKILLS, 'ui');
const owliverFacet = reg.skillsWithFacet(reg.SKILLS, 'owliver');
record('Owliver Skills list is populated', owliverFacet.length > 0, `${owliverFacet.length} skills`);
record(
'Board Skills list resolves without error',
Array.isArray(uiFacet),
`${uiFacet.length} skills declare a \`ui:\` block`
);
/* ── 2. Registration integrity: nothing half-loads in silence ─────────────── */
console.log('\n── Registration integrity ──');
const clean = reg.readSkillRegistry([]);
record(
'shipped registry reports no diagnostics',
clean.diagnostics.length === 0,
clean.diagnostics.map((d) => d.message).join(' | ') || 'none'
);
record(
'every shipped skill registered every capability it declared',
reg.SKILLS.every((s) => !(s.owliverErrors?.length || s.uiErrors?.length))
);
/* An unreadable stored definition must be REPORTED, not swallowed. */
const brokenYaml = `---\nid: broken\n name: bad indent\n---\n# Broken\n`;
const withBroken = reg.readSkillRegistry([{ path: 'custom/broken.md', raw: brokenYaml }]);
record(
'unreadable stored skill is reported, not silently dropped',
withBroken.diagnostics.some((d) => d.kind === 'unreadable'),
withBroken.diagnostics.find((d) => d.kind === 'unreadable')?.message ?? 'NO DIAGNOSTIC'
);
/* A definition with one bad capability keeps the good one AND says so. */
const partial = `---
id: partial-check
name: Partial Check
description: One resolvable capability and one that names no source.
pages:
- positions
status: active
owliver:
enabled: true
capabilities:
- summary
- flow
responses:
summary:
source: position.activity
periods:
- today
---
# Partial Check
`;
const withPartial = reg.readSkillRegistry([{ path: 'custom/partial.md', raw: partial }]);
const partialSkill = withPartial.skills.find((s) => s.id === 'partial-check');
record(
'partly-resolvable skill still registers its good capability',
partialSkill?.owliver.capabilities.includes('summary'),
`capabilities = ${JSON.stringify(partialSkill?.owliver.capabilities)}`
);
record(
'and the capability it LOST is reported',
withPartial.diagnostics.some((d) => d.kind === 'incomplete' && d.skillId === 'partial-check'),
withPartial.diagnostics.find((d) => d.kind === 'incomplete')?.message ?? 'NO DIAGNOSTIC'
);
/* Shadowing a built-in is allowed, but must be visible. */
const shadow = `---\nid: create-position\nname: Shadow\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n# Shadow\n`;
const withShadow = reg.readSkillRegistry([{ path: 'custom/shadow.md', raw: shadow }]);
record(
'a custom skill overriding a built-in is reported',
withShadow.diagnostics.some((d) => d.kind === 'shadowed' && d.skillId === 'create-position'),
withShadow.diagnostics.find((d) => d.kind === 'shadowed')?.message ?? 'NO DIAGNOSTIC'
);
/* ── 3. Markdown parsing: no silent content loss ──────────────────────────── */
console.log('\n── Markdown parsing ──');
const cp = reg.SKILLS.find((s) => s.id === 'create-position');
record(
'wrapped bullets are read whole',
cp.capabilities.some((c) => c.endsWith('out of a single sentence.')),
cp.capabilities.find((c) => c.includes('single sentence')) ?? 'TRUNCATED'
);
record(
'conversation steps all parse',
cp.conversation.length === 7 && cp.conversation.every((s) => s.field && s.question),
`${cp.conversation.length} steps`
);
const numbered = reg.parseSkill(
`---\nid: numbered\nname: Numbered\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n1. First instruction.\n2. Second instruction.\n3. Third instruction.\n`,
{ custom: true }
);
record(
'numbered instructions are preserved',
numbered.capabilities.length === 3,
JSON.stringify(numbered.capabilities)
);
/* A section ends at the next `##`, and a heading that merely starts with the
same word is a different heading. Both halves matter: the first is what stops
`Capabilities` swallowing the rest of the file, the second is what stops a
near-miss heading being read as the real one. */
const bounded = reg.parseSkill(
`---\nid: bounded\nname: Bounded\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n- one\n- two\n\n## Capabilities (v2)\n\n- three\n\n## Actions\n\n- four\n`,
{ custom: true }
);
record(
'a section stops at the next heading',
bounded.capabilities.length === 2,
JSON.stringify(bounded.capabilities)
);
/* Sub-headings (`###`) belong to the section they sit under, and must not end
it — `\n##\s` would match `### ` if the space were not required. */
const nested = reg.parseSkill(
`---\nid: nested\nname: Nested\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n`
+ `## Capabilities\n\n- one\n\n### Detail\n\n- two\n\n## Actions\n\n- three\n`,
{ custom: true }
);
record(
'a `###` sub-heading does not truncate its section',
nested.capabilities.length === 2,
JSON.stringify(nested.capabilities)
);
/* Frontmatter of every shipped file parses and carries its required fields. */
let frontmatterOk = true;
const frontmatterDetail = [];
for (const s of reg.SKILLS) {
if (!s.id || !s.name || !s.pages.length) {
frontmatterOk = false;
frontmatterDetail.push(s.path);
}
}
record('every shipped skill has id, name and pages', frontmatterOk, frontmatterDetail.join(', '));
/* ── 4. Trigger / matching ────────────────────────────────────────────────── */
console.log('\n── Trigger matching ──');
const positions = contextFor('positions');
record('positions context resolves', Boolean(positions), String(positions));
const MATRIX = [
['create a position', 'create-position'],
['i want to hire', 'create-position'],
['post a job', 'create-position'],
['add a client', 'create-position'],
['show hiring activity', 'hiring-activity-assistant'],
['hiring flow', 'hiring-activity-assistant'],
['applications over time', 'hiring-activity-assistant'],
];
for (const [question, expected] of MATRIX) {
const got = reg.matchSkill(question, positions)?.id ?? null;
record(`trigger "${question}" -> ${expected}`, got === expected, got ?? 'no match');
}
/* Every claimant is enumerable, so a contested phrase is not invisible. */
const contender = `---
id: zz-contender
name: ZZ Contender
description: Also claims a phrase create-position owns.
pages:
- positions
status: active
triggers:
- create a position
owliver:
enabled: true
capabilities:
- summary
responses:
summary:
source: position.activity
periods:
- today
---
# ZZ Contender
`;
const contested = [{ path: 'custom/zz.md', raw: contender }];
const claimants = reg.matchSkills('create a position', positions, [], contested);
record(
'all trigger claimants are enumerable',
claimants.length === 2,
claimants.map((s) => s.id).join(', ')
);
record(
'a contested trigger is reported as a collision',
reg.readSkillRegistry(contested).diagnostics.some((d) => d.kind === 'trigger-collision'),
reg.readSkillRegistry(contested).diagnostics.find((d) => d.kind === 'trigger-collision')?.message ?? 'NO DIAGNOSTIC'
);
/* ── 5. Owliver suggestions ───────────────────────────────────────────────── */
console.log('\n── Suggestions ──');
const resolver = await server.ssrLoadModule('/src/lib/skills/owliverResolver.js');
const declared = resolver.owliverSuggestions(positions, [], [], {});
record(
'every declared suggestion names a capability the skill offers',
declared.every((c) => !c.skillCapability
|| reg.SKILLS.find((s) => s.id === c.skillId)?.owliver.capabilities.includes(c.skillCapability))
);
const byIntent = declared.map((c) => `${c.skillId}:${c.skillCapability}`);
record(
'no two declared suggestions resolve to one capability',
new Set(byIntent).size === byIntent.length,
byIntent.join(' | ')
);
const dynamic = await server.ssrLoadModule('/src/components/ai-assistant/dynamic.js');
/* The generator is exercised at three different data shapes: the labels must
track the data, and must never appear when the count behind them is zero. */
const factsFor = (n) => ({
postings: [], starvedPositions: new Array(n).fill({}), unscreened: new Array(n).fill({}),
scored: [], stalled: [], ranked: [], activity: [], missingCredentials: [],
});
const shapes = [0, 1, 7, 12];
let labelsOk = true;
const offenders = [];
for (const n of shapes) {
for (const chip of dynamic.buildPrompts('admin.positions', factsFor(n), null)) {
/* A complete intent asks or instructs. A raw fragment — "4 ready for
interview" — does neither, and is what this check exists to catch. */
const isIntent = /\?$/.test(chip.label) || /^(show|summari[sz]e|create|continue|compare|find|explain|what|which|who)\b/i.test(chip.label);
if (!isIntent) { labelsOk = false; offenders.push(`n=${n}: "${chip.label}"`); }
if (n === 0 && /\b0\b/.test(chip.label)) { labelsOk = false; offenders.push(`n=0 leaked a zero count: "${chip.label}"`); }
}
}
record('every generated label is a complete intent', labelsOk, offenders.join(' ; ') || 'all labels are intents');
/* Counts must move with the data rather than being baked in. */
const at7 = dynamic.buildPrompts('admin.positions', factsFor(7), null).map((c) => c.label);
const at12 = dynamic.buildPrompts('admin.positions', factsFor(12), null).map((c) => c.label);
record(
'labels are dynamic, not hardcoded',
at7.some((l) => l.includes('7')) && at12.some((l) => l.includes('12')) && !at7.some((l) => l.includes('12')),
`7 -> ${at7.find((l) => l.includes('7'))} | 12 -> ${at12.find((l) => l.includes('12'))}`
);
const atZero = dynamic.buildPrompts('admin.positions', factsFor(0), null).map((c) => c.label);
record(
'no count-bearing suggestion when the count is zero',
!atZero.some((l) => /\d/.test(l)),
atZero.join(' | ')
);
/**
* The workforce branch — the one the fragments came from.
*
* "4 ready for interview" and "2 strong for Event Server" were generated here,
* so a suggestion check that passes `null` for workforce proves nothing about
* them. Applications are dated today so the "applied today" branch is live too.
*/
const workforceAt = (readyCount) => {
const position = { id: 'p1', title: 'Event Server – Fine Dining', status: 'active', headcount: 5 };
const today = new Date();
const applications = Array.from({ length: readyCount }, (_, i) => ({
id: `a${i}`,
job_posting_id: 'p1',
email: `c${i}@example.com`,
applicant_name: `Candidate ${i}`,
status: i % 2 ? 'ai_screened' : 'shortlisted',
ai_score: 90,
created_date: today.toISOString(),
updated_date: today.toISOString(),
}));
return {
positions: [position],
currentPositionId: null,
context: { applications, today, profiles: [], staff: [], assignments: [] },
};
};
for (const n of [1, 4, 9]) {
const labels = dynamic
.buildPrompts('admin.positions', factsFor(2), workforceAt(n))
.map((c) => c.label);
const ready = labels.find((l) => /ready for interview/i.test(l));
record(
`workforce branch at ${n}: "ready for interview" is a complete question`,
Boolean(ready) && /\?$/.test(ready) && ready.includes(String(n)) && /^Which\b/.test(ready),
ready ?? 'NOT GENERATED'
);
/* Singular/plural has to track the count, or the fix trades one awkward
label for another. */
if (n === 1) {
record(
'a single candidate reads as one candidate',
ready === 'Which 1 candidate is ready for interview?',
ready ?? 'NOT GENERATED'
);
}
const fragment = labels.find((l) => /^\d+\s/.test(l));
record(
`workforce branch at ${n}: no bare status fragments`,
!fragment,
fragment ?? 'none'
);
}
/* ── 6. Authoring: what the editors save, and what they refuse ────────────── */
console.log('\n── Authoring ──');
const fields = await server.ssrLoadModule('/src/lib/skills/skillFields.js');
const templates = await server.ssrLoadModule('/src/lib/skills/customSkills.js');
const surfaces = await server.ssrLoadModule('/src/lib/skills/surfaces.js');
/* A template must produce something that works before it is edited. The Board
template used to default to a source its own default placement cannot read. */
record(
'the Board template validates as written',
reg.validateSkillSource(templates.uiSkillTemplate({
id: 'template-check', name: 'Template Check', pages: ['positions'],
})) === null,
reg.validateSkillSource(templates.uiSkillTemplate({
id: 'template-check', name: 'Template Check', pages: ['positions'],
})) || 'valid'
);
/* An Owliver block that can answer nothing is refused rather than saved and
then reported as a skill that does not work. */
record(
'an Owliver skill with no capabilities is refused',
Boolean(reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'],
}))),
reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'],
})) || 'ACCEPTED — should have been refused'
);
/* The section-can-never-resolve check, in both directions. */
const boardOn = (page, placement) => `---
id: context-check
name: Context Check
description: Reads position activity.
pages:
- ${page}
status: active
ui:
type: flow
placement: ${placement}
source: position.activity
---
# Context Check
`;
record(
'a position source on a page with no position is refused',
Boolean(reg.validateSkillSource(boardOn('analytics', 'after-header'))),
reg.validateSkillSource(boardOn('analytics', 'after-header')) || 'ACCEPTED — should have been refused'
);
record(
'the same source is refused above the Positions grid',
Boolean(reg.validateSkillSource(boardOn('positions', 'after-position-list'))),
reg.validateSkillSource(boardOn('positions', 'after-position-list')) || 'ACCEPTED — should have been refused'
);
record(
'and accepted inside a position card',
reg.validateSkillSource(boardOn('positions', 'after-position-card')) === null,
reg.validateSkillSource(boardOn('positions', 'after-position-card')) || 'accepted'
);
/* Owliver responses are deliberately NOT subject to that rule: an unmet need is
a question back, not a dead card. `hiring-activity-assistant` depends on it. */
record(
'a shipped Owliver skill reading one position still validates',
reg.validateSkillSource(
reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown
) === null
);
record(
'every shipped definition still validates',
reg.SKILLS.every((s) => reg.validateSkillSource(s.markdown) === null),
reg.SKILLS.filter((s) => reg.validateSkillSource(s.markdown)).map((s) => s.id).join(', ') || 'all valid'
);
/* Reading a definition into the editor fields — the upload path. */
for (const skill of reg.SKILLS.filter((s) => s.kind === 'assistant')) {
const read = fields.owliverFieldsFromSource(skill.markdown);
record(
`\`${skill.id}\` reads back into the Owliver fields`,
read.id === skill.id && read.name === skill.name
&& read.pages.join(',') === skill.pages.join(','),
`${read.name} / ${read.pages.join(', ')}`
);
}
record(
'an unparseable file leaves the fields empty rather than throwing',
fields.owliverFieldsFromSource('not a definition').id === ''
);
/* Writing a field back — the half that used to do nothing. */
const original = reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown;
const renamed = fields.patchFrontmatter(original, { name: 'Renamed Assistant' });
record(
'patching a field changes what the registry reads',
reg.parseSkill(renamed, { custom: true }).name === 'Renamed Assistant',
reg.parseSkill(renamed, { custom: true }).name
);
record(
'patching a field leaves the body untouched',
reg.parseSkill(renamed, { custom: true }).body === reg.parseSkill(original, { custom: true }).body
);
record(
'patching a field preserves frontmatter comments',
renamed.includes('# One suggestion per capability.')
);
record(
'patching a field changes nothing else',
original.split('\n').filter((l) => !l.startsWith('name:')).join('\n')
=== renamed.split('\n').filter((l) => !l.startsWith('name:')).join('\n')
);
record(
'a patched definition still validates',
reg.validateSkillSource(renamed) === null,
reg.validateSkillSource(renamed) || 'valid'
);
const repaged = fields.patchFrontmatter(original, { pages: ['analytics', 'activity'] });
record(
'patching a list replaces the whole block',
reg.parseSkill(repaged, { custom: true }).pages.join(',') === 'analytics,activity',
reg.parseSkill(repaged, { custom: true }).pages.join(',')
);
const restatused = fields.patchFrontmatter(original, { status: 'inactive' });
record(
'patching a key the file never declared adds it',
reg.parseSkill(restatused, { custom: true }).status === 'inactive'
);
/* Both keys, because that is what the editor's own field handler writes: the
registry reads capabilities as the union of the declared list and the keys of
`responses:`, so patching one without the other changes nothing. */
const recapped = fields.patchFrontmatter(original, {
'owliver.capabilities': ['summary'],
'owliver.responses': { summary: { source: 'position.activity', periods: ['today'] } },
});
record(
'patching a nested block rewrites only that block',
reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',') === 'summary',
reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',')
);
/* Asserted on the text, not on the parse: with `flow` no longer offered, the
registry correctly drops the suggestion that names it — which is the rule
working, not the patch reaching a sibling key it should not have. */
record(
'and leaves its siblings in the same block alone',
recapped.includes('Summarize hiring activity for this position')
&& recapped.includes('Show hiring activity as a flow')
&& recapped.includes(' enabled: true')
);
/* The two `ui:` shapes, told apart — the per-page form must not be overwritten
from four single-valued fields. */
record(
'the shorthand `ui:` form is recognised',
fields.uiShape(boardOn('positions', 'after-position-card')) === 'shorthand'
);
record(
'a definition with no `ui:` block reports none',
fields.uiShape(original) === 'none'
);
/* The picker and the validator read the same table. */
record(
'a position card supplies a position',
surfaces.contextSuppliedBy(['positions'], 'after-position-card').includes('positionId')
);
record(
'the Positions list supplies nothing',
surfaces.contextSuppliedBy(['positions'], 'after-position-list').length === 0
);
record(
'Analytics supplies nothing',
surfaces.contextSuppliedBy(['analytics'], 'after-header').length === 0
);
record(
'every surface declares what its placements provide',
surfaces.SKILL_SURFACES.every((s) => s.provides && typeof s.provides === 'object'),
surfaces.SKILL_SURFACES.filter((s) => !s.provides).map((s) => s.id).join(', ') || 'all declared'
);
/* ── 7. Upload hydration: a file, into the fields ─────────────────────────── */
console.log('\n── Upload hydration ──');
/**
* The shapes people actually upload.
*
* Every case here is a definition that arrived from outside the editors, which
* is the only way most definitions arrive. What is being asserted is not that
* the parser is lenient — it is that the three identity fields, the pages and
* the section a reader can see in the file are the ones the form shows.
*/
const COMPLETE = `---
id: hiring-activity-assistant
name: Hiring Activity Assistant
description: Answer questions about recent hiring activity on a position.
type: board
ui:
- page: Positions
placement: grid-card
source: position.activity
---
# Hiring Activity Assistant
`;
const NO_ID = `---
name: Hiring Activity Assistant
description: Answer questions about recent hiring activity.
pages:
- positions
---
# Hiring Activity Assistant
`;
const MULTI_PAGE = `---
id: hiring-activity
name: Hiring Activity
description: Hiring activity across the workspace.
ui:
- page: Positions
placement: grid-card
source: candidates.activity
- page: Analytics
placement: panel
source: hires.performance
---
# Hiring Activity
`;
/* Case 1 — a complete definition hydrates every field it declares. */
{
const f = fields.boardFieldsFromSource(COMPLETE);
record('upload: name hydrates', f.name === 'Hiring Activity Assistant', f.name || 'EMPTY');
record('upload: id hydrates', f.id === 'hiring-activity-assistant', f.id || 'EMPTY');
record(
'upload: description hydrates',
f.description === 'Answer questions about recent hiring activity on a position.',
f.description || 'EMPTY'
);
record('upload: pages come from the `ui:` entries', f.pages.join(',') === 'positions', f.pages.join(',') || 'EMPTY');
record('upload: `grid-card` resolves to a real placement', f.placement === 'after-position-card', f.placement || 'EMPTY');
record('upload: source hydrates', f.source === 'position.activity', f.source || 'EMPTY');
record('upload: an undeclared type is inferred from the source', f.type === 'flow', f.type || 'EMPTY');
record('upload: the definition validates as written', reg.validateSkillSource(COMPLETE) === null,
reg.validateSkillSource(COMPLETE) || 'valid');
record('upload: it is classified as a Board skill',
fields.facetsFromSource(COMPLETE).join(',') === 'ui', fields.facetsFromSource(COMPLETE).join(','));
/* The same file read by the other editor's reader — one pipeline, two views. */
const o = fields.owliverFieldsFromSource(COMPLETE);
record('upload: the Owliver reader hydrates the same identity',
o.id === f.id && o.name === f.name && o.description === f.description);
}
/* Case 2 — no `id:`, so it is slugged from the name and never invented. */
{
const f = fields.boardFieldsFromSource(NO_ID);
record('upload: a missing id slugs the name', f.id === 'hiring-activity-assistant', f.id || 'EMPTY');
record('upload: a missing id does not become the placeholder path', f.id !== 'custom');
record('upload: an explicit id is never replaced by a generated one',
fields.boardFieldsFromSource(COMPLETE).id === 'hiring-activity-assistant');
record('upload: a file with no frontmatter hydrates nothing',
fields.boardFieldsFromSource('# Just a heading\n').id === '');
}
/* Case 3 — several pages, and every entry survives a field edit. */
{
const f = fields.boardFieldsFromSource(MULTI_PAGE);
record('upload: every page in the list is reported', f.pages.join(',') === 'positions,analytics', f.pages.join(','));
const parsed = reg.parseSkill(MULTI_PAGE, { custom: true });
record('upload: every entry becomes a section',
parsed.ui.positions.sections.length === 1 && parsed.ui.analytics.sections.length === 1);
record('upload: each entry keeps its own source',
parsed.ui.positions.sections[0].source === 'candidates.activity'
&& parsed.ui.analytics.sections[0].source === 'hires.performance');
record('upload: a multi-page definition validates', reg.validateSkillSource(MULTI_PAGE) === null,
reg.validateSkillSource(MULTI_PAGE) || 'valid');
/* The fields must not be able to flatten it. */
record('upload: the section fields are read-only against a list', !fields.uiIsEditableFromFields(MULTI_PAGE));
record('upload: a single-section definition stays field-editable',
fields.uiIsEditableFromFields(templates.uiSkillTemplate({ id: 'x', name: 'X', pages: ['positions'] })));
/* Renaming is identity, not structure: it must still work, and must not
touch either entry. */
const renamedMulti = fields.patchFrontmatter(MULTI_PAGE, { name: 'Renamed Multi' });
const after = reg.parseSkill(renamedMulti, { custom: true });
record('upload: renaming a multi-page definition keeps both entries',
after.ui.positions?.sections.length === 1 && after.ui.analytics?.sections.length === 1,
Object.keys(after.ui).join(','));
record('upload: ...and actually renames it', after.name === 'Renamed Multi', after.name);
record('upload: ...and leaves the `ui:` text byte-identical',
renamedMulti.slice(renamedMulti.indexOf('ui:')) === MULTI_PAGE.slice(MULTI_PAGE.indexOf('ui:')));
}
/* Files as they actually arrive: from Windows, from a download, from paste. */
{
const dirty = {
'a byte-order mark': `${COMPLETE}`,
'CRLF line endings': COMPLETE.replace(/\n/g, '\r\n'),
'a BOM and CRLF': `${COMPLETE.replace(/\n/g, '\r\n')}`,
'a blank line above the fence': `\n\n${COMPLETE}`,
'trailing spaces on the fence': COMPLETE.replace(/^---$/gm, '--- '),
};
for (const [what, md] of Object.entries(dirty)) {
const f = fields.boardFieldsFromSource(md);
record(
`upload: a file with ${what} still hydrates`,
f.name === 'Hiring Activity Assistant' && f.id === 'hiring-activity-assistant'
&& f.pages.join(',') === 'positions',
`${f.name || 'EMPTY'} / ${f.id || 'EMPTY'} / ${f.pages.join(',') || 'EMPTY'}`
);
record(
`upload: ...and the registry reads it the same way`,
reg.parseSkill(md, { custom: true }).name === 'Hiring Activity Assistant',
reg.parseSkill(md, { custom: true }).name
);
record(`upload: ...and it validates`, reg.validateSkillSource(md) === null,
reg.validateSkillSource(md) || 'valid');
}
/* Normalising on the way in is what keeps `patchFrontmatter` safe: an
unrecognised fence would have it write a second one above the first. */
const patchedDirty = fields.patchFrontmatter(fields.normalizeUpload(`${COMPLETE}`), { name: 'Clean' });
record('upload: patching a normalised file writes one frontmatter block',
(patchedDirty.match(/^---$/gm) || []).length === 2,
`${(patchedDirty.match(/^---$/gm) || []).length} fences`);
record('upload: ...and it still parses', reg.parseSkill(patchedDirty, { custom: true }).name === 'Clean');
/* A file with nothing to read must be refused, not read as a blank skill. */
record('upload: a file with no fence is not readable', !fields.isReadableDefinition('# Just prose\n'));
record('upload: a real definition is readable', fields.isReadableDefinition(COMPLETE));
}
/* Page names and placements as they are written in the product, not as the
vocabulary spells them internally. */
record('upload: `Positions` resolves to the positions surface', surfaces.canonicalPage('Positions') === 'positions');
record('upload: `Talent Pool` resolves to the talent-pool surface', surfaces.canonicalPage('Talent Pool') === 'talent-pool');
record('upload: `panel` on Analytics resolves to a real placement',
surfaces.placementFor('analytics', 'panel') === 'after-header');
record('upload: an alias never resolves onto a surface that lacks it',
surfaces.placementFor('analytics', 'grid-card') === null);
record('upload: a canonical placement still resolves to itself',
surfaces.placementFor('positions', 'after-position-card') === 'after-position-card');
/* Case 4 — an existing definition reopened for editing. */
for (const skill of reg.SKILLS) {
const f = skill.facets?.includes('ui')
? fields.boardFieldsFromSource(skill.markdown)
: fields.owliverFieldsFromSource(skill.markdown);
record(
`reopening \`${skill.id}\` loads its identity unchanged`,
f.id === skill.id && f.name === skill.name && f.description === skill.description
&& f.pages.join(',') === skill.pages.join(','),
`${f.id} / ${f.name} / ${f.pages.join(', ')}`
);
}
/* Case 5 — a manual edit is not reverted by later synchronisation. */
{
/* The editor's own handler, in miniature: hydrate from the file, edit one
field, then edit an unrelated one. The first edit must survive the second. */
let draft = fields.owliverFieldsFromSource(COMPLETE);
let src = COMPLETE;
const edit = (patch) => {
draft = { ...draft, ...patch };
src = fields.patchFrontmatter(src, {
id: draft.id || undefined,
name: draft.name || undefined,
description: draft.description || undefined,
pages: draft.pages?.length && !fields.pagesAreDerived(src) ? draft.pages : undefined,
});
};
edit({ name: 'My Own Name' });
record('edit: a manual name reaches the artefact',
reg.parseSkill(src, { custom: true }).name === 'My Own Name');
edit({ description: 'My own description.' });
record('edit: a later edit does not revert the earlier one',
reg.parseSkill(src, { custom: true }).name === 'My Own Name',
reg.parseSkill(src, { custom: true }).name);
record('edit: ...and applies itself',
reg.parseSkill(src, { custom: true }).description === 'My own description.');
record('edit: ...and the `ui:` block is untouched throughout',
src.slice(src.indexOf('ui:')) === COMPLETE.slice(COMPLETE.indexOf('ui:')));
/* A second upload replaces the draft outright — it is a new source. */
const rehydrated = fields.owliverFieldsFromSource(MULTI_PAGE);
record('edit: a derived `pages:` is never written back',
!src.includes('\npages:'), src.includes('\npages:') ? 'pages: was inserted' : 'not written');
record('edit: a declared `pages:` still patches normally',
reg.parseSkill(
fields.patchFrontmatter(NO_ID, { pages: ['analytics'] }), { custom: true }
).pages.join(',') === 'analytics');
record('edit: an inherited trigger is not written into the file',
!src.includes('triggers:'), src.includes('triggers:') ? 'triggers: was materialised' : 'not written');
record('edit: a declared trigger is still read into the fields',
fields.owliverFieldsFromSource(
reg.SKILLS.find((x) => x.id === 'hiring-activity-assistant').markdown
).triggers.includes('hiring activity'));
record('edit: a definition with no triggers reads none',
fields.owliverFieldsFromSource(COMPLETE).triggers.length === 0,
JSON.stringify(fields.owliverFieldsFromSource(COMPLETE).triggers));
record('edit: a second upload hydrates from the new file',
rehydrated.id === 'hiring-activity' && rehydrated.name === 'Hiring Activity',
`${rehydrated.id} / ${rehydrated.name}`);
}
/* ── 8. Markdown ↔ manual equivalence ─────────────────────────────────────
The two authoring paths, asserted to produce one artefact.
A form that cannot express what the format can is not a shortcut to it: the
manual Owliver editor carried a single `source` shared by every selected
capability, so a capability chosen before a source was picked composed no
`responses:` block at all — and a definition whose capabilities all drop
registers, shows as active on its page, and contributes no chip. These check
that both doors compose the same definition, and that the one that cannot be
configured is refused rather than saved empty. */
console.log('\n── Markdown ↔ manual equivalence ──');
const dataResolver = await server.ssrLoadModule('/src/lib/skills/dataResolver.js');
const positionsContext = Object.entries(placement.PLACEMENT_ROUTES)
.find(([route]) => reg.pageKeyForRoute(route) === 'positions')?.[1];
/* The canonical model is what `parseSkill` makes of a definition. Comparing the
whole record would compare its Markdown too, which is the one thing the two
paths are allowed to differ on — so the runtime configuration is compared,
which is exactly what every consumer reads. */
const canonical = (skill) => JSON.parse(JSON.stringify({
id: skill.id,
name: skill.name,
description: skill.description,
status: skill.status,
pages: skill.pages,
kind: skill.kind,
triggers: skill.triggers,
ui: skill.ui,
owliver: skill.owliver,
}));
/* TEST A / B — the Board card, written both ways. */
const BOARD_MD = `---
id: board
name: Board
description: Helps Owliver understand, analyze, and act on the current task board.
pages:
- positions
status: active
ui:
type: card
placement: after-position-list-summary
title: Board
source: candidates.activity
periods:
- today
- last-7-days
- previous-month
---
# Board
`;
const boardManual = templates.uiSkillTemplate({
id: 'board',
name: 'Board',
description: 'Helps Owliver understand, analyze, and act on the current task board.',
pages: ['positions'],
type: 'card',
placement: 'after-position-list-summary',
title: 'Board',
source: 'candidates.activity',
periods: ['today', 'last-7-days', 'previous-month'],
});
record('TEST A: Markdown Board saves', reg.validateSkillSource(BOARD_MD) === null,
reg.validateSkillSource(BOARD_MD) || 'accepted');
record('TEST B: manual Board saves', reg.validateSkillSource(boardManual) === null,
reg.validateSkillSource(boardManual) || 'accepted');
const boardFromMd = reg.parseSkill(BOARD_MD, { custom: true });
const boardFromForm = reg.parseSkill(boardManual, { custom: true });
const boardSection = (skill) => Object.values(skill.ui || {}).flatMap((pg) => pg.sections || [])[0];
record('TEST A: one section, above the position list',
Object.values(boardFromMd.ui).flatMap((pg) => pg.sections).length === 1
&& boardSection(boardFromMd).placement === 'after-position-list-summary',
`${boardSection(boardFromMd).type}@${boardSection(boardFromMd).placement}`);
record('TEST A: the Board card contributes no Owliver capability',
boardFromMd.owliver.enabled === false && boardFromMd.owliver.capabilities.length === 0);
record('TEST A: no unresolvable section', reg.unresolvableSections(boardFromMd).length === 0,
reg.unresolvableSections(boardFromMd)[0]?.message || 'none');
record('TEST B: manual Board renders the identical section',
JSON.stringify(boardSection(boardFromForm)) === JSON.stringify(boardSection(boardFromMd)),
JSON.stringify(boardSection(boardFromForm)));
record('TEST B: manual Board keeps its title', boardSection(boardFromForm).title === 'Board',
String(boardSection(boardFromForm).title));
/* TEST C / D — the conversational skill, written both ways. */
const OWLIVER_MD = `---
id: owliver-conversation-test
name: Hiring Activity Test
description: Shows hiring activity from the Positions page.
pages:
- positions
status: active
triggers:
- hiring activity test
owliver:
enabled: true
suggestions:
- Show hiring activity
- Summarize hiring activity
capabilities:
- summary
responses:
summary:
source: candidates.activity
periods:
- today
- last-7-days
- previous-month
---
# Hiring Activity Test
`;
const owliverManual = templates.owliverSkillTemplate({
id: 'owliver-conversation-test',
name: 'Hiring Activity Test',
description: 'Shows hiring activity from the Positions page.',
pages: ['positions'],
suggestions: ['Show hiring activity', 'Summarize hiring activity'],
capabilities: ['summary'],
responses: {
summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] },
},
});
record('TEST C: Markdown Owliver skill saves', reg.validateSkillSource(OWLIVER_MD) === null,
reg.validateSkillSource(OWLIVER_MD) || 'accepted');
record('TEST D: manual Owliver skill saves', reg.validateSkillSource(owliverManual) === null,
reg.validateSkillSource(owliverManual) || 'accepted');
for (const [label, source] of [['TEST C', OWLIVER_MD], ['TEST D', owliverManual]]) {
const custom = [{ path: 'custom/owliver-conversation-test.md', raw: source }];
const skill = reg.parseSkill(source, { custom: true });
record(`${label}: the capability keeps its source`,
skill.owliver.responses.summary?.source === 'candidates.activity',
skill.owliver.responses.summary?.source || 'dropped');
record(`${label}: the capability keeps its periods`,
JSON.stringify(skill.owliver.responses.summary?.periods)
=== JSON.stringify(['today', 'last-7-days', 'previous-month']),
JSON.stringify(skill.owliver.responses.summary?.periods));
record(`${label}: it draws no page card`,
Object.keys(skill.ui).length === 0, `${Object.keys(skill.ui).length} pages`);
const chips = resolver.owliverSuggestions(positionsContext, [], custom, {});
const mine = chips.filter((c) => c.skillId === 'owliver-conversation-test');
record(`${label}: its suggestion reaches the panel`,
mine.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(chips.map((c) => c.label)));
record(`${label}: the suggestion answers without asking for a record`,
mine.every((c) => !c.deferred));
const owlSkills = resolver.owliverSkillsForContext(positionsContext, [], custom);
const matched = resolver.matchOwliverSkill('Show hiring activity', owlSkills);
record(`${label}: clicking it routes to this skill's summary`,
matched?.skill.id === 'owliver-conversation-test' && matched?.capability === 'summary' && matched?.exact,
`${matched?.skill.id}:${matched?.capability} exact=${matched?.exact}`);
const answered = resolver.resolveOwliverResponse({
skill: matched.skill,
capability: matched.capability,
question: 'Show hiring activity',
context: {
positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }],
applications: [{ id: 'a1', job_posting_id: 'p1', created_date: new Date().toISOString() }],
},
now: new Date(),
});
record(`${label}: it answers with the configured reading`,
answered?.missing === null && resolver.summaryLines(answered.data).length === 3,
JSON.stringify(resolver.summaryLines(answered?.data || {})));
}
/* TEST H — the same skill through both doors, normalized, compared. */
record('TEST H: Markdown and manual normalize identically (Board)',
JSON.stringify(canonical(boardFromMd)) === JSON.stringify(canonical(boardFromForm)),
JSON.stringify(canonical(boardFromForm)).slice(0, 120));
record('TEST H: Markdown and manual normalize identically (Owliver)',
JSON.stringify(canonical(reg.parseSkill(OWLIVER_MD, { custom: true })))
=== JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))),
JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))).slice(0, 120));
/* TEST E — a capability with no source is refused, never saved empty. */
const noSourceFields = {
id: 'no-source-test',
name: 'No Source Test',
description: 'A capability with nothing to read.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
capabilities: ['summary'],
responses: {},
};
const noSource = templates.owliverSkillTemplate(noSourceFields);
record('TEST E: an unconfigured capability is named by the form',
JSON.stringify(fields.unconfiguredCapabilities(noSourceFields)) === '["summary"]',
JSON.stringify(fields.unconfiguredCapabilities(noSourceFields)));
record('TEST E: and the save is refused',
/owliver\.responses\.summary/.test(reg.validateSkillSource(noSource) || ''),
reg.validateSkillSource(noSource) || 'ACCEPTED — a skill with no capability would register');
record('TEST E: the refused definition would have had zero capabilities',
reg.parseSkill(noSource, { custom: true }).owliver.capabilities.length === 0,
'which is why it is refused rather than stored');
/* TEST F — two capabilities, two different sources, neither inheriting. */
const twoSources = templates.owliverSkillTemplate({
id: 'two-source-test',
name: 'Two Source Test',
description: 'Two capabilities reading two sources.',
pages: ['positions'],
suggestions: ['Summarize workspace applications', 'List the open roles'],
capabilities: ['summary', 'list'],
responses: {
summary: { source: 'candidates.activity', periods: ['today'] },
list: { source: 'positions.demand', limit: 5 },
},
});
const twoSkill = reg.parseSkill(twoSources, { custom: true });
record('TEST F: manual save accepted', reg.validateSkillSource(twoSources) === null,
reg.validateSkillSource(twoSources) || 'accepted');
record('TEST F: each capability keeps its own source',
twoSkill.owliver.responses.summary?.source === 'candidates.activity'
&& twoSkill.owliver.responses.list?.source === 'positions.demand',
`summary→${twoSkill.owliver.responses.summary?.source}, list→${twoSkill.owliver.responses.list?.source}`);
record('TEST F: neither inherits the other\'s options',
JSON.stringify(twoSkill.owliver.responses.summary.periods) === '["today"]'
&& twoSkill.owliver.responses.list.limit === 5
&& twoSkill.owliver.responses.list.periods.length === 0,
`summary periods=${JSON.stringify(twoSkill.owliver.responses.summary.periods)}, list limit=${twoSkill.owliver.responses.list.limit}`);
record('TEST F: both are offered as suggestions',
resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/two.md', raw: twoSources }], {})
.filter((c) => c.skillId === 'two-source-test').length === 2);
/* TEST G — editing one field leaves every other configuration alone. */
const edited = fields.patchFrontmatter(
twoSources,
fields.owliverPatch(
{ ...fields.owliverFieldsFromSource(twoSources), description: 'A new description.' },
{ existing: twoSources }
)
);
const editedSkill = reg.parseSkill(edited, { custom: true });
record('TEST G: the edit lands', editedSkill.description === 'A new description.', editedSkill.description);
record('TEST G: every capability source survives the edit',
JSON.stringify(canonical(editedSkill).owliver.responses)
=== JSON.stringify(canonical(twoSkill).owliver.responses),
`summary→${editedSkill.owliver.responses.summary?.source}, list→${editedSkill.owliver.responses.list?.source}`);
record('TEST G: suggestions survive the edit',
JSON.stringify(editedSkill.owliver.suggestions) === JSON.stringify(twoSkill.owliver.suggestions));
const boardEdited = fields.patchFrontmatter(
boardManual,
fields.boardPatch(
{ ...fields.boardFieldsFromSource(boardManual), description: 'A new description.' },
{ existing: boardManual }
)
);
record('TEST G: Board UI configuration survives the edit',
JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true })))
=== JSON.stringify(boardSection(boardFromForm)),
JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true }))));
/* TEST 6 — one source/shape resolver, shared by the pickers and the validator. */
record('shape compatibility: the resolver refuses what the normalizer refuses',
surfaces.sourcesForShape('list').every((src) => surfaces.sourceSupportsShape(src.id, 'list'))
&& !surfaces.sourcesForShape('list').some((src) => src.id === 'candidates.activity'),
`list sources: ${surfaces.sourcesForShape('list').map((s) => s.id).join(', ')}`);
record('shape compatibility: a list against candidates.activity is refused on save',
/cannot be shown as/.test(reg.validateSkillSource(templates.owliverSkillTemplate({
id: 'bad-shape-test',
name: 'Bad Shape Test',
description: 'A list of something with no list in it.',
pages: ['positions'],
capabilities: ['list'],
responses: { list: { source: 'candidates.activity' } },
})) || ''),
'the picker cannot offer it, and the save refuses it');
record('summary is prose, so every source suits it',
surfaces.shapeForCapability('summary') === null
&& surfaces.sourcesForShape(null).length === surfaces.DATA_SOURCES.length);
/* ── 9. The reported failure, reproduced end to end ───────────────────────── */
console.log('\n── Regression: manual Owliver skill on Positions ──');
/* Exactly the configuration from the report: Pages = Positions, one suggestion,
Summary, `candidates.activity`, three periods — composed the way the form
composes it, then read the way the panel reads it. */
const REGRESSION = templates.owliverSkillTemplate({
id: 'owliver-conversation-test',
name: 'Hiring Activity Test',
description: 'Shows hiring activity from the Positions page.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
capabilities: ['summary'],
responses: {
summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] },
},
});
const stored = [{ path: 'custom/owliver-conversation-test.md', raw: REGRESSION }];
record('the manual definition saves', reg.validateSkillSource(REGRESSION) === null,
reg.validateSkillSource(REGRESSION) || 'accepted');
record('it registers', reg.allSkills(stored).some((s) => s.id === 'owliver-conversation-test'));
record('it is attached to Positions',
reg.skillsForContext(positionsContext, [], stored).some((s) => s.id === 'owliver-conversation-test'));
record('it keeps a capability — the step that used to empty',
reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities.length === 1,
JSON.stringify(reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities));
record('it reaches owliverSkillsForContext',
resolver.owliverSkillsForContext(positionsContext, [], stored)
.some((s) => s.id === 'owliver-conversation-test'));
const regressionChips = resolver.owliverSuggestions(positionsContext, [], stored, {});
record('"Show hiring activity" is offered as a chip',
regressionChips.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(regressionChips.map((c) => c.label)));
const clicked = resolver.matchOwliverSkill(
'Show hiring activity',
resolver.owliverSkillsForContext(positionsContext, [], stored)
);
record('clicking it invokes this skill\'s summary',
clicked?.skill.id === 'owliver-conversation-test' && clicked?.capability === 'summary',
`${clicked?.skill.id}:${clicked?.capability}`);
const now = new Date();
const answer = resolver.resolveOwliverResponse({
skill: clicked.skill,
capability: clicked.capability,
question: 'Show hiring activity',
context: {
positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }],
applications: [
{ id: 'a1', job_posting_id: 'p1', created_date: now.toISOString() },
{ id: 'a2', job_posting_id: 'p1', created_date: new Date(now - 3 * 86400000).toISOString() },
],
},
now,
});
record('it answers with the configured source and periods',
answer?.missing === null && answer.section.source === 'candidates.activity'
&& answer.section.periods.length === 3,
`${answer?.section.source} over ${JSON.stringify(answer?.section.periods)}`);
record('the answer carries real figures',
resolver.summaryLines(answer.data).length === 3,
JSON.stringify(resolver.summaryLines(answer.data)));
record('and no `position.activity needs a position` is possible',
dataResolver.resolveSkillData(answer.section, { applications: [], positions: [] }, now).unavailable !== true,
'the source declares no context requirement');
/* Reload: what is stored is the Markdown, so the round trip through storage is
the round trip through the parser. */
const reloaded = reg.parseSkill(
fields.patchFrontmatter(REGRESSION, {}),
{ custom: true }
);
record('the source survives a reload',
reloaded.owliver.responses.summary?.source === 'candidates.activity',
reloaded.owliver.responses.summary?.source || 'lost');
record('the form reads it back unchanged',
JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses)
=== JSON.stringify({ summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'], limit: null } }),
JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses));
/* ── 10. Storage round trip ───────────────────────────────────────────────
The editors do not hand the registry a record; they write Markdown into
`preferences.customSkills`, which `base44Client` persists to localStorage and
reads back on the next load. "The source disappeared after reload" is a claim
about *that* path, so it is exercised here rather than assumed: the same
module the app runs, against a localStorage the same shape the browser's is. */
console.log('\n── Storage round trip ──');
const store = new Map();
globalThis.localStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
const { base44 } = await server.ssrLoadModule('/src/api/base44Client.js');
const custom = await server.ssrLoadModule('/src/lib/skills/customSkills.js');
/* Saving is what the editor's Save button does: compose, validate, upsert,
write the preference. */
const saved = custom.upsertCustomSkill(base44.auth.preferences().customSkills || [], REGRESSION);
const written = await base44.auth.updatePreferences({ customSkills: saved.next });
record('the definition is persisted', written.persisted, written.error?.message || 'written to storage');
/* Reload: a fresh read of the same key, exactly as a page load does. */
const rehydratedSkills = base44.auth.preferences().customSkills || [];
record('it survives the reload', rehydratedSkills.length === 1
&& String(rehydratedSkills[0].raw).includes('candidates.activity'),
`${rehydratedSkills.length} stored`);
const afterReload = reg.parseSkill(rehydratedSkills[0].raw, { custom: true });
record('its capability survives the reload',
afterReload.owliver.responses.summary?.source === 'candidates.activity',
afterReload.owliver.responses.summary?.source || 'lost');
record('and it still offers its chip after the reload',
resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {})
.some((c) => c.label === 'Show hiring activity'),
JSON.stringify(resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {}).map((c) => c.label)));
record('the editor reopens it with its source intact',
fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses.summary?.source === 'candidates.activity',
JSON.stringify(fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses));
/* The Board skill through the same path, stored beside it — the two subsystems
are independent, so both must survive one write. */
const bothStored = custom.upsertCustomSkill(rehydratedSkills, boardManual).next;
await base44.auth.updatePreferences({ customSkills: bothStored });
const finalStored = base44.auth.preferences().customSkills || [];
const finalRegistry = reg.readSkillRegistry(finalStored);
record('Board and Owliver skills coexist in storage', finalStored.length === 2,
`${finalStored.length} stored`);
record('neither produces a registry diagnostic',
!finalRegistry.diagnostics.some((d) => d.level === 'error'),
finalRegistry.diagnostics.filter((d) => d.level === 'error').map((d) => d.message).join(' | ') || 'clean');
record('the Board card draws once on Positions, and offers no chip',
reg.getSkillsForPage('positions', { customSources: finalStored })
.filter((sk) => sk.id === 'board')
.every((sk) => Object.values(sk.ui).flatMap((pg) => pg.sections).length === 1
&& sk.owliver.capabilities.length === 0));
record('the Owliver skill offers its chip, and draws no card',
resolver.owliverSuggestions(positionsContext, [], finalStored, {})
.some((c) => c.skillId === 'owliver-conversation-test')
&& Object.keys(reg.parseSkill(REGRESSION, { custom: true }).ui).length === 0);
/* ── 11. A fresh Add Owliver Skill screen ─────────────────────────────────
What the form starts as, asserted at the state that drives the checkbox
rather than at the checkbox. `checked` is
`draft.capabilities.includes(capability.id)` and the per-capability
configuration is rendered under `{checked && …}`, so the array below is the
whole of "what is ticked" and "what is shown". */
console.log('\n── Fresh Add Owliver Skill ──');
const CAPS = surfaces.OWLIVER_CAPABILITIES.map((c) => c.id);
/* 1. NEW skill. `emptyDraft` is `EMPTY_OWLIVER_FIELDS`, which is what the
editor initializes to when there is no `:id` and nothing handed over. */
record('new skill: initial capabilities are []',
Array.isArray(fields.EMPTY_OWLIVER_FIELDS.capabilities)
&& fields.EMPTY_OWLIVER_FIELDS.capabilities.length === 0,
JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.capabilities));
record('new skill: no response is pre-configured either',
Object.keys(fields.EMPTY_OWLIVER_FIELDS.responses).length === 0,
JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.responses));
record(`new skill: all ${CAPS.length} capability checkboxes start unchecked`,
CAPS.every((id) => !fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)),
CAPS.filter((id) => fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)).join(', ') || 'none ticked');
/* The template the fresh screen composes must not declare capabilities either:
an author who types a name before ticking anything patches *that* file, and a
`capabilities:` key in it would tick boxes nobody chose. */
const freshTemplate = templates.owliverSkillTemplate({});
record('new skill: the composed template declares no capabilities',
!/^\s*capabilities:/m.test(freshTemplate),
freshTemplate.split('\n').filter((l) => /capabilit/i.test(l)).join(' / ') || 'no capabilities key');
record('new skill: reading that template back still yields []',
fields.owliverFieldsFromSource(freshTemplate).capabilities.length === 0,
JSON.stringify(fields.owliverFieldsFromSource(freshTemplate).capabilities));
/* 2. One capability, chosen. The editor's `update({ capabilities })` is a plain
state merge, so the assertion is on what that array then means. */
const pickedSummary = { ...fields.EMPTY_OWLIVER_FIELDS, capabilities: ['summary'] };
record('selecting Summary ticks exactly Summary',
JSON.stringify(pickedSummary.capabilities) === '["summary"]');
record('and only Summary renders a configuration block',
CAPS.filter((id) => pickedSummary.capabilities.includes(id)).length === 1,
CAPS.filter((id) => pickedSummary.capabilities.includes(id)).join(', '));
record('an unsourced Summary is invalid, and only Summary is reported',
JSON.stringify(fields.unconfiguredCapabilities(pickedSummary)) === '["summary"]',
JSON.stringify(fields.unconfiguredCapabilities(pickedSummary)));
/* 3. Choosing its source clears the error, and the definition saves. */
const sourcedSummary = {
...pickedSummary,
id: 'fresh-summary-test',
name: 'Fresh Summary Test',
description: 'One capability, configured.',
pages: ['positions'],
suggestions: ['Show hiring activity'],
responses: { summary: { source: 'candidates.activity', periods: ['today'] } },
};
record('choosing a source clears the validation error',
fields.unconfiguredCapabilities(sourcedSummary).length === 0,
JSON.stringify(fields.unconfiguredCapabilities(sourcedSummary)));
const freshSaved = templates.owliverSkillTemplate(sourcedSummary);
record('and the save succeeds', reg.validateSkillSource(freshSaved) === null,
reg.validateSkillSource(freshSaved) || 'accepted');
record('the saved definition declares only the chosen capability',
JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities) === '["summary"]',
JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities));
record('and its suggestion reaches the Positions panel',
resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/fresh.md', raw: freshSaved }], {})
.some((c) => c.skillId === 'fresh-summary-test'));
/* 4. Several, chosen by hand — only those, each validated on its own. */
const pickedTwo = {
...fields.EMPTY_OWLIVER_FIELDS,
capabilities: ['summary', 'list'],
responses: { summary: { source: 'candidates.activity' } },
};
record('selecting two capabilities renders exactly those two',
CAPS.filter((id) => pickedTwo.capabilities.includes(id)).join(',') === 'summary,list');
record('and only the unconfigured one is reported',
JSON.stringify(fields.unconfiguredCapabilities(pickedTwo)) === '["list"]',
JSON.stringify(fields.unconfiguredCapabilities(pickedTwo)));
/* 5. An EXISTING skill hydrates from its own definition, and from nothing else.
Including the capability that did *not* resolve: it is the one the author
opened the editor to fix, and dropping it from the form would delete it
from the file on the next save. */
const DECLARED_ONE = `---
id: declared-one
name: Declared One
description: Declares a single capability.
pages:
- positions
status: active
owliver:
enabled: true
suggestions:
- Show hiring activity
capabilities:
- summary
responses:
summary:
source: candidates.activity
---
# Declared One
`;
record('existing skill: hydrates exactly its declared capability',
JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities) === '["summary"]',
JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities));
record('existing skill: no new-skill default is applied on top',
CAPS.filter((id) => fields.owliverFieldsFromSource(DECLARED_ONE).capabilities.includes(id)).length === 1);
const HALF_BROKEN = DECLARED_ONE
.replace(' - summary\n', ' - summary\n - list\n')
.replace('id: declared-one', 'id: half-broken');
const halfFields = fields.owliverFieldsFromSource(HALF_BROKEN);
record('existing skill: a declared capability that lost its source still shows',
JSON.stringify(halfFields.capabilities) === '["summary","list"]',
JSON.stringify(halfFields.capabilities));
record('...unsourced, so it stays invalid rather than silently dropped',
JSON.stringify(fields.unconfiguredCapabilities(halfFields)) === '["list"]',
JSON.stringify(fields.unconfiguredCapabilities(halfFields)));
record('...and the configured one keeps its source through the round trip',
halfFields.responses.summary.source === 'candidates.activity',
halfFields.responses.summary.source);
record('...while the runtime still registers only what can answer',
JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities) === '["summary"]',
JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities));
/* A definition inheriting its reading from a `ui:` section still hydrates the
capability it actually offers — the union is declaration-first, not
declaration-only. */
const INHERITED = `---
id: inherited-cap
name: Inherited Cap
description: A capability reading the page section.
pages:
- positions
status: active
ui:
type: card
placement: after-position-list-summary
source: candidates.activity
owliver:
enabled: true
capabilities:
- card
---
# Inherited Cap
`;
record('a capability inheriting from `ui:` still hydrates with its source',
fields.owliverFieldsFromSource(INHERITED).responses.card?.source === 'candidates.activity',
fields.owliverFieldsFromSource(INHERITED).responses.card?.source || 'lost');
await server.close();
/* ── 8. Production bundle ─────────────────────────────────────────────────── */
if (process.argv.includes('--dist')) {
console.log('\n── Production bundle ──');
const dir = join(ROOT, 'dist/assets');
if (!existsSync(dir)) {
record('dist/assets exists', false, 'run `npm run build` first');
} else {
const bundle = readdirSync(dir)
.filter((f) => f.endsWith('.js'))
.map((f) => readFileSync(join(dir, f), 'utf8'))
.join('\n');
for (const s of reg.SKILLS) {
record(`bundle carries \`${s.id}\``, bundle.includes(s.id));
}
/* Body content, not just the id — a manifest naming a skill whose Markdown
did not survive is the failure this is looking for. */
record(
'bundle carries skill body text',
bundle.includes('Which client is this role for'),
'create-position conversation step found in bundle'
);
}
}
/* ── Summary ──────────────────────────────────────────────────────────────── */
const failed = results.filter((r) => !r.pass);
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
if (failed.length) {
console.log('\nFailed:');
failed.forEach((r) => console.log(` - ${r.name}${r.detail ? ` (${r.detail})` : ''}`));
process.exit(1);
}