This commit is contained in:
@@ -3840,6 +3840,192 @@ record('every published agent offers at least one starter where it applies',
|
||||
.every((a) => a.pages.length === 0 || runtime.agentStarters(a, null).length > 0),
|
||||
ALL_AGENTS.map((a) => `${a.id}:${a.starters.length}`).join(' '));
|
||||
|
||||
/* ── Removing an agent ───────────────────────────────────────────────────
|
||||
*
|
||||
* Removal is permanent, and the two things that make it safe are both
|
||||
* structural rather than visible: WHICH agents offer it, and that archiving
|
||||
* never quietly becomes it.
|
||||
*
|
||||
* The menu is duplicated between the card view and the table row — two blocks
|
||||
* of the same JSX — so every property here is asserted against BOTH. A guard
|
||||
* added to one and forgotten in the other is exactly the shape of bug that
|
||||
* would ship a delete button on a built-in agent.
|
||||
*
|
||||
* `shipped` is the source of truth, from the ids this build bundles. Nothing
|
||||
* here reads a display name: two agents may legitimately share one.
|
||||
*/
|
||||
const removeItems = agentsListSource.match(
|
||||
/onAction\('remove', agent\)[\s\S]{0,400}?<\/DropdownMenuItem>/g) || [];
|
||||
record('both agent menus offer removal, and neither offers it unconditionally',
|
||||
removeItems.length === 2
|
||||
&& (agentsListSource.match(/\{\(overridden \|\| !shipped\) && \(/g) || []).length === 2,
|
||||
`${removeItems.length} remove item(s), each behind (overridden || !shipped)`);
|
||||
|
||||
record('a custom agent is offered Remove, and a built-in one is only offered a revert',
|
||||
removeItems.every((block) => / Remove</.test(block) && /Revert to shipped/.test(block)
|
||||
&& /shipped\s*\?/.test(block)),
|
||||
'the label is chosen by `shipped`, not by name');
|
||||
|
||||
/* Archive is reversible; Remove is not. The menu must not make removal depend
|
||||
on status, or an archived custom agent becomes unremovable — which would
|
||||
silently turn Archive into the only exit and leave the row forever. */
|
||||
record('removal does not depend on an agent status, so an archived custom agent stays removable',
|
||||
removeItems.every((block) => !/status/.test(block)),
|
||||
'the remove item reads `shipped`/`overridden` only');
|
||||
|
||||
/* Removal is REVERSIBLE, and the confirmation is where a reader learns that.
|
||||
Copy that promised permanence would be a false statement about what the
|
||||
button does — the agent is archived, not destroyed — so the wording is
|
||||
asserted, including the absence of the two phrases that would be lies. */
|
||||
record('removing a custom agent confirms first, in words that say it is reversible',
|
||||
/'Remove agent\?'/.test(agentsListSource)
|
||||
&& /This agent will be removed from the active Agents list\./.test(agentsListSource)
|
||||
&& /can be restored later from Archived/.test(agentsListSource)
|
||||
&& /'Remove Agent'/.test(agentsListSource),
|
||||
'title, description and confirm label');
|
||||
|
||||
record('...and the confirmation never claims the removal is permanent',
|
||||
!/cannot be undone/i.test(agentsListSource)
|
||||
&& !/permanently (remove|delete)/i.test(agentsListSource),
|
||||
'no irreversibility language anywhere on the screen');
|
||||
|
||||
/* The substance behind that copy: Remove on an authored agent runs ARCHIVE.
|
||||
Only a shipped agent's own override is deleted, and deleting an override is
|
||||
how "Revert to shipped" restores the product's definition — it destroys no
|
||||
agent. If this resolution is ever removed, Remove becomes a hard delete
|
||||
again and every promise above becomes false. */
|
||||
record('Remove archives an authored agent rather than deleting it',
|
||||
/const resolved = action === 'remove' && !isShipped\(agent\.id\) \? 'archive' : action;/
|
||||
.test(agentsListSource),
|
||||
'run() resolves remove -> archive for anything not shipped');
|
||||
|
||||
record('...and the default list stops showing what has been removed',
|
||||
/useState\('active'\)/.test(agentsListSource)
|
||||
&& /if \(status === 'active'\) return found\.filter\(\(a\) => a\.status !== 'archived'\);/
|
||||
.test(agentsListSource),
|
||||
'the default filter is active, which excludes archived');
|
||||
|
||||
record('...while Archived stays reachable as the place removal is undone',
|
||||
/setStatus\('archived'\)/.test(agentsListSource)
|
||||
&& /onAction\('restore', agent\)/.test(agentsListSource),
|
||||
'the Archived tab and Restore as draft both survive');
|
||||
|
||||
/* The tab must be there BEFORE anything is in it. It was rendered behind
|
||||
`archivedCount > 0`, so the one screen that says where a removed agent went
|
||||
only appeared once a reader had already removed one and gone looking — and
|
||||
the confirmation names "Archived" while no such tab is on screen. */
|
||||
record('...and Archived is offered even when it is empty',
|
||||
!/\{archivedCount > 0 && \(/.test(agentsListSource)
|
||||
&& /Archived <span[^>]*>\(\{archivedCount\}\)/.test(agentsListSource),
|
||||
'the tab is unconditional and shows its count');
|
||||
|
||||
record('...and reverting a built-in agent is not worded as a removal',
|
||||
/'Revert to the shipped definition\?'/.test(agentsListSource)
|
||||
&& /isShipped\(confirming\?\.agent\?\.id\) \? 'Confirm' : 'Remove Agent'/.test(agentsListSource),
|
||||
'the shipped path keeps its own copy');
|
||||
|
||||
record('a removal that fails is reported rather than swallowed',
|
||||
/if \(result\?\.error\) toast\.error\(result\.error\)/.test(agentsListSource),
|
||||
'run() surfaces the error the mutation returned');
|
||||
|
||||
/* The gate itself, exercised rather than read: every id this build ships is
|
||||
`shipped`, so none of them reaches the branch that renders Remove. This is
|
||||
the same set `useAgents().isShipped` tests against — `AGENTS`, bundled from
|
||||
the agent definitions under src/agents — so the check moves with the
|
||||
product. */
|
||||
const shippedAgentIds = new Set(ALL_AGENTS.map((a) => a.id));
|
||||
record('every agent this build ships is classified as built-in',
|
||||
ALL_AGENTS.every((a) => shippedAgentIds.has(a.id)),
|
||||
`${shippedAgentIds.size} shipped id(s)`);
|
||||
record('...and an authored agent is not, whatever its status',
|
||||
['draft', 'published', 'archived'].every((status) => {
|
||||
const id = `authored-${status}-agent`;
|
||||
return !shippedAgentIds.has(id);
|
||||
}),
|
||||
'draft, published and archived authored ids are all removable');
|
||||
|
||||
/* ── Remove and restore, exercised end to end ────────────────────────────
|
||||
*
|
||||
* The claim the confirmation makes — "its configuration and skills will be
|
||||
* preserved and it can be rmRestored later" — is a claim about these two
|
||||
* functions, so it is tested against them rather than against the sentence.
|
||||
*
|
||||
* Both are pure over Markdown and the Markdown is the authoritative artefact,
|
||||
* so "nothing was lost" is checkable exactly: the source may differ in the
|
||||
* status line and in nothing else, and the parsed agent may differ in `status`
|
||||
* and in nothing else. A patch that dropped a tool, renumbered a version or
|
||||
* rewrote an id would fail here rather than in somebody's account.
|
||||
*/
|
||||
const rmLifecycle = await server.ssrLoadModule('/src/lib/agents/agentLifecycle.js');
|
||||
|
||||
const rmAuthored = [
|
||||
'---',
|
||||
'id: coverage-helper',
|
||||
'name: Coverage Helper',
|
||||
'description: Finds and offers cover for open shifts.',
|
||||
'status: published',
|
||||
'version: 3',
|
||||
'pages:',
|
||||
' - control-center',
|
||||
' - positions',
|
||||
'tools:',
|
||||
' - workforce.coverage',
|
||||
'skills:',
|
||||
' - create-position',
|
||||
'---',
|
||||
'',
|
||||
'## Instructions',
|
||||
'Find the shifts nobody has taken and say who could take them.',
|
||||
].join('\n');
|
||||
|
||||
const rmStatusless = (src) => src.replace(/^status:.*$/m, 'status: <redacted>');
|
||||
/* `markdown` is excluded because it IS the source, status line and all — the
|
||||
line above already asserts that source byte-for-byte, so comparing it again
|
||||
here would only re-report the one difference both checks expect. Every other
|
||||
parsed field, including tools, skills, pages, instructions and version, is
|
||||
compared. */
|
||||
const rmWithoutStatus = (agent) => {
|
||||
const { status, markdown, ...rest } = agent;
|
||||
return JSON.stringify(rest);
|
||||
};
|
||||
|
||||
const rmRemoved = rmLifecycle.archiveAgent(rmAuthored);
|
||||
const rmRestored = rmLifecycle.restoreAgent(rmRemoved);
|
||||
|
||||
const rmBefore = agentReg.parseAgent(rmAuthored, { custom: true });
|
||||
const rmAfterRemove = agentReg.parseAgent(rmRemoved, { custom: true });
|
||||
const rmAfterRestore = agentReg.parseAgent(rmRestored, { custom: true });
|
||||
|
||||
record('remove: an authored agent becomes archived rather than disappearing',
|
||||
rmAfterRemove.status === 'archived',
|
||||
`status ${rmBefore.status} -> ${rmAfterRemove.status}`);
|
||||
|
||||
record('remove: the definition keeps its identity',
|
||||
rmAfterRemove.id === rmBefore.id && rmAfterRemove.id === 'coverage-helper',
|
||||
rmAfterRemove.id);
|
||||
|
||||
record('remove: instructions, tools, skills, pages and version all survive',
|
||||
rmStatusless(rmRemoved) === rmStatusless(rmAuthored)
|
||||
&& rmWithoutStatus(rmAfterRemove) === rmWithoutStatus(rmBefore),
|
||||
'the source differs in the status line and nothing else');
|
||||
|
||||
record('restore: the SAME agent comes back, as a draft',
|
||||
rmAfterRestore.id === rmBefore.id && rmAfterRestore.status === 'draft',
|
||||
`${rmAfterRestore.id} @ ${rmAfterRestore.status}`);
|
||||
|
||||
record('restore: nothing was lost on the round trip',
|
||||
rmStatusless(rmRestored) === rmStatusless(rmAuthored)
|
||||
&& rmWithoutStatus(rmAfterRestore) === rmWithoutStatus(rmBefore),
|
||||
'archive -> restore is lossless apart from status');
|
||||
|
||||
/* Removing something already archived must not escalate into anything else:
|
||||
it is still that same archived definition afterwards. */
|
||||
const rmRemovedTwice = rmLifecycle.archiveAgent(rmRemoved);
|
||||
record('remove: removing an already-archived agent leaves it archived and whole',
|
||||
agentReg.parseAgent(rmRemovedTwice, { custom: true }).status === 'archived'
|
||||
&& rmStatusless(rmRemovedTwice) === rmStatusless(rmAuthored),
|
||||
'idempotent, and still non-destructive');
|
||||
|
||||
/* ── 18. Conversation records and insights ───────────────────────────────
|
||||
*
|
||||
* Conversations gained fields: which agent answered, where, what it used, and
|
||||
@@ -8774,6 +8960,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
const skillE = await server.ssrLoadModule('/src/lib/ui/skillNodes.js');
|
||||
const uiCfgE = await server.ssrLoadModule('/src/lib/skills/uiConfig.js');
|
||||
const editorOps = await server.ssrLoadModule('/src/components/ui-editor/ops.js');
|
||||
const validateE = await server.ssrLoadModule('/src/lib/ui/validate.js');
|
||||
await server.ssrLoadModule('/src/components/ui-tree/nodeTypes.jsx');
|
||||
await server.ssrLoadModule('/src/pages/admin/hired-history/nodes.jsx');
|
||||
await server.ssrLoadModule('/src/pages/admin/positions/nodes.js');
|
||||
@@ -9207,7 +9394,7 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
})().pass);
|
||||
|
||||
record('a failed node renders in place, named, and still addressable', (() => {
|
||||
const boundary = new boundaryE.UiNodeBoundary({ node: { id: 'bad', type: 'chart', props: {} } });
|
||||
const boundary = new boundaryE.UiNodeBoundary({ node: { id: 'bad', type: 'bar-chart', props: {} } });
|
||||
boundary.state = { failed: true, forNode: 'bad' };
|
||||
const html = draw(boundary.render());
|
||||
return {
|
||||
@@ -9552,7 +9739,8 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
})().pass);
|
||||
|
||||
record('replace: an unregistered type is refused', (() => {
|
||||
const r = replaceWith('pie-chart');
|
||||
/* A name nobody registers, and nothing here should ever register. */
|
||||
const r = replaceWith('sculpture');
|
||||
return { pass: !r.ok && /Unsupported UI type/i.test(JSON.stringify(r.problems)),
|
||||
detail: JSON.stringify(r.problems).slice(0, 120) };
|
||||
})().pass);
|
||||
@@ -9612,21 +9800,41 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
})().pass);
|
||||
|
||||
|
||||
/* ── Chart and pie ────────────────────────────────────────────────────
|
||||
/* ── Visualizations ───────────────────────────────────────────────────
|
||||
*
|
||||
* Two node types that are deliberately *not* skill section types, drawing
|
||||
* Five node types that are deliberately *not* skill section types, drawing
|
||||
* the payload a `flow` reading already resolves to. The tests that matter
|
||||
* are the ones about compatibility: nothing may become a chart unless its
|
||||
* reading really carries a series.
|
||||
* are about compatibility, and there are now two kinds: whether a component
|
||||
* *can* draw a reading, and whether doing so would be true.
|
||||
*/
|
||||
|
||||
const VIZ = ['bar-chart', 'line-chart', 'area-chart', 'pie-chart', 'donut-chart'];
|
||||
|
||||
/* What a reading means, asked the same way every consumer asks it. */
|
||||
const seriesMod = await server.ssrLoadModule('/src/lib/ui/series.js');
|
||||
|
||||
const seriesNode = (type, source, extra = {}) => nodeE.makeNode({
|
||||
id: 'viz', type, origin: 'user', props: { title: 'Hiring performance' },
|
||||
data: { source }, layout: { spacingBefore: 'lg' },
|
||||
presentation: { variant: 'emphasis', density: 'compact' }, ...extra,
|
||||
});
|
||||
|
||||
for (const type of ['chart', 'pie']) {
|
||||
/**
|
||||
* A reading whose meaning this visualization can honestly draw.
|
||||
*
|
||||
* There is no single source that fits all five, and that is the point: a
|
||||
* line may draw a reading that is ordered, a pie may draw one whose points
|
||||
* are parts of a whole, and no reading is both. Picked from what the source
|
||||
* itself declares rather than assumed.
|
||||
*/
|
||||
const READING_OF_KIND = {
|
||||
periodic: 'candidates.activity',
|
||||
cumulative: 'position.pipeline',
|
||||
parts: 'activity.breakdown',
|
||||
};
|
||||
const readingFor = (type) => READING_OF_KIND[nreg.get(type).seriesKinds[0]];
|
||||
|
||||
for (const type of VIZ) {
|
||||
record(`${type}: is a registered node type with a real component`, (() => {
|
||||
const entry = nreg.get(type);
|
||||
return { pass: Boolean(entry) && typeof entry.component === 'function' && entry.dataRequired,
|
||||
@@ -9639,6 +9847,12 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
detail: entry.dataShapes.join(', ') };
|
||||
})().pass);
|
||||
|
||||
record(`${type}: declares which series meanings it can honestly draw`, (() => {
|
||||
const entry = nreg.get(type);
|
||||
const known = entry.seriesKinds.every((k) => nodeE.SERIES_KINDS.includes(k));
|
||||
return { pass: entry.seriesKinds.length > 0 && known, detail: entry.seriesKinds.join(', ') };
|
||||
})().pass);
|
||||
|
||||
record(`${type}: supports the presentation vocabulary it honours`, (() => {
|
||||
const entry = nreg.get(type);
|
||||
return { pass: entry.variants.length === 3 && entry.densities.length === 2,
|
||||
@@ -9646,15 +9860,28 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
})().pass);
|
||||
|
||||
record(`${type}: a card on a series reading can become one`, (() => {
|
||||
const r = opsE.applyOperation([seriesNode('card', 'hires.performance')],
|
||||
const source = readingFor(type);
|
||||
const r = opsE.applyOperation([seriesNode('card', source)],
|
||||
{ op: 'replace', target: 'viz', type }, ectx);
|
||||
const after = r.ok ? nodeE.findNode(r.tree, 'viz') : null;
|
||||
return { pass: r.ok && after.type === type && after.id === 'viz'
|
||||
&& after.data.source === 'hires.performance', detail: JSON.stringify(r.problems) };
|
||||
&& after.data.source === source, detail: JSON.stringify(r.problems) };
|
||||
})().pass);
|
||||
|
||||
/* Gap 4: a reading whose meaning was never declared is refused, not
|
||||
guessed at. `hires.performance` draws `flow` and `stats` — structurally
|
||||
every chart could take it — and says nothing about what its figures
|
||||
mean, which used to be read as consent. */
|
||||
record(`${type}: a reading of unknown meaning is refused, not guessed`, (() => {
|
||||
const before = [seriesNode('card', 'hires.performance')];
|
||||
const r = opsE.applyOperation(before, { op: 'replace', target: 'viz', type }, ectx);
|
||||
const said = JSON.stringify(r.problems);
|
||||
return { pass: !r.ok && r.tree === before && /does not say what its figures mean/i.test(said),
|
||||
detail: said.slice(0, 170) };
|
||||
})().pass);
|
||||
|
||||
record(`${type}: replacement keeps layout, presentation and title`, (() => {
|
||||
const after = opsE.applyOperation([seriesNode('card', 'hires.performance')],
|
||||
const after = opsE.applyOperation([seriesNode('card', readingFor(type))],
|
||||
{ op: 'replace', target: 'viz', type }, ectx).tree[0];
|
||||
return { pass: after.layout.spacingBefore === 'lg' && after.presentation.variant === 'emphasis'
|
||||
&& after.presentation.density === 'compact' && after.props.title === 'Hiring performance',
|
||||
@@ -9678,42 +9905,571 @@ console.log('\n── Candidates vs Talent Pool ──');
|
||||
})().pass);
|
||||
|
||||
record(`${type}: the editor offers it only where the reading fits`, (() => {
|
||||
const fits = nreg.replacements('card',
|
||||
{ shapes: surfaces.dataSourceFor('hires.performance').shapes, page: 'analytics' });
|
||||
const doesNot = nreg.replacements('list',
|
||||
{ shapes: surfaces.dataSourceFor('position.requirements').shapes, page: 'analytics' });
|
||||
return { pass: fits.includes(type) && !doesNot.includes(type),
|
||||
detail: `fits: ${fits.join(',')} | not: ${doesNot.join(',')}` };
|
||||
const source = readingFor(type);
|
||||
const fits = nreg.replacements('card', {
|
||||
shapes: surfaces.dataSourceFor(source).shapes, page: 'analytics',
|
||||
seriesKind: seriesMod.kindOfBinding({ source }), bound: true,
|
||||
});
|
||||
const doesNot = nreg.replacements('list', {
|
||||
shapes: surfaces.dataSourceFor('position.requirements').shapes, page: 'analytics',
|
||||
seriesKind: seriesMod.kindOfBinding({ source: 'position.requirements' }), bound: true,
|
||||
});
|
||||
/* And the one gap 4 is about: structurally able, meaning undeclared. */
|
||||
const unknown = nreg.replacements('card', {
|
||||
shapes: surfaces.dataSourceFor('hires.performance').shapes, page: 'analytics',
|
||||
seriesKind: seriesMod.kindOfBinding({ source: 'hires.performance' }), bound: true,
|
||||
});
|
||||
return { pass: fits.includes(type) && !doesNot.includes(type) && !unknown.includes(type),
|
||||
detail: `fits: ${fits.join(',')} | not: ${doesNot.join(',')} | unknown: ${unknown.join(',') || 'none'}` };
|
||||
})().pass);
|
||||
|
||||
record(`${type}: Owliver plans the same operation the editor emits`, (() => {
|
||||
const tree = [seriesNode('card', 'hires.performance')];
|
||||
const tree = [seriesNode('card', readingFor(type))];
|
||||
const node = inspectE.inspectTree(tree, ectx)[0];
|
||||
const fromEditor = editorOps.replaceOp(node, type);
|
||||
const phrase = type === 'pie' ? 'change the Hiring performance to a pie' : 'change the Hiring performance to a chart';
|
||||
const fromOwliver = intentE.matchUiEdit(phrase, { tree, registry: nreg, page: 'analytics' });
|
||||
const label = nreg.get(type).label.toLowerCase();
|
||||
const fromOwliver = intentE.matchUiEdit(
|
||||
`change the Hiring performance to a ${label}`, { tree, registry: nreg, page: 'analytics' });
|
||||
return { pass: JSON.stringify(fromEditor) === JSON.stringify(fromOwliver?.op),
|
||||
detail: `${JSON.stringify(fromEditor)} vs ${JSON.stringify(fromOwliver?.op)}` };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
record('chart/pie: are not skill section types', (() => {
|
||||
record('visualizations: none of them is a skill section type', (() => {
|
||||
/* The MD vocabulary is untouched, so the Go parser and its oracle are too. */
|
||||
const inMd = surfaces.SUPPORTED_SECTION_TYPES.filter((t) => t === 'chart' || t === 'pie');
|
||||
const inMd = surfaces.SUPPORTED_SECTION_TYPES.filter((t) => VIZ.includes(t));
|
||||
return { pass: inMd.length === 0, detail: surfaces.SUPPORTED_SECTION_TYPES.join(',') };
|
||||
})().pass);
|
||||
|
||||
record('chart/pie: ordinary data questions do not become visualisations', (() => {
|
||||
record('visualizations: ordinary data questions do not become visualisations', (() => {
|
||||
const tree = [seriesNode('card', 'hires.performance')];
|
||||
const ordinary = ['What is on this page?', 'Show recent hires', 'How many candidates applied?',
|
||||
'What is the hiring trend?', 'Compare the top candidates'];
|
||||
const taken = ordinary.filter((q) => {
|
||||
const m = intentE.matchUiEdit(q, { tree, registry: nreg, page: 'analytics' });
|
||||
return m?.kind === 'plan' && (m.op?.type === 'chart' || m.op?.type === 'pie');
|
||||
return m?.kind === 'plan' && VIZ.includes(m.op?.type);
|
||||
});
|
||||
return { pass: taken.length === 0, detail: taken.join(' | ') || 'none taken' };
|
||||
})().pass);
|
||||
|
||||
/* ── Series registry and meaning ──────────────────────────────────────
|
||||
*
|
||||
* The half `dataShapes` could not express. A `flow` is, in the section
|
||||
* vocabulary's own words, "a sequence of stages *or* periods" — so these
|
||||
* check that the difference is now declared, enforced, and explained.
|
||||
*/
|
||||
{
|
||||
const seriesE = await server.ssrLoadModule('/src/lib/ui/series.js');
|
||||
|
||||
record('series: the kind vocabulary is closed', () => true);
|
||||
record('series: the kind vocabulary is closed',
|
||||
JSON.stringify(nodeE.SERIES_KINDS) === JSON.stringify(['periodic', 'cumulative', 'parts']),
|
||||
nodeE.SERIES_KINDS.join(', '));
|
||||
|
||||
record('series: Control Center publishes its hiring activity', (() => {
|
||||
const entry = seriesE.seriesFor('control-center.hiring-activity');
|
||||
return { pass: Boolean(entry) && entry.kind === 'periodic' && entry.measures.length === 4,
|
||||
detail: entry ? `${entry.kind} · ${entry.measures.map((m) => m.key).join(',')}` : 'missing' };
|
||||
})().pass);
|
||||
|
||||
record('series: a registration with an unknown kind is refused', (() => {
|
||||
try {
|
||||
seriesE.registerSeries({ id: 'x.y', kind: 'vibes', measures: [{ key: 'a', label: 'A' }], read: () => [] });
|
||||
return false;
|
||||
} catch (e) { return /unknown kind/i.test(e.message); }
|
||||
})());
|
||||
|
||||
record('series: a registration with no measure is refused', (() => {
|
||||
try {
|
||||
seriesE.registerSeries({ id: 'x.z', kind: 'parts', measures: [], read: () => [] });
|
||||
return false;
|
||||
} catch (e) { return /at least one measure/i.test(e.message); }
|
||||
})());
|
||||
|
||||
record('series: a registration with no read is refused', (() => {
|
||||
try {
|
||||
seriesE.registerSeries({ id: 'x.w', kind: 'parts', measures: [{ key: 'a', label: 'A' }] });
|
||||
return false;
|
||||
} catch (e) { return /needs a `read/i.test(e.message); }
|
||||
})());
|
||||
|
||||
record('series: a reading bound to periods is periodic', (() => {
|
||||
const kind = seriesE.kindOfBinding({ source: 'candidates.activity', params: { periods: ['today'] } });
|
||||
return { pass: kind === 'periodic', detail: String(kind) };
|
||||
})().pass);
|
||||
|
||||
record('series: a binding that cannot say vetoes nothing', (() => {
|
||||
const kind = seriesE.kindOfBinding({ source: 'hires.performance', params: {} });
|
||||
return { pass: kind === null, detail: String(kind) };
|
||||
})().pass);
|
||||
|
||||
record('series: a page series resolves the page\u2019s own rows, unchanged', (() => {
|
||||
const rows = [{ label: '9/1', applications: 3, screened: 2, interviews: 1, hires: 0 }];
|
||||
const out = seriesE.readSeries({ series: 'control-center.hiring-activity' },
|
||||
{ context: { activitySeries: rows } });
|
||||
return { pass: out.rows.length === 1 && out.rows[0].applications === 3
|
||||
&& out.rows[0].label === '9/1' && out.measures.length === 4,
|
||||
detail: JSON.stringify(out.rows) };
|
||||
})().pass);
|
||||
|
||||
record('series: nothing is invented when the page published nothing', (() => {
|
||||
const out = seriesE.readSeries({ series: 'control-center.hiring-activity' }, { context: {} });
|
||||
return { pass: out.rows.length === 0, detail: JSON.stringify(out.rows) };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
/* ── Built-in chart → another visualization ───────────────────────────
|
||||
*
|
||||
* The whole point, end to end on the real page composition: the section
|
||||
* Control Center ships is bound to a series it publishes, and that is the
|
||||
* only reason it can be drawn another way.
|
||||
*/
|
||||
{
|
||||
const seriesE = await server.ssrLoadModule('/src/lib/ui/series.js');
|
||||
await server.ssrLoadModule('/src/pages/admin/control-center/nodes.jsx');
|
||||
const ccTree = () => compE.composePage('control-center', ectx).tree;
|
||||
const activity = () => ccTree().find((n) => n.id === 'cc-activity');
|
||||
|
||||
record('built-in: Hiring activity is bound to the page\u2019s own series', (() => {
|
||||
const node = activity();
|
||||
return { pass: node?.data?.series === 'control-center.hiring-activity',
|
||||
detail: JSON.stringify(node?.data) };
|
||||
})().pass);
|
||||
|
||||
{
|
||||
const verdict = validateE.validateTree(ccTree(), ectx);
|
||||
record('built-in: it composes and validates as it ships',
|
||||
verdict.ok === true, JSON.stringify(verdict.problems).slice(0, 260));
|
||||
}
|
||||
|
||||
for (const type of ['bar-chart', 'line-chart', 'area-chart']) {
|
||||
record(`built-in: Hiring activity can become a ${type}`, (() => {
|
||||
const r = opsE.applyOperation(ccTree(), { op: 'replace', target: 'cc-activity', type }, ectx);
|
||||
const after = r.ok ? nodeE.findNode(r.tree, 'cc-activity') : null;
|
||||
return { pass: r.ok && after.type === type
|
||||
&& after.id === 'cc-activity'
|
||||
&& after.data.series === 'control-center.hiring-activity'
|
||||
&& after.origin === 'builtin',
|
||||
detail: JSON.stringify(r.problems).slice(0, 160) };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
for (const type of ['pie-chart', 'donut-chart']) {
|
||||
record(`built-in: Hiring activity refuses ${type}, and says why`, (() => {
|
||||
const r = opsE.applyOperation(ccTree(), { op: 'replace', target: 'cc-activity', type }, ectx);
|
||||
const said = JSON.stringify(r.problems);
|
||||
return { pass: !r.ok && /ordered in time/i.test(said), detail: said.slice(0, 170) };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
record('built-in: a refused visualization mutates nothing', (() => {
|
||||
const before = ccTree();
|
||||
const r = opsE.applyOperation(before, { op: 'replace', target: 'cc-activity', type: 'pie-chart' }, ectx);
|
||||
return { pass: !r.ok && r.tree === before, detail: String(r.ok) };
|
||||
})().pass);
|
||||
|
||||
record('built-in: the offering is the registry\u2019s, and excludes the untrue ones', (() => {
|
||||
const node = activity();
|
||||
const offered = nreg.replacements(node.type, {
|
||||
page: 'control-center',
|
||||
seriesKind: seriesE.kindOfBinding(node.data),
|
||||
seriesBound: true,
|
||||
bound: true,
|
||||
});
|
||||
return { pass: offered.includes('bar-chart') && offered.includes('line-chart')
|
||||
&& offered.includes('area-chart') && !offered.includes('pie-chart')
|
||||
&& !offered.includes('donut-chart'),
|
||||
detail: offered.join(', ') };
|
||||
})().pass);
|
||||
|
||||
record('built-in: Owliver and the editor emit the same operation', (() => {
|
||||
const tree = ccTree();
|
||||
const node = inspectE.inspectTree(tree, { ...ectx, page: 'control-center' })
|
||||
.find((n) => n.id === 'cc-activity');
|
||||
const fromEditor = editorOps.replaceOp(node, 'bar-chart');
|
||||
const fromOwliver = intentE.matchUiEdit('Change Hiring activity to a bar chart',
|
||||
{ tree, registry: nreg, page: 'control-center' });
|
||||
return { pass: JSON.stringify(fromEditor) === JSON.stringify(fromOwliver?.op),
|
||||
detail: `${JSON.stringify(fromEditor)} vs ${JSON.stringify(fromOwliver?.op)}` };
|
||||
})().pass);
|
||||
|
||||
record('built-in: a bar chart can be changed back to a line chart', (() => {
|
||||
const barred = opsE.applyOperation(ccTree(),
|
||||
{ op: 'replace', target: 'cc-activity', type: 'bar-chart' }, ectx).tree;
|
||||
const r = opsE.applyOperation(barred, { op: 'replace', target: 'cc-activity', type: 'line-chart' }, ectx);
|
||||
const after = r.ok ? nodeE.findNode(r.tree, 'cc-activity') : null;
|
||||
return { pass: r.ok && after.type === 'line-chart'
|
||||
&& after.data.series === 'control-center.hiring-activity',
|
||||
detail: JSON.stringify(r.problems).slice(0, 140) };
|
||||
})().pass);
|
||||
|
||||
record('built-in: a stored patch replays the change from cold', (() => {
|
||||
const patch = { ops: [{ op: 'replace', target: 'cc-activity', type: 'bar-chart' }] };
|
||||
const round = JSON.parse(JSON.stringify(patch));
|
||||
const out = compE.composePage('control-center', { patch: round, ...ectx });
|
||||
const after = out.tree.find((n) => n.id === 'cc-activity');
|
||||
return { pass: after?.type === 'bar-chart' && out.skipped.length === 0,
|
||||
detail: `${after?.type} · skipped ${out.skipped.length}` };
|
||||
})().pass);
|
||||
|
||||
record('built-in: a stored patch that became untrue is refused on load', (() => {
|
||||
const patch = { ops: [{ op: 'replace', target: 'cc-activity', type: 'pie-chart' }] };
|
||||
const out = compE.composePage('control-center', { patch, ...ectx });
|
||||
const after = out.tree.find((n) => n.id === 'cc-activity');
|
||||
return { pass: after?.type === 'cc-activity' && out.skipped.length === 1,
|
||||
detail: `${after?.type} · skipped ${out.skipped.length}` };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
/* ── Continuity, controls, and knowing what a reading means ───────────
|
||||
*
|
||||
* The four gaps the live verification found, each as the smallest test
|
||||
* that would have caught it.
|
||||
*/
|
||||
{
|
||||
const seriesE = await server.ssrLoadModule('/src/lib/ui/series.js');
|
||||
const ccTree = () => compE.composePage('control-center', ectx).tree;
|
||||
const barred = () => opsE.applyOperation(ccTree(),
|
||||
{ op: 'replace', target: 'cc-activity', type: 'bar-chart' }, ectx).tree;
|
||||
const ask = (q, opts = {}) => intentE.matchUiEdit(q,
|
||||
{ tree: ccTree(), registry: nreg, page: 'control-center', ...opts });
|
||||
|
||||
/* ── A. "Change it back to a line chart." ─────────────────────────── */
|
||||
|
||||
record('continuity: a replaced node keeps the name of what it reads', (() => {
|
||||
const node = inspectE.inspectTree(barred(), ectx).find((n) => n.id === 'cc-activity');
|
||||
return { pass: node?.type === 'bar-chart' && node?.title === 'Hiring activity',
|
||||
detail: `${node?.type} · ${node?.title}` };
|
||||
})().pass);
|
||||
|
||||
record('continuity: it is still addressable by that name after replacement', (() => {
|
||||
const m = intentE.matchUiEdit('Change Hiring activity to a line chart',
|
||||
{ tree: barred(), registry: nreg, page: 'control-center' });
|
||||
return { pass: m?.kind === 'plan' && m.op.target === 'cc-activity' && m.op.type === 'line-chart',
|
||||
detail: `${m?.kind}: ${JSON.stringify(m?.op)}` };
|
||||
})().pass);
|
||||
|
||||
record('continuity: "change it back to a line chart" resolves to what was changed', (() => {
|
||||
const m = intentE.matchUiEdit('Change it back to a line chart',
|
||||
{ tree: barred(), registry: nreg, page: 'control-center', focus: 'cc-activity' });
|
||||
return { pass: m?.kind === 'plan' && m.op.op === 'replace'
|
||||
&& m.op.target === 'cc-activity' && m.op.type === 'line-chart',
|
||||
detail: `${m?.kind}: ${JSON.stringify(m?.op)}` };
|
||||
})().pass);
|
||||
|
||||
record('continuity: the same sentence with no subject is still unknown', (() => {
|
||||
const m = intentE.matchUiEdit('Change it back to a line chart',
|
||||
{ tree: barred(), registry: nreg, page: 'control-center' });
|
||||
return { pass: m?.kind === 'unknown', detail: String(m?.kind) };
|
||||
})().pass);
|
||||
|
||||
record('continuity: a request that names something else is never redirected', (() => {
|
||||
/* Focus is a last resort, not a preference: the words still win. */
|
||||
const m = intentE.matchUiEdit('Hide the pipeline intelligence',
|
||||
{ tree: ccTree(), registry: nreg, page: 'control-center', focus: 'cc-activity' });
|
||||
return { pass: m?.kind === 'plan' && m.op.target === 'cc-pipeline',
|
||||
detail: JSON.stringify(m?.op) };
|
||||
})().pass);
|
||||
|
||||
record('continuity: a focus that has left the page resolves to nothing', (() => {
|
||||
const m = intentE.matchUiEdit('Change it back to a line chart',
|
||||
{ tree: barred(), registry: nreg, page: 'control-center', focus: 'no-such-node' });
|
||||
return { pass: m?.kind === 'unknown', detail: String(m?.kind) };
|
||||
})().pass);
|
||||
|
||||
record('continuity: a phrase with no pronoun does not reach for the focus', (() => {
|
||||
const m = intentE.matchUiEdit('Change the audit log to a table',
|
||||
{ tree: ccTree(), registry: nreg, page: 'control-center', focus: 'cc-activity' });
|
||||
return { pass: m?.kind !== 'plan' || m.op.target !== 'cc-activity', detail: String(m?.kind) };
|
||||
})().pass);
|
||||
|
||||
record('continuity: the data is untouched across bar → it → line', (() => {
|
||||
const rows = [{ label: '9/1', applications: 4, screened: 2, interviews: 1, hires: 1 }];
|
||||
const read = (tree) => seriesE.readSeries(tree.find((n) => n.id === 'cc-activity').data,
|
||||
{ context: { activitySeries: rows } });
|
||||
const before = read(ccTree());
|
||||
const asBar = read(barred());
|
||||
const m = intentE.matchUiEdit('Change it back to a line chart',
|
||||
{ tree: barred(), registry: nreg, page: 'control-center', focus: 'cc-activity' });
|
||||
const asLine = read(opsE.applyOperation(barred(), m.op, ectx).tree);
|
||||
const same = JSON.stringify(before.rows) === JSON.stringify(asBar.rows)
|
||||
&& JSON.stringify(asBar.rows) === JSON.stringify(asLine.rows);
|
||||
return { pass: same && before.rows.length === 1 && before.rows[0].applications === 4,
|
||||
detail: JSON.stringify(asLine.rows) };
|
||||
})().pass);
|
||||
|
||||
/* ── B. The control survives the replacement ──────────────────────── */
|
||||
|
||||
record('control: the reading publishes the range, not the component', (() => {
|
||||
const entry = seriesE.seriesFor('control-center.hiring-activity');
|
||||
const values = (entry?.control?.options || []).map((o) => o.value);
|
||||
return { pass: JSON.stringify(values) === JSON.stringify(['7d', '30d', '90d']),
|
||||
detail: values.join(', ') || 'none' };
|
||||
})().pass);
|
||||
|
||||
for (const type of ['bar-chart', 'line-chart', 'area-chart']) {
|
||||
record(`control: 7D/30D/90D is still there after becoming a ${type}`, (() => {
|
||||
const tree = opsE.applyOperation(ccTree(),
|
||||
{ op: 'replace', target: 'cc-activity', type }, ectx).tree;
|
||||
const node = tree.find((n) => n.id === 'cc-activity');
|
||||
const control = seriesE.controlOfBinding(node.data, { range: '30d', setRange: () => {} });
|
||||
return { pass: Boolean(control) && control.value === '30d'
|
||||
&& control.options.length === 3 && control.label.length > 0,
|
||||
detail: control ? `${control.label} · ${control.options.map((o) => o.label).join('/')}` : 'lost' };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
record('control: choosing a range calls the page back, and nothing else', (() => {
|
||||
const seen = [];
|
||||
const control = seriesE.controlOfBinding({ series: 'control-center.hiring-activity' },
|
||||
{ range: '7d', setRange: (v) => seen.push(v) });
|
||||
control.set('90d');
|
||||
return { pass: seen.length === 1 && seen[0] === '90d', detail: seen.join(',') };
|
||||
})().pass);
|
||||
|
||||
record('control: none is invented when the page publishes none', (() => {
|
||||
const control = seriesE.controlOfBinding({ series: 'control-center.hiring-activity' }, {});
|
||||
return { pass: control === null, detail: JSON.stringify(control) };
|
||||
})().pass);
|
||||
|
||||
record('control: a reading that declares none has none', (() => {
|
||||
const control = seriesE.controlOfBinding({ source: 'candidates.activity' }, { range: '7d' });
|
||||
return { pass: control === null, detail: JSON.stringify(control) };
|
||||
})().pass);
|
||||
|
||||
record('control: a control with no option is refused at registration', (() => {
|
||||
try {
|
||||
seriesE.registerSeries({ id: 'x.control', kind: 'parts', measures: [{ key: 'a', label: 'A' }],
|
||||
read: () => [], control: { id: 'r', options: [], read: () => null, write: () => {} } });
|
||||
return false;
|
||||
} catch (e) { return /at least one option/i.test(e.message); }
|
||||
})());
|
||||
|
||||
record('control: a control with no writer is refused at registration', (() => {
|
||||
try {
|
||||
seriesE.registerSeries({ id: 'x.control2', kind: 'parts', measures: [{ key: 'a', label: 'A' }],
|
||||
read: () => [], control: { id: 'r', options: [{ value: 'a', label: 'A' }], read: () => null } });
|
||||
return false;
|
||||
} catch (e) { return /needs a `read` and a `write`/i.test(e.message); }
|
||||
})());
|
||||
|
||||
record('replace: identity, binding, origin and visibility all survive', (() => {
|
||||
const before = ccTree().find((n) => n.id === 'cc-activity');
|
||||
const after = barred().find((n) => n.id === 'cc-activity');
|
||||
return { pass: after.id === before.id && after.origin === before.origin
|
||||
&& Boolean(after.hidden) === Boolean(before.hidden)
|
||||
&& JSON.stringify(after.data) === JSON.stringify(before.data),
|
||||
detail: JSON.stringify({ id: after.id, origin: after.origin, data: after.data }) };
|
||||
})().pass);
|
||||
|
||||
/* ── C. Asking for alternatives, in the words people use ──────────── */
|
||||
|
||||
for (const phrase of [
|
||||
"I don't like this design. Show me other options.",
|
||||
'Can you give me another visualization?',
|
||||
'What else can I use?',
|
||||
'Give me some alternatives.',
|
||||
'I want a different chart.',
|
||||
]) {
|
||||
record(`alternatives: "${phrase.slice(0, 40)}…" is answered without a model`, (() => {
|
||||
/* `matchUiEdit` is a pure function of the tree and the registry: there
|
||||
is no provider to reach and nothing to await, so an answer here is
|
||||
an answer with zero model calls by construction. */
|
||||
const m = ask(phrase, { focus: 'cc-activity' });
|
||||
const types = (m?.options || []).map((o) => o.type);
|
||||
return { pass: m?.kind === 'options' && types.length > 0
|
||||
&& types.every((t) => Boolean(nreg.get(t))),
|
||||
detail: `${m?.kind}: ${types.join(', ') || '—'}` };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
record('alternatives: the answer is the registry\u2019s, and excludes the untrue', (() => {
|
||||
const m = ask('Show me other options.', { focus: 'cc-activity' });
|
||||
const types = (m?.options || []).map((o) => o.type);
|
||||
return { pass: types.includes('bar-chart') && types.includes('area-chart')
|
||||
&& !types.includes('pie-chart') && !types.includes('donut-chart'),
|
||||
detail: types.join(', ') };
|
||||
})().pass);
|
||||
|
||||
record('alternatives: every one offered is one replace would accept', (() => {
|
||||
const m = ask('I want a different chart.', { focus: 'cc-activity' });
|
||||
const bad = (m?.options || []).filter((o) => (
|
||||
!opsE.applyOperation(ccTree(), { op: 'replace', target: 'cc-activity', type: o.type }, ectx).ok
|
||||
));
|
||||
return { pass: (m?.options || []).length > 0 && bad.length === 0,
|
||||
detail: bad.map((o) => o.type).join(', ') || 'none' };
|
||||
})().pass);
|
||||
|
||||
record('alternatives: an ordinary question is still left alone', (() => {
|
||||
const left = [
|
||||
'What are my options for this position?',
|
||||
'What other candidates applied?',
|
||||
'Give me an alternative candidate',
|
||||
'Show me another worker for Friday',
|
||||
'Is there a different applicant for this shift?',
|
||||
].filter((q) => ask(q, { focus: 'cc-activity' }) !== null);
|
||||
return { pass: left.length === 0, detail: left.join(' | ') || 'none taken' };
|
||||
})().pass);
|
||||
|
||||
/* ── D. Meaning that was never established ────────────────────────── */
|
||||
|
||||
record('meaning: a reading that declares one is believed', (() => {
|
||||
const declared = ['position.activity', 'candidates.activity', 'position.pipeline',
|
||||
'candidates.pipeline', 'activity.breakdown']
|
||||
.map((id) => [id, seriesE.kindOfBinding({ source: id })]);
|
||||
const bad = declared.filter(([, kind]) => !nodeE.SERIES_KINDS.includes(kind));
|
||||
return { pass: bad.length === 0, detail: declared.map(([i, k]) => `${i}=${k}`).join(' ') };
|
||||
})().pass);
|
||||
|
||||
record('meaning: every declared kind is one of the closed three', (() => {
|
||||
const bad = surfaces.DATA_SOURCES
|
||||
.filter((source) => source.series != null)
|
||||
.filter((source) => !nodeE.SERIES_KINDS.includes(source.series));
|
||||
return { pass: bad.length === 0, detail: bad.map((s) => `${s.id}=${s.series}`).join(', ') || 'all known' };
|
||||
})().pass);
|
||||
|
||||
record('meaning: a reading that declares none is refused rather than guessed', (() => {
|
||||
const node = nodeE.makeNode({ id: 'viz', type: 'card', origin: 'user',
|
||||
data: { source: 'hires.performance' } });
|
||||
const m = intentE.matchUiEdit('Change the Hiring performance card to a pie chart',
|
||||
{ tree: [node], registry: nreg, page: 'analytics' });
|
||||
return { pass: m?.kind === 'refused' && /does not say what its figures mean/i.test(m.message),
|
||||
detail: `${m?.kind}: ${m?.message}` };
|
||||
})().pass);
|
||||
|
||||
record('meaning: nor is it offered in the first place', (() => {
|
||||
const node = nodeE.makeNode({ id: 'viz', type: 'card', origin: 'user',
|
||||
data: { source: 'hires.performance' } });
|
||||
const m = intentE.matchUiEdit('Show me other options for the card',
|
||||
{ tree: [node], registry: nreg, page: 'analytics', focus: 'viz' });
|
||||
const types = (m?.options || []).map((o) => o.type);
|
||||
const charts = types.filter((t) => nreg.get(t).seriesKinds.length > 0);
|
||||
return { pass: charts.length === 0, detail: types.join(', ') || 'nothing offered' };
|
||||
})().pass);
|
||||
|
||||
record('meaning: a stored patch that never established one is refused on load', (() => {
|
||||
const tree = [nodeE.makeNode({ id: 'viz', type: 'bar-chart', origin: 'user',
|
||||
data: { source: 'hires.performance' } })];
|
||||
const verdict = validateE.validateTree(tree, ectx);
|
||||
return { pass: !verdict.ok, detail: JSON.stringify(verdict.problems).slice(0, 170) };
|
||||
})().pass);
|
||||
|
||||
record('meaning: components that claim no meaning are untouched by the rule', (() => {
|
||||
/* The Card → Table path, which says nothing about what its rows mean
|
||||
and must keep working exactly as it did. */
|
||||
const node = nodeE.makeNode({ id: 'viz', type: 'card', origin: 'user',
|
||||
props: { title: 'Recent hires' }, data: { source: 'hires.recent' } });
|
||||
const r = opsE.applyOperation([node], { op: 'replace', target: 'viz', type: 'table' }, ectx);
|
||||
const after = r.ok ? nodeE.findNode(r.tree, 'viz') : null;
|
||||
return { pass: r.ok && after.type === 'table' && after.data.source === 'hires.recent'
|
||||
&& after.props.title === 'Recent hires',
|
||||
detail: JSON.stringify(r.problems).slice(0, 150) };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
/* ── Asking for the alternatives ──────────────────────────────────────
|
||||
*
|
||||
* The request that most invites a model to invent something. Every answer
|
||||
* here is the registry's, and it is the same list the replace path checks
|
||||
* a single name against.
|
||||
*/
|
||||
{
|
||||
const ccTree = () => compE.composePage('control-center', ectx).tree;
|
||||
const ask = (q) => intentE.matchUiEdit(q, { tree: ccTree(), registry: nreg, page: 'control-center' });
|
||||
|
||||
for (const phrase of [
|
||||
'Show me other options for the Hiring activity',
|
||||
'I do not like this design, show me other designs for Hiring activity',
|
||||
'What else could the Hiring activity be',
|
||||
'Give me alternatives for the Hiring activity',
|
||||
]) {
|
||||
record(`options: "${phrase.slice(0, 44)}…" is answered from the registry`, (() => {
|
||||
const m = ask(phrase);
|
||||
const types = (m?.options || []).map((o) => o.type);
|
||||
return { pass: m?.kind === 'options' && types.length > 0
|
||||
&& types.every((t) => Boolean(nreg.get(t)))
|
||||
&& !types.includes('pie-chart'),
|
||||
detail: `${m?.kind}: ${types.join(', ')}` };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
record('options: at most four are offered', (() => {
|
||||
const m = ask('Show me other options for the Hiring activity');
|
||||
return { pass: (m?.options || []).length <= 4, detail: String(m?.options?.length) };
|
||||
})().pass);
|
||||
|
||||
record('options: every option is one replace would accept', (() => {
|
||||
const m = ask('Show me other options for the Hiring activity');
|
||||
const bad = (m?.options || []).filter((o) => (
|
||||
!opsE.applyOperation(ccTree(), { op: 'replace', target: 'cc-activity', type: o.type }, ectx).ok
|
||||
));
|
||||
return { pass: bad.length === 0, detail: bad.map((o) => o.type).join(', ') || 'none' };
|
||||
})().pass);
|
||||
|
||||
record('options: an ordinary question about options is left alone', (() => {
|
||||
const left = ['What are my options for this position?', 'Show me the candidates',
|
||||
'What other candidates applied?', 'Give me an alternative candidate']
|
||||
.filter((q) => ask(q) !== null);
|
||||
return { pass: left.length === 0, detail: left.join(' | ') || 'none taken' };
|
||||
})().pass);
|
||||
|
||||
record('options: a refusal offers the alternatives', (() => {
|
||||
const m = ask('Change Hiring activity to a pie chart');
|
||||
const labels = (m?.alternatives || []).map((a) => a.type);
|
||||
return { pass: m?.kind === 'refused' && /ordered in time/i.test(m.message)
|
||||
&& labels.includes('bar-chart') && !labels.includes('pie-chart'),
|
||||
detail: `${m?.message} → ${labels.join(', ')}` };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
/* ── Safety ───────────────────────────────────────────────────────────
|
||||
*
|
||||
* Nothing a person can type produces markup, a class name or a component.
|
||||
*/
|
||||
{
|
||||
const ccTree = () => compE.composePage('control-center', ectx).tree;
|
||||
const ask = (q) => intentE.matchUiEdit(q, { tree: ccTree(), registry: nreg, page: 'control-center' });
|
||||
|
||||
record('safety: an invented visualization type is refused, not improvised', (() => {
|
||||
const m = ask('Turn the Hiring activity into a neon 3D holographic chart');
|
||||
const planned = m?.kind === 'plan';
|
||||
return { pass: !planned, detail: `${m?.kind}` };
|
||||
})().pass);
|
||||
|
||||
record('safety: an unregistered type never reaches the tree', (() => {
|
||||
const r = opsE.applyOperation(ccTree(),
|
||||
{ op: 'replace', target: 'cc-activity', type: 'holograph' }, ectx);
|
||||
return { pass: !r.ok && /Unsupported UI type/i.test(JSON.stringify(r.problems)),
|
||||
detail: JSON.stringify(r.problems).slice(0, 120) };
|
||||
})().pass);
|
||||
|
||||
record('safety: a request for custom CSS produces no operation', (() => {
|
||||
const bad = ['Give this card some custom CSS', 'Add class bg-red-500 to the Hiring activity',
|
||||
'Set the style of the Hiring activity to display:none',
|
||||
'Inject <script>alert(1)</script> into the Hiring activity']
|
||||
.filter((q) => ask(q)?.kind === 'plan');
|
||||
return { pass: bad.length === 0, detail: bad.join(' | ') || 'none planned' };
|
||||
})().pass);
|
||||
|
||||
record('safety: a class name in props is dropped by the schema', (() => {
|
||||
const r = opsE.applyOperation(
|
||||
[seriesNode('bar-chart', 'hires.performance')],
|
||||
{ op: 'update', target: 'viz', props: { className: 'bg-red-500', style: 'color:red' } },
|
||||
ectx
|
||||
);
|
||||
const props = r.ok ? nodeE.findNode(r.tree, 'viz').props : null;
|
||||
return { pass: !r.ok || (!('className' in props) && !('style' in props)),
|
||||
detail: JSON.stringify(props) };
|
||||
})().pass);
|
||||
|
||||
record('safety: an ambiguous request asks rather than guesses', (() => {
|
||||
const m = ask('Change this');
|
||||
return { pass: m === null || m.kind === 'ambiguous' || m.kind === 'unknown'
|
||||
|| m.kind === 'unknown-type' || m.kind === 'refused',
|
||||
detail: String(m?.kind) };
|
||||
})().pass);
|
||||
}
|
||||
|
||||
if (!hadWindow) delete globalThis.window;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user