Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
262 lines
9.3 KiB
JavaScript
262 lines
9.3 KiB
JavaScript
// ADR-0005 §C-10 · Phase 8b — widget-atom self-contained snapshots.
|
|
//
|
|
// Pure unit tests for atomsToMarkdown / loadWidgetsForAtoms (no DB).
|
|
// Confirms:
|
|
// - widget atoms emit the canonical fenced block from
|
|
// widget-atom-serializer.js
|
|
// - the block round-trips through parseWidgetAtom
|
|
// - a missing widget falls back to the `missing` placeholder
|
|
// - non-widget atoms render unchanged
|
|
//
|
|
// Test isolation: this file is fully synchronous and has zero DB reach,
|
|
// so the ADR-0009 boot guard is a no-op here.
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { atomsToMarkdown, loadWidgetsForAtoms } from '../renderMarkdown.js';
|
|
import {
|
|
serializeWidgetAtom,
|
|
parseWidgetAtom,
|
|
} from '../../atoms/widget-atom-serializer.js';
|
|
|
|
const FENCE = '```';
|
|
|
|
describe('atomsToMarkdown — widget atoms (ADR-0005 §C-10 / Phase 8b)', () => {
|
|
it('emits canonical block for widget atom with explicit widgetMap', () => {
|
|
const atoms = [
|
|
{ level: 'h1', content: 'Doc title' },
|
|
{ level: 'text', content: 'Intro paragraph.' },
|
|
{
|
|
level: 'widget',
|
|
widget_ref: 218,
|
|
settings_override: { density: 'compact' },
|
|
},
|
|
];
|
|
const widgetMap = {
|
|
218: { id: 218, preset_name: 'tickets-list', config: {} },
|
|
};
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
|
|
expect(md).toContain('# Doc title');
|
|
expect(md).toContain('Intro paragraph.');
|
|
// Canonical fenced block format
|
|
expect(md).toContain(`${FENCE}widget:218 preset=tickets-list`);
|
|
// Body is the canonical settings JSON
|
|
expect(md).toContain('{"density":"compact"}');
|
|
});
|
|
|
|
it('round-trips: serialize → parse yields {widget_ref, preset, settings_override}', () => {
|
|
const atom = {
|
|
level: 'widget',
|
|
widget_ref: 42,
|
|
settings_override: { a: 1, b: 'two', nested: { z: true } },
|
|
};
|
|
const widgetMap = {
|
|
42: { id: 42, preset_name: 'documents', config: {} },
|
|
};
|
|
const md = atomsToMarkdown([atom], widgetMap);
|
|
|
|
// Extract just the fenced block (no preceding atoms here, so md IS the block).
|
|
const parsed = parseWidgetAtom(md);
|
|
expect(parsed.widget_ref).toBe(42);
|
|
expect(parsed.preset).toBe('documents');
|
|
expect(parsed.settings_override).toEqual({ a: 1, b: 'two', nested: { z: true } });
|
|
});
|
|
|
|
it('round-trips deterministically: serialize(parse(serialize(x))) === serialize(x)', () => {
|
|
const block = serializeWidgetAtom({
|
|
widget_ref: 7,
|
|
preset: 'bdd-panel',
|
|
settings: { cols: ['id', 'name'], collapsed: false },
|
|
});
|
|
const parsed = parseWidgetAtom(block);
|
|
const reSerialized = serializeWidgetAtom({
|
|
widget_ref: parsed.widget_ref,
|
|
preset: parsed.preset,
|
|
settings: parsed.settings_override,
|
|
});
|
|
expect(reSerialized).toBe(block);
|
|
});
|
|
|
|
it('falls back to `missing` marker when widgetMap key is null', () => {
|
|
const atoms = [
|
|
{ level: 'widget', widget_ref: 999, settings_override: { x: 1 } },
|
|
];
|
|
const widgetMap = { 999: null }; // explicit "we looked, it's gone"
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
|
|
expect(md).toBe(`${FENCE}widget:999 missing\n${FENCE}`);
|
|
// Missing-marker is intentionally NOT round-trip safe — parseWidgetAtom
|
|
// rejects it because there is no `preset=...` segment. The marker is
|
|
// for archive/human consumption only.
|
|
expect(() => parseWidgetAtom(md)).toThrow();
|
|
});
|
|
|
|
it('falls back to `missing` marker when widgetMap key is absent', () => {
|
|
const atoms = [{ level: 'widget', widget_ref: 123 }];
|
|
const widgetMap = {}; // 123 not present → treat as missing
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
expect(md).toBe(`${FENCE}widget:123 missing\n${FENCE}`);
|
|
});
|
|
|
|
it('uses atom.preset when no widgetMap is provided (atom-as-source-of-truth)', () => {
|
|
const atoms = [
|
|
{
|
|
level: 'widget',
|
|
widget_ref: 5,
|
|
preset: 'documents',
|
|
settings_override: { foo: 'bar' },
|
|
},
|
|
];
|
|
const md = atomsToMarkdown(atoms); // no widgetMap
|
|
|
|
expect(md).toContain(`${FENCE}widget:5 preset=documents`);
|
|
expect(md).toContain('{"foo":"bar"}');
|
|
const parsed = parseWidgetAtom(md);
|
|
expect(parsed.widget_ref).toBe(5);
|
|
expect(parsed.preset).toBe('documents');
|
|
expect(parsed.settings_override).toEqual({ foo: 'bar' });
|
|
});
|
|
|
|
it('emits widget:0 missing for widget atom with no usable widget_ref', () => {
|
|
const atoms = [{ level: 'widget' /* no widget_ref */ }];
|
|
const md = atomsToMarkdown(atoms);
|
|
expect(md).toBe(`${FENCE}widget:0 missing\n${FENCE}`);
|
|
});
|
|
|
|
it('does not affect non-widget atoms (regression guard)', () => {
|
|
const atoms = [
|
|
{ level: 'h1', content: 'Title' },
|
|
{ level: 'h2', content: 'Sub' },
|
|
{ level: 'h3', content: 'Sub-sub' },
|
|
{ level: 'divider' },
|
|
{ level: 'text', content: 'A paragraph.' },
|
|
];
|
|
const md = atomsToMarkdown(atoms);
|
|
expect(md).toBe('# Title\n\n## Sub\n\n### Sub-sub\n\n---\n\nA paragraph.');
|
|
});
|
|
|
|
it('mixes paragraph + widget atom into one canonical snapshot', () => {
|
|
const atoms = [
|
|
{ level: 'text', content: 'Paragraph before the widget.' },
|
|
{ level: 'widget', widget_ref: 18, settings_override: {} },
|
|
{ level: 'text', content: 'Paragraph after.' },
|
|
];
|
|
const widgetMap = {
|
|
18: { id: 18, preset_name: 'tickets-list', config: {} },
|
|
};
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
|
|
const expectedBlock = serializeWidgetAtom({
|
|
widget_ref: 18,
|
|
preset: 'tickets-list',
|
|
settings: {},
|
|
});
|
|
expect(md).toBe(
|
|
`Paragraph before the widget.\n\n${expectedBlock}\n\nParagraph after.`
|
|
);
|
|
|
|
// Round-trip: parse the embedded block back out.
|
|
const parsed = parseWidgetAtom(expectedBlock);
|
|
expect(parsed).toEqual({
|
|
widget_ref: 18,
|
|
preset: 'tickets-list',
|
|
settings_override: {},
|
|
});
|
|
});
|
|
|
|
it('settings_override falls back to {} for non-object/array values', () => {
|
|
const atoms = [
|
|
{ level: 'widget', widget_ref: 1, settings_override: 'oops' },
|
|
{ level: 'widget', widget_ref: 2, settings_override: ['arr', 'is', 'invalid'] },
|
|
{ level: 'widget', widget_ref: 3, settings_override: null },
|
|
];
|
|
const widgetMap = {
|
|
1: { id: 1, preset_name: 'p', config: {} },
|
|
2: { id: 2, preset_name: 'p', config: {} },
|
|
3: { id: 3, preset_name: 'p', config: {} },
|
|
};
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
// All three should serialize with empty `{}` body.
|
|
const blocks = md.split('\n\n');
|
|
expect(blocks).toHaveLength(3);
|
|
for (const block of blocks) {
|
|
expect(block).toMatch(/preset=p\n\{\}\n```$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('loadWidgetsForAtoms — DI loader (no DB)', () => {
|
|
it('returns a map of {widgetId → widget|null} keyed by atom widget_refs', async () => {
|
|
const atoms = [
|
|
{ level: 'h1', content: 'x' },
|
|
{ level: 'widget', widget_ref: 10 },
|
|
{ level: 'widget', widget_ref: 20 },
|
|
{ level: 'widget', widget_ref: 999 }, // gone
|
|
];
|
|
const calls = [];
|
|
const loadWidget = async (id) => {
|
|
calls.push(id);
|
|
if (id === 10) return { id: 10, preset_name: 'a', config: {} };
|
|
if (id === 20) return { id: 20, preset_name: 'b', config: {} };
|
|
return null;
|
|
};
|
|
const map = await loadWidgetsForAtoms(atoms, { loadWidget });
|
|
expect(map[10]).toMatchObject({ id: 10, preset_name: 'a' });
|
|
expect(map[20]).toMatchObject({ id: 20, preset_name: 'b' });
|
|
expect(map[999]).toBeNull();
|
|
expect(calls.sort()).toEqual([10, 20, 999]);
|
|
});
|
|
|
|
it('returns {} for empty atoms or atoms with no widget refs', async () => {
|
|
expect(await loadWidgetsForAtoms([])).toEqual({});
|
|
expect(await loadWidgetsForAtoms([{ level: 'h1', content: 'x' }])).toEqual({});
|
|
});
|
|
|
|
it('skips invalid widget_refs (non-numeric, zero, negative)', async () => {
|
|
const atoms = [
|
|
{ level: 'widget', widget_ref: null },
|
|
{ level: 'widget', widget_ref: 0 },
|
|
{ level: 'widget', widget_ref: -5 },
|
|
{ level: 'widget', widget_ref: 'abc' },
|
|
];
|
|
const loadWidget = async () => ({ id: 1, preset_name: 'never-called', config: {} });
|
|
const map = await loadWidgetsForAtoms(atoms, { loadWidget });
|
|
expect(map).toEqual({});
|
|
});
|
|
|
|
it('treats loader exceptions as "missing" (null in map)', async () => {
|
|
const atoms = [{ level: 'widget', widget_ref: 7 }];
|
|
const loadWidget = async () => { throw new Error('db down'); };
|
|
const map = await loadWidgetsForAtoms(atoms, { loadWidget });
|
|
expect(map[7]).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('end-to-end: atoms → markdown → parse', () => {
|
|
it('document with one paragraph + one widget-atom snapshots correctly', () => {
|
|
const atoms = [
|
|
{ level: 'text', content: 'Hello world.' },
|
|
{
|
|
level: 'widget',
|
|
widget_ref: 218,
|
|
settings_override: { display_mode: 'card', filter_status: 'open' },
|
|
},
|
|
];
|
|
const widgetMap = {
|
|
218: { id: 218, preset_name: 'tickets-list', config: {} },
|
|
};
|
|
const md = atomsToMarkdown(atoms, widgetMap);
|
|
|
|
// Snapshot must contain plaintext paragraph followed by canonical block.
|
|
expect(md.startsWith('Hello world.\n\n')).toBe(true);
|
|
|
|
// Extract the widget block (everything after the first '\n\n').
|
|
const block = md.slice(md.indexOf('\n\n') + 2);
|
|
const parsed = parseWidgetAtom(block);
|
|
expect(parsed).toEqual({
|
|
widget_ref: 218,
|
|
preset: 'tickets-list',
|
|
settings_override: { display_mode: 'card', filter_status: 'open' },
|
|
});
|
|
});
|
|
});
|