atlas-iac/testing/tests/test_hermes_hux_ui_foundation_node.js
jenkins b66c762f5d hermes(webui): add HUX card UI models with contract-locked suites
Standalone per-card browser model/security/view modules for HUX-01..10
plus node+pytest suites that read the hux.v1 contract schemas directly.
Reconciled drift found on integration: the activity model now accepts
all 32 hux.event.v1 kinds (delegation.*, memory.suppressed,
memory.retrieval_removed, budget.exhausted, side_effect.*), the
autonomy model carries the external_side_effect capability, and the
foundation boundary test now asserts the shipped static HUX surface
exists on disk and that images never bake activated HUX_FLAGS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 04:12:03 -03:00

277 lines
14 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
const path = require('node:path');
const vm = require('node:vm');
const ROOT = path.resolve(__dirname, '..', '..');
const foundation = require(path.join(ROOT, 'dockerfiles/hermes-webui-hux/foundation.js'));
const shellApi = require(path.join(ROOT, 'dockerfiles/hermes-webui-hux/shell.js'));
const IDENTITY = Object.freeze({
tenant_ref: 'tnt_0123456789abcdef',
user_ref: 'usr_0123456789abcdef',
surface: 'chat',
});
function capability(flags, identity = IDENTITY) {
return {schema: 'hux.capabilities.v1', api_version: 'hux.v1', identity, flags};
}
function response(status, body) {
return {status, ok: status >= 200 && status < 300, json: async () => body};
}
test('capability negotiation is versioned, dependency-aware, and immutable', () => {
const result = foundation.normalizeCapabilities(capability([
'hux.foundation', 'hux.activity_timeline', 'hux.autonomy', 'hux.multimodal', 'hux.bogus',
]), foundation.normalizeIdentity(IDENTITY));
assert.deepEqual(result.flags, ['hux.activity_timeline', 'hux.autonomy', 'hux.foundation']);
assert.equal(result.apiVersion, 'hux.v1');
assert(Object.isFrozen(result));
assert(Object.isFrozen(result.identity));
assert.throws(() => foundation.normalizeCapabilities({}, result.identity), /Unsupported/);
assert.throws(() => foundation.normalizeCapabilities([], result.identity), /not an object/);
assert.throws(() => foundation.normalizeCapabilities(capability('bad'), result.identity), /flags/);
assert.throws(() => foundation.normalizeCapabilities(capability([], {...IDENTITY, surface: 'api'}), result.identity), /does not match/);
});
test('browser script path exports only the versioned namespace', () => {
const context = {globalThis: {}};
vm.runInNewContext(fs.readFileSync(path.join(ROOT, 'dockerfiles/hermes-webui-hux/foundation.js'), 'utf8'), context);
assert.equal(context.globalThis.HermesHuxFoundation.API_VERSION, 'hux.v1');
context.globalThis.HermesHuxFoundation = context.globalThis.HermesHuxFoundation;
vm.runInNewContext(fs.readFileSync(path.join(ROOT, 'dockerfiles/hermes-webui-hux/shell.js'), 'utf8'), context);
assert.equal(typeof context.globalThis.HermesHuxShell.createShell, 'function');
});
test('identity accepts only opaque tenant and user references', () => {
assert.deepEqual(foundation.normalizeIdentity(IDENTITY), {
tenantRef: IDENTITY.tenant_ref, userRef: IDENTITY.user_ref, surface: 'chat',
});
assert.throws(() => foundation.normalizeIdentity(null), /Missing/);
assert.throws(() => foundation.normalizeIdentity({...IDENTITY, tenant_ref: 'tenant-0'}), /tenant/);
assert.throws(() => foundation.normalizeIdentity({...IDENTITY, user_ref: 'brad@example.com'}), /user/);
assert.throws(() => foundation.normalizeIdentity({...IDENTITY, surface: 'browser'}), /surface/);
});
test('client stays off for 404 and reports safe states without response bodies', async () => {
const calls = [];
let result = response(404, {});
const client = foundation.createClient({
expectedIdentity: IDENTITY,
fetcher: async (...args) => { calls.push(args); return result; },
});
const states = [];
const unsubscribe = client.subscribe((state) => states.push(state.phase));
assert.equal(client.getState().phase, 'disabled');
await client.negotiate();
assert.deepEqual(states, ['disabled', 'loading', 'disabled']);
assert.equal(calls[0][0], '/hux/v1/capabilities');
assert.equal(calls[0][1].credentials, 'same-origin');
assert.equal(client.enabled('hux.foundation'), false);
result = response(200, capability([]));
await client.negotiate();
assert.equal(client.getState().phase, 'disabled');
result = response(500, {secret: 'must not escape'});
await client.negotiate();
assert.equal(client.getState().phase, 'error');
assert.equal(client.getState().error.includes('secret'), false);
unsubscribe();
});
test('client enables verified flags and rejects unsafe endpoint construction', async () => {
const client = foundation.createClient({
expectedIdentity: IDENTITY,
baseUrl: '/hux/v1/',
fetcher: async () => response(200, capability(['hux.foundation', 'hux.projects'])),
});
await client.negotiate();
assert.equal(client.getState().phase, 'ready');
assert.equal(client.enabled('hux.projects'), true);
assert.equal(client.enabled('hux.bogus'), false);
assert.equal(client.endpoint('/projects'), '/hux/v1/projects');
assert.throws(() => client.endpoint('https://evil.example/x'), /scoped/);
assert.throws(() => client.endpoint('//evil.example/x'), /scoped/);
assert.throws(() => client.endpoint('/../other-tenant'), /scoped/);
assert.throws(() => foundation.createClient({expectedIdentity: IDENTITY, baseUrl: 'https://evil.example'}), /same-origin/);
assert.throws(() => foundation.createClient({expectedIdentity: IDENTITY, fetcher: null}), /fetch/);
assert.throws(() => client.subscribe('nope'), /Listener/);
});
test('client discards late negotiation results', async () => {
const resolvers = [];
const client = foundation.createClient({
expectedIdentity: IDENTITY,
fetcher: () => new Promise((resolve) => resolvers.push(resolve)),
});
const first = client.negotiate();
const second = client.negotiate();
resolvers[1](response(200, capability(['hux.foundation'])));
await second;
resolvers[0](response(404, {}));
await first;
assert.equal(client.getState().phase, 'ready');
});
test('event adapter exposes summaries and references, never details or URIs', () => {
const adapters = foundation.createAdapters({identity: IDENTITY, conversationId: 'conv_0001abcd'});
const rawEvent = {
schema: 'hux.event.v1', id: 'evt_0001aaaa', seq: 2,
ts: '2026-08-24T00:00:00Z', conversation_id: 'conv_0001abcd',
kind: 'tool.result', summary: 'Checked service health', detail: {raw: 'private output'},
evidence: [{kind: 'tool_result', id: 'call-1', uri: 'https://secret.example', hash: 'sha256:no'}],
sensitivity: 'personal', redaction: {level: 'none'},
};
const event = adapters.adaptEvent(rawEvent);
assert.equal(event.summary, 'Checked service health');
assert.deepEqual(event.evidence, [{kind: 'tool_result', id: 'call-1'}]);
assert.equal('detail' in event, false);
assert(Object.isFrozen(event.evidence[0]));
const hidden = adapters.adaptEvent({...rawEvent, redaction: {level: 'full'}});
assert.equal(hidden.summary, 'Details hidden by privacy controls.');
assert.equal(hidden.redacted, true);
assert.throws(() => adapters.adaptEvent({...rawEvent, conversation_id: 'conv_other'}), /conversation/);
assert.throws(() => adapters.adaptEvent({...rawEvent, seq: -1}), /activity/);
assert.throws(() => adapters.adaptEvent({...rawEvent, summary: ''}), /summary/);
assert.throws(() => adapters.adaptEvent({...rawEvent, ts: 'today'}), /timestamp/);
assert.throws(() => adapters.adaptEvent({...rawEvent, kind: 'tool.raw'}), /classification/);
assert.throws(() => adapters.adaptEvent({...rawEvent, redaction: {level: 'maybe'}}), /redaction/);
const withoutConversation = {...rawEvent};
delete withoutConversation.conversation_id;
assert.throws(() => adapters.adaptEvent(withoutConversation), /matching conversation/);
assert.throws(() => adapters.adaptEvent(null), /not an object/);
});
test('object adapter is display-only and enforces owner and conversation', () => {
const adapters = foundation.createAdapters({identity: IDENTITY, conversationId: 'conv_0001abcd'});
const view = adapters.adaptObject({
schema: 'hux.artifact.v1', id: 'art_0001aaaa', owner: IDENTITY.user_ref,
conversation_id: 'conv_0001abcd', project_id: 'prj_0001aaaa', title: 'Plan',
type: 'markdown', status: 'ready', sensitivity: 'personal', content: 'never expose me',
updated_at: '2026-08-24T00:00:00Z',
});
assert.deepEqual(view, {
schema: 'hux.artifact.v1', id: 'art_0001aaaa', title: 'Plan', kind: 'markdown',
status: 'ready', sensitivity: 'personal', conversationId: 'conv_0001abcd',
projectId: 'prj_0001aaaa', updatedAt: '2026-08-24T00:00:00Z',
});
assert.equal('content' in view, false);
assert.throws(() => adapters.adaptObject({...view, owner: 'usr_ffffffffffffffff'}), /user boundary/);
assert.throws(() => adapters.adaptObject({...view, schema: 'hux.artifact.v2'}), /version/);
assert.throws(() => foundation.createAdapters({identity: IDENTITY, conversationId: 'bad'}), /conversation id/);
});
class FakeNode {
constructor(tag) {
this.tagName = tag.toUpperCase();
this.attributes = {};
this.children = [];
this.dataset = {};
this.hidden = false;
this.parentNode = null;
this.listeners = {};
this.textContent = '';
}
setAttribute(name, value) {
this.attributes[name] = String(value);
if (name.startsWith('data-')) {
const key = name.slice(5).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
this.dataset[key] = 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; }
click() { if (this.listeners.click) this.listeners.click({target: this}); }
focus() { this.focused = true; }
keydown(key) { if (this.listeners.keydown) this.listeners.keydown({key, preventDefault() {}}); }
}
function fakeClient() {
let state = Object.freeze({phase: 'disabled', capabilities: null, error: null});
let listener = null;
let retries = 0;
return {
enabled: (flag) => state.phase === 'ready' && state.capabilities.flags.includes(flag),
getState: () => state,
negotiate: () => { retries += 1; },
retries: () => retries,
subscribe: (next) => { listener = next; next(state); return () => { listener = null; }; },
setState: (next) => { state = Object.freeze(next); if (listener) listener(state); },
};
}
function descendants(node) {
return node.children.flatMap((child) => [child, ...descendants(child)]);
}
test('accessible shell stays hidden until an extension capability is verified', () => {
const client = fakeClient();
const host = new FakeNode('main');
const shell = shellApi.createShell({client, document: {createElement: (tag) => new FakeNode(tag)}});
shell.register({id: 'projects', flag: 'hux.projects', label: 'Projects', order: 20, render: ({container}) => { container.textContent = 'Projects view'; }});
shell.register({id: 'activity', flag: 'hux.activity_timeline', label: 'Activity', order: 10, render: ({container}) => { container.textContent = 'Activity view'; }});
const root = shell.mount(host);
assert.equal(root.hidden, true);
assert.equal(root.getAttribute('data-hux-version'), 'hux.v1');
client.setState({phase: 'loading', capabilities: null, error: null});
assert.equal(root.hidden, false);
assert.equal(descendants(root)[0].getAttribute('role'), 'status');
client.setState({phase: 'ready', capabilities: {flags: ['hux.foundation', 'hux.activity_timeline', 'hux.projects']}, error: null});
const tabs = descendants(root).filter((node) => node.getAttribute('role') === 'tab');
const panels = descendants(root).filter((node) => node.getAttribute('role') === 'tabpanel');
assert.deepEqual(tabs.map((tab) => tab.textContent), ['Activity', 'Projects']);
assert.equal(tabs[0].getAttribute('aria-selected'), 'true');
assert.equal(panels[0].hidden, false);
tabs[1].click();
assert.equal(tabs[1].getAttribute('aria-selected'), 'true');
assert.equal(panels[0].hidden, true);
assert.equal(panels[1].hidden, false);
tabs[1].keydown('ArrowLeft');
assert.equal(tabs[0].focused, true);
assert.equal(tabs[0].getAttribute('aria-selected'), 'true');
client.setState({phase: 'error', capabilities: null, error: 'Safe error'});
const retry = descendants(root).find((node) => node.textContent === 'Try again');
retry.click();
assert.equal(client.retries(), 1);
shell.destroy();
assert.equal(host.children.length, 0);
});
test('shell rejects malformed and duplicate extensions', () => {
const client = fakeClient();
const shell = shellApi.createShell({client, document: {createElement: (tag) => new FakeNode(tag)}});
assert.throws(() => shell.register({id: 'X', flag: 'hux.projects', label: 'x', render() {}}), /id/);
assert.throws(() => shell.register({id: 'base', flag: 'hux.foundation', label: 'x', render() {}}), /feature flag/);
assert.throws(() => shell.register({id: 'unknown', flag: 'hux.nope', label: 'x', render() {}}), /feature flag/);
assert.throws(() => shell.register({id: 'projects', flag: 'hux.projects', label: '', render() {}}), /label/);
const remove = shell.register({id: 'projects', flag: 'hux.projects', label: 'Projects', render() {}});
assert.throws(() => shell.register({id: 'projects', flag: 'hux.projects', label: 'Again', render() {}}), /already/);
remove();
assert.throws(() => shellApi.createShell({}), /client and document/);
});
test('shell hides empty ready state and contains extension renderer failure', () => {
const client = fakeClient();
const host = new FakeNode('main');
const shell = shellApi.createShell({client, document: {createElement: (tag) => new FakeNode(tag)}});
const root = shell.mount(host);
client.setState({phase: 'ready', capabilities: {flags: ['hux.foundation']}, error: null});
assert.equal(root.hidden, true);
shell.register({id: 'broken-panel', flag: 'hux.projects', label: 'Broken', render() { throw new Error('raw private error'); }});
client.setState({phase: 'ready', capabilities: {flags: ['hux.foundation', 'hux.projects']}, error: null});
const safe = descendants(root).find((node) => node.textContent.includes('temporarily unavailable'));
assert(safe);
assert.equal(safe.textContent.includes('private error'), false);
});