'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/); });