atlas-iac/testing/tests/test_hermes_hux_ui_runtime_wave_a.js

482 lines
28 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const foundation = require('../../dockerfiles/hermes-webui-hux/foundation.js');
const contract = require('../../dockerfiles/hermes-webui-hux/runtime/wave_a_contract.js');
const runtimeApi = require('../../dockerfiles/hermes-webui-hux/runtime/wave_a_activity_memory.js');
const IDENTITY = Object.freeze({tenantSlot: 'slot-3', userRef: 'usr_0123456789abcdef',
surface: 'chat', trust: 'router'});
const CONVERSATION = 'conv_0001abcd';
function capability(overrides) {
return {schema: 'hux.capabilities.v1', contract_version: '1.1.0',
identity: {tenant_slot: IDENTITY.tenantSlot, subject: IDENTITY.userRef,
surface: IDENTITY.surface, trust: IDENTITY.trust},
cards: [
{card: 'HUX-11', flag: 'hux.foundation', enabled: true, routes: ['/hux/v1/capabilities']},
{card: 'HUX-01', flag: 'hux.activity_timeline', enabled: true,
routes: ['/hux/v1/conversations/{id}/events']},
{card: 'HUX-10', flag: 'hux.privacy', enabled: true, routes: ['/hux/v1/privacy/policy']},
{card: 'HUX-02', flag: 'hux.memory_control', enabled: true, routes: ['/hux/v1/memory']},
], server: {}, ...overrides};
}
function event(overrides) {
return {schema: 'hux.event.v1', id: 'evt_0001aaaa', seq: 1, ts: '2026-08-24T10:00:00Z',
conversation_id: CONVERSATION, kind: 'tool.result',
summary: 'Checked token=private Bearer abc.DEF', sensitivity: 'personal',
redaction: {level: 'none'}, turn: 1,
evidence: [{kind: 'tool_result', id: 'call-1', uri: 'https://private.example'}],
identity: capability().identity, detail: {raw: 'never render'}, ...overrides};
}
function memory(overrides) {
return {schema: 'hux.memory.v1', id: 'mem_0001aaaa', owner: IDENTITY.userRef,
scope: {level: 'conversation', scope_id: CONVERSATION}, kind: 'preference',
content: 'Prefers concise answers token=private', status: 'active', approval_mode: 'automatic',
sensitivity: 'personal', ttl: {policy: 'decay', decay_days: 180},
created_at: '2026-08-24T10:00:00Z', updated_at: '2026-08-24T10:00:01Z',
reason: 'The user asked password=hidden', retrievable: true, revision: 2,
identity: capability().identity, ...overrides};
}
function response(status, body, etag) {
return {status, ok: status >= 200 && status < 300, json: async () => body,
headers: {get: (name) => name === 'ETag' ? etag || null : null}};
}
test('canonical capability adapter bridges the backend cards to the foundation client shape', () => {
const normalized = contract.normalizeCapabilities(capability(), IDENTITY);
assert.deepEqual(normalized.flags,
['hux.foundation', 'hux.activity_timeline', 'hux.privacy', 'hux.memory_control']);
assert(Object.isFrozen(normalized));
assert.equal(normalized.contractVersion, '1.1.0');
const disabled = contract.normalizeCapabilities(capability({cards: [
{card: 'HUX-11', flag: 'hux.foundation', enabled: false, routes: []},
{card: 'HUX-01', flag: 'hux.activity_timeline', enabled: true, routes: []},
]}), IDENTITY);
assert.deepEqual(disabled.flags, []);
const missingDependency = contract.normalizeCapabilities(capability({cards: [
capability().cards[0],
{card: 'HUX-04', flag: 'hux.artifacts', enabled: true, routes: ['/hux/v1/artifacts']},
{card: 'HUX-08', flag: 'hux.research', enabled: true, routes: ['/hux/v1/sources']},
]}), IDENTITY);
assert.deepEqual(missingDependency.flags, ['hux.foundation']);
const invalid = [null, [], capability({schema: 'old'}), capability({contract_version: '2.0.0'}),
capability({identity: {...capability().identity, tenant_slot: 'slot-4'}}),
capability({cards: 'bad'}), capability({cards: Array(foundation.FLAGS.length * 2 + 1).fill({})}),
capability({cards: [null]}), capability({cards: [
{card: 'HUX-01', flag: 'wrong', enabled: true, routes: []}]}),
capability({cards: [{card: 'HUX-99', flag: 'hux.foundation', enabled: 'yes', routes: []}]}),
capability({cards: [{card: 'HUX-99', flag: 'hux.foundation', enabled: true, routes: 'bad'}]}),
capability({cards: [{card: 'HUX-99', flag: 'hux.foundation', enabled: true, routes: ['remote']}]}),
capability({cards: [capability().cards[0], capability().cards[0]]}),
];
invalid.forEach((value) => assert.throws(() => contract.normalizeCapabilities(value, IDENTITY), /capabilit/i));
});
test('identity, endpoint, idempotency, and display helpers fail closed', () => {
assert.deepEqual(contract.normalizeExpectedIdentity(IDENTITY), IDENTITY);
for (const bad of [null, {...IDENTITY, tenantSlot: 'tenant'}, {...IDENTITY, userRef: 'brad@example.com'},
{...IDENTITY, surface: 'browser'}, {...IDENTITY, trust: 'human'}]) {
assert.throws(() => contract.normalizeExpectedIdentity(bad), /identity/);
}
assert.equal(contract.sameCanonicalIdentity(capability().identity, IDENTITY), true);
assert.equal(contract.sameCanonicalIdentity(null, IDENTITY), false);
assert.equal(contract.sameCanonicalIdentity({...capability().identity, subject: 'usr_ffffffffffffffff'}, IDENTITY), false);
assert.equal(contract.safeEndpoint('/hux/v1', '/memory'), '/hux/v1/memory');
for (const path of ['memory', '//remote', '/../tenant', '/%2e%2e/x', '/x\\y']) {
assert.throws(() => contract.safeEndpoint('/hux/v1', path), /scoped/);
}
assert.equal(contract.safeIdempotencyKey('webui:key:0001'), 'webui:key:0001');
assert.throws(() => contract.safeIdempotencyKey('short'), /Idempotency/);
assert.throws(() => contract.safeIdempotencyKey('bad key value'), /Idempotency/);
assert.equal(contract.safeText(null, 'fallback', 20), 'fallback');
assert.equal(contract.safeText(' \n ', 'fallback', 20), 'fallback');
assert.equal(contract.safeText('password=x Bearer abc.DEF', 'fallback', 100),
'password=[redacted] Bearer [redacted]');
assert.equal(contract.safeText('-----BEGIN PRIVATE KEY----- x -----END PRIVATE KEY-----', 'x', 100), '[redacted]');
assert.equal(contract.safeText('abcdef', 'x', 4), 'abc…');
});
test('canonical client negotiates safely, stays same-origin, and sends exact concurrency headers', async () => {
const calls = [];
let next = response(200, capability());
const client = contract.createCanonicalClient({expectedIdentity: IDENTITY, baseUrl: '/hux/v1/',
fetcher: async (...args) => { calls.push(args); return next; }});
const phases = [];
const unsubscribe = client.subscribe((state) => phases.push(state.phase));
await assert.rejects(() => client.request('/memory'), /not ready/);
await client.negotiate();
assert.equal(client.getState().phase, 'ready');
assert.equal(client.enabled('hux.memory_control'), true);
assert.equal(client.enabled('hux.bogus'), false);
assert.equal(client.endpoint('/memory'), '/hux/v1/memory');
next = response(200, {ok: true}, '3');
const result = await client.request('/memory/mem_0001aaaa/edit', {method: 'POST',
headers: {'If-Match': '2', 'Idempotency-Key': 'memory:key:0001'}, body: {content: 'new'}});
assert.deepEqual(result, {body: {ok: true}, etag: '3'});
const init = calls.at(-1)[1];
assert.equal(init.credentials, 'same-origin');
assert.equal(init.cache, 'no-store');
assert.equal(init.headers['If-Match'], '2');
assert.equal(init.headers['Idempotency-Key'], 'memory:key:0001');
assert.equal(init.headers['Content-Type'], 'application/json');
assert.equal(init.body, '{"content":"new"}');
next = response(409, {secret: 'must not escape'});
await assert.rejects(() => client.request('/memory'), (error) => error.status === 409 &&
!error.message.includes('secret'));
next = response(500, {secret: 'private'});
await assert.rejects(() => client.request('/memory'), /failed \(500\)/);
unsubscribe();
assert.deepEqual(phases, ['disabled', 'loading', 'ready']);
assert.throws(() => client.subscribe('bad'), /Listener/);
});
test('canonical client handles disabled, invalid, failed and superseded negotiations', async () => {
assert.throws(() => contract.createCanonicalClient({expectedIdentity: IDENTITY, fetcher: null}), /fetch/);
assert.throws(() => contract.createCanonicalClient({expectedIdentity: IDENTITY,
baseUrl: 'https://remote.example', fetcher: async () => response(200, {})}), /same-origin/);
let next = response(404, {});
const client = contract.createCanonicalClient({expectedIdentity: IDENTITY, fetcher: async () => next});
await client.negotiate();
assert.equal(client.getState().phase, 'disabled');
next = response(500, {raw: 'private'});
await client.negotiate();
assert.equal(client.getState().phase, 'error');
assert.equal(client.getState().error.includes('private'), false);
next = response(200, capability({identity: {...capability().identity, subject: 'usr_ffffffffffffffff'}}));
await client.negotiate();
assert.equal(client.getState().error, 'Hermes workspace identity could not be verified.');
next = response(200, capability({cards: [{card: 'HUX-11', flag: 'hux.foundation', enabled: false, routes: []}]}));
await client.negotiate();
assert.equal(client.getState().phase, 'disabled');
const resolvers = [];
const late = contract.createCanonicalClient({expectedIdentity: IDENTITY,
fetcher: () => new Promise((resolve) => resolvers.push(resolve))});
const first = late.negotiate(); const second = late.negotiate();
resolvers[1](response(200, capability())); await second;
resolvers[0](response(404, {})); await first;
assert.equal(late.getState().phase, 'ready');
});
test('event pages expose only redacted summaries and bounded reference identifiers', () => {
const page = contract.normalizeEventPage({items: [event()], next: null}, IDENTITY, CONVERSATION);
assert.equal(page.items[0].summary, 'Checked token=[redacted] Bearer [redacted]');
assert.deepEqual(page.items[0].evidence, [{kind: 'tool_result', id: 'call-1'}]);
assert.equal(JSON.stringify(page).includes('never render'), false);
const hidden = contract.normalizeEventPage({items: [event({redaction: {level: 'full'}})], next: 1},
IDENTITY, CONVERSATION);
assert.equal(hidden.items[0].summary, 'Details hidden by privacy controls.');
const empty = contract.normalizeEventPage({items: [event({summary: ''})], next: null}, IDENTITY, CONVERSATION);
assert.equal(empty.items[0].summary, 'Activity details unavailable.');
const noEvidence = contract.normalizeEventPage({items: [event({evidence: [{kind: 'raw', id: 'x'}, null]})],
next: null}, IDENTITY, CONVERSATION);
assert.deepEqual(noEvidence.items[0].evidence, []);
const invalidPages = [null, {items: null, next: null}, {items: Array(201).fill(event()), next: null},
{items: [], next: '1'}, {items: [event({seq: 2}), event({id: 'evt_0002aaaa', seq: 1})], next: null}];
invalidPages.forEach((raw) => assert.throws(() => contract.normalizeEventPage(raw, IDENTITY, CONVERSATION), /Activity/));
const invalidEvents = [null, event({identity: {...capability().identity, tenant_slot: 'slot-4'}}),
event({schema: 'old'}), event({id: 'bad'}), event({seq: -1}), event({ts: 'today'}),
event({conversation_id: 'conv_other1234'}), event({kind: 'tool.raw'}),
event({sensitivity: 'secret'}), event({redaction: null}), event({redaction: {level: 'maybe'}})];
invalidEvents.forEach((item) => assert.throws(() =>
contract.normalizeEventPage({items: [item], next: null}, IDENTITY, CONVERSATION), /boundary|Activity/));
});
test('memory pages enforce ownership, conversation scope, redaction, retention and revisions', () => {
const active = contract.normalizeMemoryPage({items: [memory()], next: null}, IDENTITY, CONVERSATION).items[0];
assert.equal(active.content, 'Prefers concise answers token=[redacted]');
assert.equal(active.reason, 'The user asked password=[redacted]');
assert.equal(active.retention, 'decays after 180 days');
assert.equal(contract.normalizeMemoryPage({items: [memory({ttl: {policy: 'never'}})], next: null},
IDENTITY, CONVERSATION).items[0].retention, 'does not expire');
assert.equal(contract.normalizeMemoryPage({items: [memory({ttl: {policy: 'expires_at',
expires_at: '2026-09-24T10:00:00Z'}})], next: null}, IDENTITY, CONVERSATION).items[0].retention,
'expires 2026-09-24T10:00:00Z');
assert.equal(contract.normalizeMemoryPage({items: [memory({status: 'forgotten', content: '',
retrievable: false})], next: null}, IDENTITY, CONVERSATION).items[0].content,
'Content removed by privacy controls.');
assert.equal(contract.normalizeMemoryPage({items: [memory({sensitivity: 'restricted', content: ''})], next: null},
IDENTITY, CONVERSATION).items[0].content, 'Content removed by privacy controls.');
assert.equal(contract.normalizeMemoryPage({items: [memory({scope: {level: 'global'}})], next: null},
IDENTITY, CONVERSATION).items[0].scope.scopeId, null);
assert.equal(contract.normalizeMemoryPage({items: [memory({scope: {level: 'project', scope_id: 'prj_0001aaaa'}})],
next: null}, IDENTITY, CONVERSATION).items[0].scope.level, 'project');
const invalidPages = [null, {items: null, next: null}, {items: Array(501).fill(memory()), next: null},
{items: [], next: 1}, {items: [memory(), memory()], next: null}];
invalidPages.forEach((raw) => assert.throws(() => contract.normalizeMemoryPage(raw, IDENTITY, CONVERSATION), /Memory/));
const invalid = [null, memory({identity: {...capability().identity, subject: 'usr_ffffffffffffffff'}}),
memory({schema: 'old'}), memory({id: 'bad'}), memory({owner: 'usr_ffffffffffffffff'}),
memory({revision: 0}), memory({kind: 'secret'}), memory({status: 'deleted'}),
memory({approval_mode: 'always'}), memory({sensitivity: 'secret'}), memory({retrievable: 'yes'}),
memory({created_at: 'today'}), memory({updated_at: 'today'}),
memory({updated_at: '2026-08-23T10:00:00Z'}), memory({content: 4}),
memory({content: 'x'.repeat(2001)}), memory({reason: ''}), memory({reason: 'x'.repeat(281)}),
memory({scope: null}), memory({scope: {level: 'unknown'}}),
memory({scope: {level: 'project', scope_id: 'bad'}}),
memory({scope: {level: 'conversation', scope_id: 'conv_other1234'}}),
memory({status: 'forgotten', content: 'must be gone'}), memory({ttl: null}),
memory({ttl: {policy: 'unknown'}}), memory({ttl: {policy: 'expires_at', expires_at: 'today'}}),
memory({ttl: {policy: 'decay', decay_days: 0}}), memory({ttl: {policy: 'decay', decay_days: 3651}})];
invalid.forEach((item) => assert.throws(() =>
contract.normalizeMemoryPage({items: [item], next: null}, IDENTITY, CONVERSATION), /boundary|Memory|Terminal/));
});
class FakeNode {
constructor(tag) {
this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = []; this.dataset = {};
this.hidden = false; this.parentNode = null; this.listeners = {}; this.textContent = ''; this.value = '';
}
setAttribute(name, value) {
this.attributes[name] = String(value);
if (name === 'value') this.value = String(value);
if (name.startsWith('data-')) this.dataset[name.slice(5).replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = String(value);
}
getAttribute(name) { return this.attributes[name]; }
appendChild(node) { node.parentNode = this; this.children.push(node); return node; }
removeChild(node) { this.children.splice(this.children.indexOf(node), 1); node.parentNode = null; }
replaceChildren(...nodes) {
this.children.forEach((node) => { node.parentNode = null; }); this.children = [];
nodes.forEach((node) => this.appendChild(node));
}
addEventListener(name, listener) { this.listeners[name] = listener; }
trigger(name, extra) { return this.listeners[name] && this.listeners[name]({target: this,
preventDefault() {}, ...(extra || {})}); }
click() { return this.trigger('click'); }
focus() { this.focused = true; }
}
function descendants(node) {
return node.children.flatMap((child) => [child, ...descendants(child)]);
}
function byText(node, text) {
return descendants(node).find((item) => item.textContent === text);
}
async function flush() {
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
}
test('vanilla runtime mounts through the shell and renders safe Activity and Memory panels', async () => {
const calls = [];
const fetcher = async (url, init) => {
calls.push([url, init]);
if (url.endsWith('/capabilities')) return response(200, capability());
if (url.includes('/events?')) return response(200, {items: [event()], next: null});
if (url.endsWith('/memory')) return response(200, {items: [memory()], next: null});
if (url.includes('/memory/')) return response(200, memory({revision: 3}), '3');
throw new Error(`unexpected ${url}`);
};
const doc = {createElement: (tag) => new FakeNode(tag)};
const host = new FakeNode('main');
const runtime = runtimeApi.createWaveARuntime({document: doc, fetcher, expectedIdentity: IDENTITY,
conversationId: CONVERSATION, idempotencyKey: () => 'webui:memory:0001'});
await runtime.mount(host); await flush();
const root = host.children[0];
assert.equal(root.getAttribute('data-hux-version'), 'hux.v1');
assert(byText(root, 'What is happening'));
assert.equal(descendants(root).some((node) => /private|never render/.test(node.textContent) &&
!node.textContent.includes('[redacted]')), false);
assert(byText(root, 'tool_result: call-1'));
const tabs = descendants(root).filter((node) => node.getAttribute('role') === 'tab');
assert.deepEqual(tabs.map((tab) => tab.textContent), ['Activity', 'Memory']);
await tabs[1].click();
assert(byText(root, 'Memory control'));
assert(byText(root, 'Prefers concise answers token=[redacted]'));
assert(byText(root, 'The user asked password=[redacted]'));
await byText(root, 'Stop using in replies').click(); await flush();
const actionCall = calls.find(([url]) => url.endsWith('/memory/mem_0001aaaa/remove_retrieval'));
assert.equal(actionCall[1].headers['If-Match'], '2');
assert.equal(actionCall[1].method, 'POST');
runtime.destroy();
assert.equal(host.children.length, 0);
});
test('memory renderer sends idempotent proposals and If-Match edits without persistence', async () => {
const calls = [];
const client = {identity: IDENTITY, request: async (path, init) => {
calls.push([path, init]);
if (path === '/memory' && (!init || init.method !== 'POST')) return {body: {items: [memory()], next: null}};
return {body: memory({revision: 3})};
}};
const doc = {createElement: (tag) => new FakeNode(tag)};
const panel = new FakeNode('section');
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'webui:memory:0002'}).render({client, container: panel});
await flush();
const proposal = descendants(panel).find((node) => node.tagName === 'FORM' &&
node.getAttribute('class') === 'hux-wave-a__propose');
const textareas = descendants(proposal).filter((node) => node.tagName === 'TEXTAREA');
const input = descendants(proposal).find((node) => node.tagName === 'INPUT');
const select = descendants(proposal).find((node) => node.tagName === 'SELECT');
textareas[0].value = 'Remember token=private'; input.value = 'Because password=private'; select.value = 'instruction';
await proposal.trigger('submit'); await flush();
const proposed = calls.find(([path, init]) => path === '/memory' && init && init.method === 'POST');
assert.equal(proposed[1].headers['Idempotency-Key'], 'webui:memory:0002');
assert.deepEqual(proposed[1].body, {kind: 'instruction', content: 'Remember token=[redacted]',
reason: 'Because password=[redacted]', approval_mode: 'ask', conversation_id: CONVERSATION,
scope: {level: 'conversation', scope_id: CONVERSATION}});
const edit = descendants(panel).find((node) => node.tagName === 'FORM' &&
node.getAttribute('class') === 'hux-wave-a__edit');
descendants(edit).find((node) => node.tagName === 'TEXTAREA').value = 'Corrected secret=value';
edit.trigger('submit'); await flush();
const edited = calls.find(([path]) => path.endsWith('/edit'));
assert.equal(edited[1].headers['If-Match'], '2');
assert.deepEqual(edited[1].body, {content: 'Corrected secret=[redacted]'});
});
test('renderer failures stay generic, conflict is actionable, and helper branches are bounded', async () => {
assert.equal(runtimeApi.friendlyKind('run.completed'), 'Run completed');
assert.equal(runtimeApi.friendlyKind('new.kind'), 'new kind');
assert.deepEqual(runtimeApi.memoryActions({status: 'proposed'}), [['approve', 'Approve'], ['reject', 'Reject']]);
assert.deepEqual(runtimeApi.memoryActions({status: 'active', retrievable: false})[0], ['restore_retrieval', 'Use in replies']);
assert.deepEqual(runtimeApi.memoryActions({status: 'expired'}), [['forget', 'Forget']]);
assert.deepEqual(runtimeApi.memoryActions({status: 'forgotten'}), []);
assert.throws(() => runtimeApi.activityExtension({document: {}, conversationId: 'bad'}), /conversation/);
assert.throws(() => runtimeApi.memoryExtension({document: {}, conversationId: CONVERSATION}), /idempotency/);
assert.throws(() => runtimeApi.createWaveARuntime({expectedIdentity: IDENTITY,
fetcher: async () => response(404, {}), conversationId: CONVERSATION}), /document/);
const doc = {createElement: (tag) => new FakeNode(tag)};
const activityPanel = new FakeNode('section');
runtimeApi.activityExtension({document: doc, conversationId: CONVERSATION}).render({container: activityPanel,
client: {identity: IDENTITY, request: async () => { throw new Error('secret raw failure'); }}});
await flush();
assert(byText(activityPanel, 'Activity could not be verified. No event details were shown.'));
const panel = new FakeNode('section');
let status = 409;
const client = {identity: IDENTITY, request: async (path) => {
if (path === '/memory') return {body: {items: [memory({status: 'proposed', content: 'pending',
retrievable: false})], next: null}};
const error = new Error('raw secret'); error.status = status; throw error;
}};
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'webui:memory:0003'}).render({container: panel, client});
await flush(); await byText(panel, 'Approve').click(); await flush();
assert(byText(panel, 'This memory changed. Refresh before trying again.'));
status = 500;
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'bad'}).render({container: panel, client});
await flush();
const proposal = descendants(panel).find((node) => node.getAttribute('class') === 'hux-wave-a__propose');
descendants(proposal).find((node) => node.tagName === 'TEXTAREA').value = 'safe';
descendants(proposal).find((node) => node.tagName === 'INPUT').value = 'reason';
await proposal.trigger('submit'); await flush();
assert(byText(panel, 'The memory proposal could not be completed.'));
});
test('mount rejects duplicate starts and default idempotency keys satisfy the backend contract', async () => {
const doc = {createElement: (tag) => new FakeNode(tag)};
const host = new FakeNode('main');
let generatedKey = null;
const runtime = runtimeApi.createWaveARuntime({document: doc, expectedIdentity: IDENTITY,
conversationId: CONVERSATION, fetcher: async (url, init) => {
if (url.endsWith('/capabilities')) return response(200, capability());
if (url.includes('/events?')) return response(200, {items: [], next: null});
if (url.endsWith('/memory') && (!init || init.method !== 'POST')) {
return response(200, {items: [], next: null});
}
generatedKey = init.headers['Idempotency-Key'];
return response(201, memory({status: 'proposed', content: 'pending', retrievable: false}));
}});
await runtime.mount(host); await flush();
const root = host.children[0];
descendants(root).find((node) => node.getAttribute('role') === 'tab' && node.textContent === 'Memory').click();
const proposal = descendants(root).find((node) => node.getAttribute('class') === 'hux-wave-a__propose');
descendants(proposal).find((node) => node.tagName === 'TEXTAREA').value = 'remember this';
descendants(proposal).find((node) => node.tagName === 'INPUT').value = 'requested by user';
await proposal.trigger('submit'); await flush();
assert.match(generatedKey, /^webui:memory:[a-z0-9]+:[a-z0-9]+$/);
await assert.rejects(() => runtime.mount(host), /already mounted/);
runtime.destroy();
runtime.destroy();
});
test('activity renderer handles empty, plural-evidence, refresh, and stale loads', async () => {
const doc = {createElement: (tag) => new FakeNode(tag)};
const panel = new FakeNode('section');
const queue = [];
const client = {identity: IDENTITY, request: () => new Promise((resolve, reject) => queue.push({resolve, reject}))};
runtimeApi.activityExtension({document: doc, conversationId: CONVERSATION}).render({client, container: panel});
queue[0].resolve({body: {items: [], next: null}}); await flush();
assert(byText(panel, 'No activity has been recorded yet.'));
const refresh = byText(panel, 'Refresh activity');
refresh.click();
refresh.click();
byText(panel, 'Loading activity…');
// Resolve an older request after a newer refresh. Its private response must be discarded.
queue[2].resolve({body: {items: [event({evidence: [
{kind: 'source', id: 'src_0001aaaa'}, {kind: 'passage', id: 'psg_0001aaaa'},
]}), event({id: 'evt_0002aaaa', seq: 2, evidence: []})], next: null}});
await flush();
assert(byText(panel, '2 evidence references'));
queue[1].resolve({body: {items: [event({summary: 'stale secret'})], next: null}});
await flush();
assert.equal(descendants(panel).some((node) => node.textContent === 'stale secret'), false);
});
test('memory renderer covers every status, empty states, refresh, action failure, and empty edits', async () => {
const doc = {createElement: (tag) => new FakeNode(tag)};
const panel = new FakeNode('section');
const items = [
memory({id: 'mem_proposed1', status: 'proposed', content: 'pending', retrievable: false}),
memory({id: 'mem_hidden001', retrievable: false}),
memory({id: 'mem_expired01', status: 'expired', content: '', retrievable: false}),
memory({id: 'mem_rejected1', status: 'rejected', content: '', retrievable: false}),
memory({id: 'mem_nostore01', status: 'no_store', content: '', retrievable: false,
scope: {level: 'global'}}),
];
let empty = false;
const calls = [];
const client = {identity: IDENTITY, request: async (path, init) => {
calls.push([path, init]);
if (path === '/memory') return {body: {items: empty ? [] : items, next: null}};
const error = new Error('private body'); error.status = 500; throw error;
}};
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'webui:memory:0004'}).render({client, container: panel});
await flush();
assert(byText(panel, 'Reject'));
assert(byText(panel, 'Use in replies'));
assert.equal(descendants(panel).filter((node) => node.textContent === 'Forget').length, 2);
await byText(panel, 'Use in replies').click(); await flush();
assert(byText(panel, 'The memory change could not be completed.'));
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'webui:memory:0004'}).render({client, container: panel});
await flush();
const edit = descendants(panel).find((node) => node.getAttribute('class') === 'hux-wave-a__edit');
edit.trigger('submit'); await flush();
assert.equal(calls.filter(([path]) => path.endsWith('/edit')).length, 0);
empty = true;
await byText(panel, 'Refresh memory').click(); await flush();
assert(byText(panel, 'No memory entries are available.'));
});
test('memory load and malformed proposal failures never reveal raw responses', async () => {
const doc = {createElement: (tag) => new FakeNode(tag)};
const panel = new FakeNode('section');
let calls = 0;
const client = {identity: IDENTITY, request: async () => { calls += 1; throw new Error('token=private'); }};
runtimeApi.memoryExtension({document: doc, conversationId: CONVERSATION,
idempotencyKey: () => 'webui:memory:0005'}).render({client, container: panel});
await flush();
assert(byText(panel, 'Memory could not be verified. No saved details were shown.'));
assert.equal(descendants(panel).some((node) => node.textContent.includes('token=private')), false);
assert.equal(calls, 1);
assert.throws(() => runtimeApi.memoryExtension({document: doc, conversationId: 'bad',
idempotencyKey: () => 'webui:memory:0005'}), /conversation/);
});