godcrm/backend/services/atoms/__tests__/widget-atom-resolver.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

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();
});
});