atlas-iac/testing/tests/test_hermes_hux_runtime_autonomy_privacy_node.js

500 lines
31 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
const api = require('../../dockerfiles/hermes-webui-hux/runtime/autonomy-privacy.js');
const IDENTITY = Object.freeze({
tenantSlot: 'slot-3',
userRef: 'usr_0123456789abcdef',
surface: 'chat',
trust: 'relay',
});
const CONVERSATION = 'conv_alpha1234';
const RUN_ID = 'run_alpha1234';
const STAMP = '2026-08-24T10:00:00Z';
function policy(extra = {}) {
return {
schema: 'hux.policy.v1', id: 'pol_conversation.alpha', owner: IDENTITY.userRef,
scope: {level: 'conversation', scope_id: CONVERSATION}, autonomy: 'safe',
grants: [{capability: 'network', decision: 'ask', expires_at: '2026-08-25T10:00:00Z',
granted_by: {type: 'user', id: IDENTITY.userRef}}],
budgets: {tokens_per_run: 1000, tool_calls_per_run: 10, wall_clock_seconds: 60,
delegations_per_run: 2, spend_units: 5, subagents_per_run: 1,
scope: {conversations: [CONVERSATION], paths: ['/workspace']}},
provenance: {surface: 'chat'}, updated_at: STAMP, revision: 1, ...extra,
};
}
function approval(extra = {}) {
return {
schema: 'hux.approval.v1', id: 'apr_alpha1234', run_id: RUN_ID,
conversation_id: CONVERSATION, capability: 'send_message',
request: {summary: 'Send the finished report', detail: 'private details', risk: 'high',
external: true, evidence: []}, status: 'pending', requested_at: STAMP,
expires_at: '2026-08-25T10:00:00Z', idempotency_key: 'approval:key', ...extra,
};
}
function approvalPage(items = [approval()], extra = {}) {
return {items, next: null, ...extra};
}
const TOPICS = ['health', 'finance', 'legal', 'relationships', 'credentials', 'minors', 'location', 'biometric'];
function privacyPolicy(extra = {}) {
return {
schema: 'hux.privacy_policy.v1', version: 1,
topics: TOPICS.map((topic, index) => ({topic,
sensitivity: index < 4 ? 'sensitive' : 'restricted',
memory_write: index < 4 ? 'ask' : 'deny', decay_days: index + 1,
notice: `Generic ${topic} notice`})),
topic_scoping: {scope_to_conversation: true, cross_surface_sharing: 'never'},
retention_audit: {interval_days: 1,
actions: ['expire_memory', 'decay_topic_context', 'purge_forgotten_content', 'report']},
...extra,
};
}
function audit(extra = {}) {
return {schema: 'hux.retention_audit.v1', id: 'aud_alpha1234', ran_at: STAMP,
policy_version: 1, results: [
{action: 'expire_memory', count: 2},
{action: 'decay_topic_context', count: 1},
{action: 'purge_forgotten_content', count: 0},
{action: 'report', count: 1},
], ...extra};
}
function notice(extra = {}) {
return {schema: 'hux.privacy_notice.v1', topic: 'health',
conversation_id: CONVERSATION, text: 'Private health category text',
controls: ['forget_this_conversation', 'switch_to_private', 'disable_memory_here', 'dismiss'],
shown_at: STAMP, ...extra};
}
function receipt(extra = {}) {
return {schema: 'hux.cancel_receipt.v1', id: 'rcpt_alpha1234', run_id: RUN_ID,
requested_by: {type: 'user', id: IDENTITY.userRef}, requested_at: STAMP,
acknowledged_at: STAMP, completed_at: STAMP, outcome: 'cancelled',
side_effects: [{description: 'Draft file removed', reverted: true,
evidence: {kind: 'receipt', id: 'receipt-1'}},
{description: 'Message was already delivered', reverted: false}],
conversation_id: CONVERSATION,
...extra};
}
function response(status, body, headers = {}) {
const lower = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
return {status, ok: status >= 200 && status < 300,
headers: {get: (name) => lower[name.toLowerCase()] || null}, json: async () => body};
}
class FakeNode {
constructor(tag) { this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = [];
this.listeners = {}; this.textContent = ''; this.parentNode = null; this.selected = false; this.value = '';
this.dataset = {}; this.hidden = false; }
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, (_, c) => c.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 = []; nodes.forEach((node) => this.appendChild(node)); }
addEventListener(name, listener) { this.listeners[name] = listener; }
click() { if (this.listeners.click) this.listeners.click({target: this}); }
focus() { this.focused = true; }
}
const document = {createElement: (tag) => new FakeNode(tag)};
function descendants(node) { return node.children.flatMap((child) => [child, ...descendants(child)]); }
function findText(node, value) { return descendants(node).find((item) => item.textContent === value); }
function allText(node) { return [node, ...descendants(node)].map((item) => item.textContent).join(' '); }
function ticks() { return new Promise((resolve) => setImmediate(() => setImmediate(resolve))); }
function harness(initial = [], flags = ['hux.foundation', 'hux.autonomy', 'hux.privacy']) {
const responses = initial.slice(); const calls = []; const enabled = new Set(flags);
const client = {apiVersion: 'hux.v1', identity: IDENTITY,
enabled: (flag) => enabled.has(flag), endpoint: (path) => `/hux/v1${path}`};
const fetcher = async (url, init) => { calls.push({url, init});
const result = responses.shift(); if (!result) throw new Error('unexpected request'); return result; };
return {calls, client, responses, fetcher};
}
test('canonical records normalize to bounded display-only autonomy views', () => {
const item = api.normalizePolicy(policy(), IDENTITY, CONVERSATION, '"1"');
assert.equal(item.autonomy, 'safe');
assert.equal(item.ifMatch, 1);
assert.equal(item.grants[0].capability, 'network');
assert.equal('provenance' in item, false);
assert.deepEqual(item.budgets.scope, {conversations: [CONVERSATION], paths: ['/workspace']});
const inherited = api.normalizePolicy(policy({id: 'pol_global', scope: {level: 'global'}}),
IDENTITY, CONVERSATION, '1');
assert.equal(inherited.ifMatch, 0);
assert.deepEqual(inherited.scope, {level: 'conversation', scope_id: CONVERSATION});
assert.equal(api.normalizePolicy(policy({id: 'pol_project.alpha',
scope: {level: 'project', scope_id: 'prj_alpha1234'}}), IDENTITY, CONVERSATION, '1').ifMatch, 0);
assert.equal(api.normalizePolicy(policy({grants: [{capability: 'shell', decision: 'deny'}]}),
IDENTITY, CONVERSATION, '1').grants[0].expires_at, undefined);
assert.equal(api.normalizeApproval(approval(), CONVERSATION, new Set(['pending'])).summary,
'Send the finished report');
assert.deepEqual(api.normalizeApprovalPage(approvalPage(), CONVERSATION).map((row) => row.id),
['apr_alpha1234']);
const stopped = api.normalizeReceipt(receipt(), RUN_ID, CONVERSATION, IDENTITY);
assert.deepEqual(stopped.sideEffects, [{description: 'Draft file removed', reverted: true},
{description: 'Message was already delivered', reverted: false}]);
});
test('policy and approval validators reject identity, scope, schema and unsafe payloads', () => {
const invalidPolicies = [null, {...policy(), extra: true}, {...policy(), schema: 'old'},
{...policy(), id: 'bad'}, {...policy(), owner: 'usr_aaaaaaaaaaaaaaaa'},
{...policy(), autonomy: 'reckless'}, {...policy(), updated_at: 'today'},
{...policy(), revision: 0}, {...policy(), scope: null},
{...policy(), scope: {level: 'global'}}, {...policy(), provenance: null},
{...policy(), id: 'pol_global', scope: {level: 'global', scope_id: CONVERSATION}},
{...policy(), id: 'pol_project.alpha', scope: {level: 'project', scope_id: 'bad'}},
{...policy(), grants: null}, {...policy(), grants: Array(65).fill(policy().grants[0])},
{...policy(), grants: [null]}, {...policy(), grants: [{capability: 'unknown', decision: 'ask'}]},
{...policy(), grants: [{capability: 'network', decision: 'maybe'}]},
{...policy(), grants: [{capability: 'network', decision: 'ask', expires_at: 'later'}]},
{...policy(), budgets: null}, {...policy(), budgets: {tokens_per_run: -1}},
{...policy(), budgets: {surprise: 1}},
{...policy(), budgets: {scope: null}},
{...policy(), budgets: {scope: {conversations: ['bad']}}},
{...policy(), budgets: {scope: {paths: ['']}}}];
invalidPolicies.forEach((raw) => assert.equal(api.normalizePolicy(raw, IDENTITY, CONVERSATION, '1'), null));
assert.equal(api.normalizePolicy(policy(), IDENTITY, CONVERSATION, '2'), null);
const invalidApprovals = [null, {...approval(), extra: true}, {...approval(), schema: 'old'},
{...approval(), id: 'bad'}, {...approval(), run_id: 'bad/run'},
{...approval(), conversation_id: 'conv_other1234'}, {...approval(), capability: 'unknown'},
{...approval(), status: 'approved'}, {...approval(), requested_at: 'today'},
{...approval(), expires_at: 'today'}, {...approval(), request: null},
{...approval(), request: {...approval().request, summary: ''}},
{...approval(), request: {...approval().request, risk: 'extreme'}},
{...approval(), request: {...approval().request, external: 'yes'}}];
invalidApprovals.forEach((raw) => assert.equal(
api.normalizeApproval(raw, CONVERSATION, new Set(['pending'])), null));
assert.equal(api.normalizeApprovalPage(null, CONVERSATION), null);
assert.equal(api.normalizeApprovalPage({items: Array(201).fill(approval()), next: null}, CONVERSATION), null);
assert.equal(api.normalizeApprovalPage({items: [approval(), approval()], next: null}, CONVERSATION), null);
assert.equal(api.normalizeApprovalPage({items: [], next: 'cursor'}, CONVERSATION), null);
const resolved = approval({status: 'approved', decision: {choice: 'once',
by: {type: 'user', id: IDENTITY.userRef}, at: STAMP}});
assert.equal(api.normalizeApproval(resolved, CONVERSATION, new Set(['approved']), 'once', IDENTITY).status,
'approved');
assert.equal(api.normalizeApproval({...resolved, decision: {...resolved.decision, choice: 'always'}},
CONVERSATION, new Set(['approved']), 'once', IDENTITY), null);
assert.equal(api.normalizeApproval({...resolved, decision: {...resolved.decision,
by: {type: 'user', id: 'usr_aaaaaaaaaaaaaaaa'}}}, CONVERSATION,
new Set(['approved']), 'once', IDENTITY), null);
assert.equal(api.normalizeApproval({...approval(), decision: resolved.decision},
CONVERSATION, new Set(['pending'])), null);
});
test('receipt validation proves run and side-effect outcome', () => {
const invalid = [null, {...receipt(), extra: true}, {...receipt(), schema: 'old'},
{...receipt(), id: 'bad'}, {...receipt(), run_id: 'other'}, {...receipt(), requested_by: null},
{...receipt(), requested_at: 'today'}, {...receipt(), outcome: 'requested'},
{...receipt(), conversation_id: 'conv_other1234'}, {...receipt(), side_effects: null},
{...receipt(), side_effects: Array(65).fill(receipt().side_effects[0])},
{...receipt(), side_effects: [null]},
{...receipt(), side_effects: [{description: '', reverted: true}]},
{...receipt(), side_effects: [{description: 'x', reverted: 'yes'}]}];
invalid.forEach((raw) => assert.equal(api.normalizeReceipt(raw, RUN_ID, CONVERSATION, IDENTITY), null));
assert.equal(api.normalizeReceipt({...receipt(), conversation_id: undefined}, RUN_ID, CONVERSATION, IDENTITY).outcome,
'cancelled');
assert.equal(api.normalizeReceipt(receipt(), RUN_ID, CONVERSATION, null), null);
assert.equal(api.normalizeReceipt({...receipt(), requested_by: {type: 'system', id: 'worker'}},
RUN_ID, CONVERSATION, {...IDENTITY, surface: 'worker'}).outcome, 'cancelled');
});
test('privacy and audit adapters omit topic details and reject malformed policy', () => {
const item = api.normalizePrivacyPolicy(privacyPolicy(), 'false');
assert.deepEqual(item, {version: 1, protectedCount: 8, shortestDecay: 1,
longestDecay: 8, crossSurfaceSharing: 'never', auditIntervalDays: 1, auditStale: false});
assert.equal(api.normalizePrivacyPolicy(privacyPolicy(), 'true').auditStale, true);
const invalid = [null, {...privacyPolicy(), extra: true}, {...privacyPolicy(), schema: 'old'},
{...privacyPolicy(), version: 0}, {...privacyPolicy(), topics: []},
{...privacyPolicy(), topics: [...privacyPolicy().topics.slice(0, 7), privacyPolicy().topics[0]]},
{...privacyPolicy(), topics: privacyPolicy().topics.map((row, i) => i ? row : {...row, sensitivity: 'personal'})},
{...privacyPolicy(), topics: privacyPolicy().topics.map((row, i) => i ? row : {...row, memory_write: 'always'})},
{...privacyPolicy(), topics: privacyPolicy().topics.map((row, i) => i ? row : {...row, decay_days: 0})},
{...privacyPolicy(), topics: privacyPolicy().topics.map((row, i) => i ? row : {...row, notice: ''})},
{...privacyPolicy(), topic_scoping: null},
{...privacyPolicy(), topic_scoping: {scope_to_conversation: false, cross_surface_sharing: 'never'}},
{...privacyPolicy(), retention_audit: null},
{...privacyPolicy(), retention_audit: {interval_days: 31, actions: ['report']}},
{...privacyPolicy(), retention_audit: {interval_days: 1, actions: []}}];
invalid.forEach((raw) => assert.equal(api.normalizePrivacyPolicy(raw, 'false'), null));
const view = api.normalizeNotice(notice(), CONVERSATION);
assert.equal(view.text, api.GENERIC_NOTICE);
assert.equal(JSON.stringify(view).includes('health'), false);
assert.equal(api.normalizeNotice({...notice(), topic: 'unknown'}, CONVERSATION), null);
assert.equal(api.normalizeNotice({...notice(), conversation_id: 'conv_other1234'}, CONVERSATION), null);
assert.equal(api.normalizeNotice({...notice(), controls: ['unknown']}, CONVERSATION), null);
});
test('audit and forget views retain only operational counts', () => {
assert.deepEqual(api.normalizeAuditPage({items: [audit()], next: null}), [{id: 'aud_alpha1234',
ranAt: STAMP, policyVersion: 1, actions: 4, affected: 3}]);
const badAudits = [null, {items: Array(201).fill(audit()), next: null},
{items: [null], next: null}, {items: [{...audit(), schema: 'old'}], next: null},
{items: [{...audit(), id: 'bad'}], next: null},
{items: [{...audit(), ran_at: 'today'}], next: null},
{items: [{...audit(), policy_version: 0}], next: null},
{items: [{...audit(), results: [null]}], next: null},
{items: [{...audit(), results: [{action: 'delete', count: 1}]}], next: null},
{items: [{...audit(), results: [{action: 'report', count: -1}]}], next: null}];
badAudits.forEach((raw) => assert.equal(api.normalizeAuditPage(raw), null));
assert.deepEqual(api.normalizeForget({conversation_id: CONVERSATION, forgotten: true,
memory_forgotten: 2, events_redacted: 4}, CONVERSATION),
{forgotten: true, memoryForgotten: 2, eventsRedacted: 4});
assert.equal(api.normalizeForget({conversation_id: CONVERSATION, forgotten: false,
memory_forgotten: 0, events_redacted: 0}, CONVERSATION), null);
});
test('extensions register with the HUX shell and validate runtime scope', () => {
const runtime = api.createRuntime({document, conversationId: CONVERSATION,
fetcher: async () => response(500, {})});
assert.deepEqual(runtime.extensions.map((item) => [item.id, item.flag]), [
['autonomy-controls', 'hux.autonomy'], ['privacy-controls', 'hux.privacy']]);
const registered = []; const removed = [];
const remove = runtime.register({register(extension) { registered.push(extension.id);
return () => removed.push(extension.id); }});
assert.deepEqual(registered, ['autonomy-controls', 'privacy-controls']);
remove(); assert.deepEqual(removed, ['privacy-controls', 'autonomy-controls']);
assert.throws(() => runtime.register(null), /shell/);
assert.throws(() => api.createRuntime({document, conversationId: 'bad', fetcher() {}}), /conversation/);
assert.throws(() => api.createRuntime({document, conversationId: CONVERSATION, runId: 'bad/run', fetcher() {}}), /run id/);
assert.throws(() => api.createRuntime({document: null, conversationId: CONVERSATION, fetcher() {}}), /document/);
const originalFetch = globalThis.fetch;
try { globalThis.fetch = undefined;
assert.throws(() => api.createRuntime({document, conversationId: CONVERSATION}), /fetch/);
} finally { globalThis.fetch = originalFetch; }
assert.throws(() => api.createRuntime(), /document/);
});
test('canonical autonomy/privacy runtime negotiates tenantSlot identity and mounts both cards', async () => {
const rawIdentity = {tenant_slot: IDENTITY.tenantSlot, subject: IDENTITY.userRef,
surface: IDENTITY.surface, trust: IDENTITY.trust};
const cards = [
['HUX-11', 'hux.foundation', ['/hux/v1/capabilities']],
['HUX-01', 'hux.activity_timeline', ['/hux/v1/conversations/{id}/events']],
['HUX-05', 'hux.autonomy', ['/hux/v1/policy']],
['HUX-10', 'hux.privacy', ['/hux/v1/privacy/policy']],
].map(([card, flag, routes]) => ({card, flag, enabled: true, routes}));
const calls = [];
const fetcher = async (url, init) => {
calls.push({url, init});
if (url === '/hux/v1/capabilities') return response(200, {schema: 'hux.capabilities.v1',
contract_version: '1.1.0', identity: rawIdentity, cards});
if (url.startsWith('/hux/v1/policy?')) return response(200, policy(), {ETag: '1'});
if (url === '/hux/v1/approvals?status=pending') return response(200, approvalPage([]));
if (url === '/hux/v1/privacy/policy') return response(200, privacyPolicy());
if (url === '/hux/v1/privacy/audit') return response(200, {items: [], next: null});
throw new Error(`unexpected ${url}`);
};
const runtime = api.createAutonomyPrivacyRuntime({document, conversationId: CONVERSATION,
expectedIdentity: IDENTITY, fetcher});
const root = new FakeNode('section');
await runtime.mount(root); await ticks();
assert.equal(runtime.client.identity.tenantSlot, 'slot-3');
assert.equal(runtime.client.identity.trust, 'relay');
assert.match(allText(root), /Autonomy and approvals/);
assert.match(allText(root), /Privacy and retention/);
assert.equal(calls.filter((call) => call.url === '/hux/v1/capabilities').length, 1);
runtime.destroy(); runtime.destroy(); assert.equal(root.children.length, 0);
await assert.rejects(() => runtime.mount(root), /destroyed/);
assert.throws(() => api.createAutonomyPrivacyRuntime({document, conversationId: CONVERSATION,
expectedIdentity: {...IDENTITY, tenantSlot: 'tenant'}, fetcher}), /identity/);
});
test('browser script exports the vanilla runtime namespace', () => {
const context = {globalThis: {HermesHuxWaveAContract: {}, HermesHuxShell: {}}};
const source = fs.readFileSync(path.join(__dirname,
'../../dockerfiles/hermes-webui-hux/runtime/autonomy-privacy.js'), 'utf8');
vm.runInNewContext(source, context);
assert.equal(typeof context.globalThis.HermesHuxAutonomyPrivacy.createRuntime, 'function');
assert.equal(typeof context.globalThis.HermesHuxAutonomyPrivacy.createAutonomyPrivacyRuntime, 'function');
});
test('autonomy renderer reads only and never approves before a click', async () => {
const h = harness([response(200, policy(), {ETag: '1'}), response(200, approvalPage())]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, runId: RUN_ID, fetcher: h.fetcher,
canStopModelResponse: () => true, stopModelResponse: async () => ({accepted: true})});
const root = new FakeNode('section'); runtime.extensions[0].render({client: h.client, container: root});
await ticks();
assert.equal(h.calls.length, 2); assert(h.calls.every((call) => call.init.method === 'GET'));
assert.match(allText(root), /Pending approvals \(1\)/);
assert.equal(allText(root).includes('private details'), false);
assert(findText(root, 'Allow once')); assert(findText(root, 'Stop model response'));
h.calls.forEach((call) => { assert.equal(call.init.credentials, 'same-origin');
assert.equal(call.init.cache, 'no-store'); assert.equal(call.init.headers.Accept.includes('version=1'), true); });
});
test('policy update uses If-Match and approval choice uses idempotency', async () => {
const h = harness([response(200, policy(), {ETag: '1'}), response(200, approvalPage()),
response(200, policy({autonomy: 'autonomous', revision: 2}), {ETag: '2'}),
response(200, approval({status: 'approved', decision: {choice: 'once', by: {type: 'user', id: IDENTITY.userRef}, at: STAMP}}))]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, fetcher: h.fetcher});
const root = new FakeNode('section'); runtime.extensions[0].render({client: h.client, container: root}); await ticks();
const select = descendants(root).find((node) => node.tagName === 'SELECT'); select.value = 'autonomous';
findText(root, 'Save autonomy').click(); await ticks();
assert.equal(h.calls[2].init.headers['If-Match'], '1');
assert.equal(JSON.parse(h.calls[2].init.body).autonomy, 'autonomous');
findText(root, 'Allow once').click(); await ticks();
assert.match(h.calls[3].init.headers['Idempotency-Key'], /^hux:approval-once:/);
assert.deepEqual(JSON.parse(h.calls[3].init.body), {choice: 'once'});
assert.match(allText(root), /Pending approvals \(0\)/);
await assert.rejects(runtime.decideApproval('apr_alpha1234', 'once'), /presented/);
await assert.rejects(runtime.savePolicy('reckless'), /invalid/);
});
test('an inherited global policy creates a conversation override with If-Match zero', async () => {
const inherited = policy({id: 'pol_global', scope: {level: 'global'}});
const created = policy({autonomy: 'ask_first', revision: 1});
const h = harness([response(200, inherited, {ETag: '1'}), response(200, approvalPage([])),
response(200, created, {ETag: '1'})]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, fetcher: h.fetcher});
runtime.extensions[0].render({client: h.client, container: new FakeNode('section')}); await ticks();
await runtime.savePolicy('ask_first');
assert.equal(h.calls[2].init.headers['If-Match'], '0');
assert.deepEqual(JSON.parse(h.calls[2].init.body).scope,
{level: 'conversation', scope_id: CONVERSATION});
});
test('stop is not complete until a validated receipt is rendered', async () => {
const h = harness([response(200, policy(), {ETag: '1'}), response(200, approvalPage([])),
response(201, receipt({side_effects: [{description: 'Draft file removed', reverted: true}]}))]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, runId: RUN_ID, fetcher: h.fetcher,
canStopModelResponse: () => true, stopModelResponse: async () => ({accepted: true})});
const root = new FakeNode('section'); runtime.extensions[0].render({client: h.client, container: root}); await ticks();
findText(root, 'Stop model response').click(); await ticks();
assert.match(h.calls[2].init.headers['Idempotency-Key'], /^hux:stop:/);
assert.deepEqual(JSON.parse(h.calls[2].init.body), {conversation_id: CONVERSATION, process_registry_empty: false});
assert.match(allText(root), /Stop receipt/); assert.match(allText(root), /Outcome: cancelled/);
assert.match(allText(root), /Reverted: Draft file removed/);
assert.match(allText(root), /model response and recorded side effects stopped/i);
});
test('privacy renderer is generic and suppresses topic replay', async () => {
const h = harness([response(200, privacyPolicy(), {'HUX-Audit-Stale': 'false'}),
response(200, {items: [audit()], next: null}), response(201, notice())]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, fetcher: h.fetcher});
const root = new FakeNode('section'); runtime.extensions[1].render({client: h.client, container: root}); await ticks();
assert.match(allText(root), /Privacy and retention/); assert.match(allText(root), /prior sensitive details are not repeated/);
await runtime.showPrivacyNotice('health');
assert.match(allText(root), /Sensitive-topic protection/); assert.equal(allText(root).includes('health'), false);
const before = h.calls.length; assert.equal(await runtime.showPrivacyNotice('finance'), null);
assert.equal(h.calls.length, before); runtime.clearPrivacyNotice();
assert.equal(allText(root).includes('Sensitive-topic protection'), false);
assert.equal(await runtime.showPrivacyNotice('unknown'), null);
});
test('privacy controls are explicit, idempotent and minimize results', async () => {
let privateCalls = 0;
const h = harness([response(200, privacyPolicy(), {'HUX-Audit-Stale': 'true'}),
response(200, {items: [], next: null}), response(201, notice()),
response(201, notice())]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, fetcher: h.fetcher,
onPrivateMode: async () => { privateCalls += 1; }});
const root = new FakeNode('section'); runtime.extensions[1].render({client: h.client, container: root}); await ticks();
await runtime.showPrivacyNotice('health'); await runtime.privacyChoice('switch_to_private');
assert.equal(privateCalls, 1); assert.match(h.calls[3].init.headers['Idempotency-Key'], /privacy-switch_to_private/);
assert.equal(allText(root).includes('health'), false); assert.match(allText(root), /Private mode was requested/);
const forgetHarness = harness([response(200, privacyPolicy()), response(200, {items: [], next: null}),
response(201, notice()), response(200, {conversation_id: CONVERSATION, forgotten: true,
memory_forgotten: 3, events_redacted: 7})]);
const forgetting = api.createRuntime({document, conversationId: CONVERSATION, fetcher: forgetHarness.fetcher});
const forgetRoot = new FakeNode('section'); forgetting.extensions[1].render({client: forgetHarness.client, container: forgetRoot}); await ticks();
await forgetting.showPrivacyNotice('health'); await forgetting.privacyChoice('forget_this_conversation');
assert.match(forgetHarness.calls[3].init.headers['Idempotency-Key'], /^hux:forget:/);
assert.match(allText(forgetRoot), /3 memory entries removed/);
const memoryHarness = harness([response(200, privacyPolicy()), response(200, {items: [], next: null}),
response(201, notice()), response(201, notice())]);
const memory = api.createRuntime({document, conversationId: CONVERSATION, fetcher: memoryHarness.fetcher});
const memoryRoot = new FakeNode('section'); memory.extensions[1].render({client: memoryHarness.client, container: memoryRoot}); await ticks();
await memory.showPrivacyNotice('health'); await memory.privacyChoice('disable_memory_here');
assert.match(allText(memoryRoot), /Memory is disabled/);
const dismissHarness = harness([response(200, privacyPolicy()), response(200, {items: [], next: null}), response(201, notice())]);
const dismissing = api.createRuntime({document, conversationId: CONVERSATION, fetcher: dismissHarness.fetcher});
const dismissRoot = new FakeNode('section'); dismissing.extensions[1].render({client: dismissHarness.client, container: dismissRoot}); await ticks();
await dismissing.showPrivacyNotice('health'); assert.equal(await dismissing.privacyChoice('dismiss'), null);
assert.match(allText(dismissRoot), /notice dismissed/);
const localHarness = harness([response(200, privacyPolicy()), response(200, {items: [], next: null}),
response(201, notice()), response(201, notice())]);
const local = api.createRuntime({document, conversationId: CONVERSATION, fetcher: localHarness.fetcher});
const localRoot = new FakeNode('section'); local.extensions[1].render({client: localHarness.client,
container: localRoot}); await ticks();
await local.showPrivacyNotice('health'); await local.privacyChoice('switch_to_private');
assert.match(allText(localRoot), /Private mode was requested/);
});
test('renderers fail closed with safe errors and reject unpresented actions', async () => {
const badClient = {...harness([]).client, apiVersion: 'hux.v0'};
const runtime = api.createRuntime({document, conversationId: CONVERSATION,
fetcher: async () => response(500, {secret: 'must not render'})});
assert.throws(() => runtime.extensions[0].render({client: badClient, container: new FakeNode('div')}), /capability/);
const root = new FakeNode('section'); runtime.extensions[0].render({client: harness([]).client, container: root}); await ticks();
assert.match(allText(root), /temporarily unavailable/); assert.equal(allText(root).includes('secret'), false);
await assert.rejects(runtime.decideApproval('apr_fake1234', 'once'), /presented/);
await assert.rejects(runtime.privacyChoice('dismiss'), /not presented/);
const privacyRoot = new FakeNode('section'); runtime.extensions[1].render({client: harness([]).client,
container: privacyRoot}); await ticks();
assert.match(allText(privacyRoot), /temporarily unavailable/);
});
test('event handlers contain failed mutations without claiming success', async () => {
const h = harness([response(200, policy(), {ETag: '1'}), response(200, approvalPage()),
response(200, policy({revision: 1}), {ETag: '1'}), response(200, approval()),
response(200, {...receipt(), run_id: 'other'})]);
const runtime = api.createRuntime({document, conversationId: CONVERSATION, runId: RUN_ID, fetcher: h.fetcher,
canStopModelResponse: () => true, stopModelResponse: async () => ({accepted: true})});
const root = new FakeNode('section'); runtime.extensions[0].render({client: h.client, container: root}); await ticks();
findText(root, 'Save autonomy').click(); await ticks();
findText(root, 'Allow once').click(); await ticks();
findText(root, 'Stop model response').click(); await ticks();
assert.match(allText(root), /tool\/side-effect cancellation unverified/);
const ph = harness([response(200, privacyPolicy({topic_scoping: {
scope_to_conversation: true, cross_surface_sharing: 'same_owner_only'}})),
response(200, {items: [], next: null}), response(201, notice()), response(200, {})]);
const pr = api.createRuntime({document, conversationId: CONVERSATION, fetcher: ph.fetcher});
const privacyRoot = new FakeNode('section'); pr.extensions[1].render({client: ph.client, container: privacyRoot}); await ticks();
await pr.showPrivacyNotice('health'); findText(privacyRoot, 'disable memory here').click(); await ticks();
assert.match(allText(privacyRoot), /temporarily unavailable/);
const badNotice = harness([response(200, privacyPolicy()), response(200, {items: [], next: null}), response(201, {})]);
const nr = api.createRuntime({document, conversationId: CONVERSATION, fetcher: badNotice.fetcher});
nr.extensions[1].render({client: badNotice.client, container: new FakeNode('section')}); await ticks();
await assert.rejects(nr.showPrivacyNotice('health'), /Invalid privacy notice/);
badNotice.responses.push(response(201, notice()));
assert.ok(await nr.showPrivacyNotice('health'));
const noHeaders = harness([]);
noHeaders.responses.push({status: 200, ok: true, json: async () => policy()});
noHeaders.responses.push(response(200, approvalPage([])));
const noHeaderRuntime = api.createRuntime({document, conversationId: CONVERSATION,
fetcher: noHeaders.fetcher});
const noHeaderRoot = new FakeNode('section'); noHeaderRuntime.extensions[0].render({client: noHeaders.client,
container: noHeaderRoot}); await ticks();
assert.match(allText(noHeaderRoot), /temporarily unavailable/);
const plainApproval = harness([response(200, policy(), {ETag: '1'}),
response(200, approvalPage([approval({request: {...approval().request, external: false}})]))]);
const plainRuntime = api.createRuntime({document, conversationId: CONVERSATION,
fetcher: plainApproval.fetcher});
const plainRoot = new FakeNode('section'); plainRuntime.extensions[0].render({client: plainApproval.client,
container: plainRoot}); await ticks();
assert.equal(allText(plainRoot).includes('external'), false);
});