Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
198 lines
7.2 KiB
JavaScript
198 lines
7.2 KiB
JavaScript
// ADR-0003 Phase 2 · T-127902 / T-127912
|
|
//
|
|
// Unit tests for widget-atom-resolver + widget-atom-serializer. No DB:
|
|
// the resolver takes an injected `loadWidget` loader so we can exercise
|
|
// happy-path, missing-widget, and merge precedence in isolation.
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { resolveWidgetAtom } from '../widget-atom-resolver.js';
|
|
import {
|
|
serializeWidgetAtom,
|
|
parseWidgetAtom,
|
|
_stableStringify,
|
|
} from '../widget-atom-serializer.js';
|
|
|
|
function makeLoader(widgetsById) {
|
|
return async (id) => widgetsById[id] ?? null;
|
|
}
|
|
|
|
describe('resolveWidgetAtom', () => {
|
|
it('resolves happy-path: widget.config + no override', async () => {
|
|
const atom = { widget_ref: 218 };
|
|
const loadWidget = makeLoader({
|
|
218: { id: 218, preset_name: 'bdd-panel', config: { cols: ['id', 'name'] }, title: 'ADRs' },
|
|
});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out.fallback).toBe(false);
|
|
expect(out.preset).toBe('bdd-panel');
|
|
expect(out.widget.id).toBe(218);
|
|
expect(out.resolved_settings).toEqual({ cols: ['id', 'name'] });
|
|
});
|
|
|
|
it('shallow merge: settings_override wins over widget.config', async () => {
|
|
const atom = {
|
|
widget_ref: 9,
|
|
settings_override: { b: 3, c: 4 },
|
|
};
|
|
const loadWidget = makeLoader({
|
|
9: { id: 9, preset_name: 'ticket-filter', config: { a: 1, b: 2 } },
|
|
});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out.resolved_settings).toEqual({ a: 1, b: 3, c: 4 });
|
|
});
|
|
|
|
it('parses widget.config when stored as a JSON string', async () => {
|
|
const atom = { widget_ref: 5 };
|
|
const loadWidget = makeLoader({
|
|
5: { id: 5, preset_name: 'bdd-panel', config: '{"x":1}' },
|
|
});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out.resolved_settings).toEqual({ x: 1 });
|
|
});
|
|
|
|
it('broken widget_ref → fallback (widget_not_found)', async () => {
|
|
const atom = { widget_ref: 9999 };
|
|
const loadWidget = makeLoader({});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out).toEqual({
|
|
widget: null,
|
|
fallback: true,
|
|
reason: 'widget_not_found',
|
|
widget_ref: 9999,
|
|
});
|
|
});
|
|
|
|
it('missing widget_ref → fallback (no_widget_ref)', async () => {
|
|
const out = await resolveWidgetAtom({ level: 'widget' }, { loadWidget: makeLoader({}) });
|
|
expect(out.fallback).toBe(true);
|
|
expect(out.reason).toBe('no_widget_ref');
|
|
});
|
|
|
|
it('non-numeric widget_ref → fallback', async () => {
|
|
const out = await resolveWidgetAtom(
|
|
{ widget_ref: 'not-a-number' },
|
|
{ loadWidget: makeLoader({}) }
|
|
);
|
|
expect(out.fallback).toBe(true);
|
|
expect(out.reason).toBe('no_widget_ref');
|
|
});
|
|
|
|
it('falls back to preset_name from config when widget.preset_name missing', async () => {
|
|
const atom = { widget_ref: 7 };
|
|
const loadWidget = makeLoader({
|
|
7: { id: 7, preset_name: null, config: { preset_name: 'from-config' } },
|
|
});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out.preset).toBe('from-config');
|
|
});
|
|
|
|
it('empty override object is treated as no-op', async () => {
|
|
const atom = { widget_ref: 1, settings_override: {} };
|
|
const loadWidget = makeLoader({
|
|
1: { id: 1, preset_name: 'bdd-panel', config: { a: 1 } },
|
|
});
|
|
const out = await resolveWidgetAtom(atom, { loadWidget });
|
|
expect(out.resolved_settings).toEqual({ a: 1 });
|
|
});
|
|
});
|
|
|
|
describe('serializeWidgetAtom / parseWidgetAtom', () => {
|
|
it('serializes to the canonical 3-line fenced block', () => {
|
|
const md = serializeWidgetAtom({
|
|
widget_ref: 218,
|
|
preset: 'bdd-panel',
|
|
settings: { b: 2, a: 1 },
|
|
});
|
|
expect(md).toBe(
|
|
'```widget:218 preset=bdd-panel\n{"a":1,"b":2}\n```'
|
|
);
|
|
});
|
|
|
|
it('key order is deterministic regardless of input order', () => {
|
|
const a = serializeWidgetAtom({ widget_ref: 1, preset: 'bdd-panel', settings: { z: 1, a: 2, m: 3 } });
|
|
const b = serializeWidgetAtom({ widget_ref: 1, preset: 'bdd-panel', settings: { m: 3, a: 2, z: 1 } });
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it('nested objects also sort keys deterministically', () => {
|
|
const raw = { outer: { z: 1, a: 2 }, alpha: 9 };
|
|
const s = _stableStringify(raw);
|
|
expect(s).toBe('{"alpha":9,"outer":{"a":2,"z":1}}');
|
|
});
|
|
|
|
it('empty settings serialize to {}', () => {
|
|
const md = serializeWidgetAtom({ widget_ref: 5, preset: 'default' });
|
|
expect(md).toBe('```widget:5 preset=default\n{}\n```');
|
|
});
|
|
|
|
it('parseWidgetAtom extracts widget_ref, preset, settings_override', () => {
|
|
const md = '```widget:42 preset=bdd-panel\n{"a":1,"b":2}\n```';
|
|
expect(parseWidgetAtom(md)).toEqual({
|
|
widget_ref: 42,
|
|
preset: 'bdd-panel',
|
|
settings_override: { a: 1, b: 2 },
|
|
});
|
|
});
|
|
|
|
it('serialize → parse → serialize is a fixed point (canonical bytes)', () => {
|
|
const md1 = serializeWidgetAtom({
|
|
widget_ref: 218,
|
|
preset: 'bdd-panel',
|
|
settings: { c: 4, a: 1, b: 3 },
|
|
});
|
|
const parsed = parseWidgetAtom(md1);
|
|
const md2 = serializeWidgetAtom({
|
|
widget_ref: parsed.widget_ref,
|
|
preset: parsed.preset,
|
|
settings: parsed.settings_override,
|
|
});
|
|
expect(md2).toBe(md1);
|
|
});
|
|
|
|
it('round-trip through resolve: parse → resolve → serialize reproduces identical bytes', async () => {
|
|
// Widget config + atom override. The full round-trip goes:
|
|
// md → parse → atom → resolve → {preset, resolved_settings}
|
|
// → serialize(resolved_settings) === md
|
|
// Invariant: resolved_settings ⊇ widget.config by shallow merge, so
|
|
// serialising the resolved map and re-parsing yields the same override,
|
|
// which re-resolves to the same resolved_settings (idempotent).
|
|
const widget = { id: 77, preset_name: 'bdd-panel', config: { a: 1, b: 2 } };
|
|
const loadWidget = makeLoader({ 77: widget });
|
|
|
|
const initialResolved = { a: 1, b: 3, c: 4 };
|
|
const md1 = serializeWidgetAtom({
|
|
widget_ref: 77,
|
|
preset: 'bdd-panel',
|
|
settings: initialResolved,
|
|
});
|
|
|
|
const parsed = parseWidgetAtom(md1);
|
|
const resolved = await resolveWidgetAtom(
|
|
{ widget_ref: parsed.widget_ref, settings_override: parsed.settings_override },
|
|
{ loadWidget }
|
|
);
|
|
expect(resolved.fallback).toBe(false);
|
|
expect(resolved.resolved_settings).toEqual(initialResolved);
|
|
|
|
const md2 = serializeWidgetAtom({
|
|
widget_ref: parsed.widget_ref,
|
|
preset: resolved.preset,
|
|
settings: resolved.resolved_settings,
|
|
});
|
|
expect(md2).toBe(md1);
|
|
});
|
|
|
|
it('parse rejects malformed input', () => {
|
|
expect(() => parseWidgetAtom('not a fence')).toThrow();
|
|
expect(() => parseWidgetAtom('```widget:abc preset=x\n{}\n```')).toThrow();
|
|
expect(() => parseWidgetAtom('```widget:1 preset=bad name\n{}\n```')).toThrow();
|
|
expect(() => parseWidgetAtom('```widget:1 preset=x\n{bad json}\n```')).toThrow();
|
|
expect(() => parseWidgetAtom('```widget:1 preset=x\n[1,2,3]\n```')).toThrow();
|
|
});
|
|
|
|
it('serialize rejects invalid inputs', () => {
|
|
expect(() => serializeWidgetAtom({ widget_ref: 0, preset: 'x' })).toThrow();
|
|
expect(() => serializeWidgetAtom({ widget_ref: 1, preset: '' })).toThrow();
|
|
expect(() => serializeWidgetAtom({ widget_ref: 1, preset: 'has spaces' })).toThrow();
|
|
});
|
|
});
|