atlas-iac/testing/tests/test_hermes_hux_runtime_wave_c_node.js

370 lines
22 KiB
JavaScript
Raw Normal View History

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
const vm = require('node:vm');
const api = require('../../dockerfiles/hermes-webui-hux/runtime/wave_c_multimodal_onboarding_release.js');
const IDENTITY = Object.freeze({tenantSlot: 'slot-3', userRef: 'usr_0123456789abcdef',
surface: 'chat', trust: 'router'});
const RAW_IDENTITY = Object.freeze({tenant_slot: IDENTITY.tenantSlot, subject: IDENTITY.userRef,
surface: IDENTITY.surface, trust: IDENTITY.trust});
const PROJECT = 'prj_alpha1234';
const CONVERSATION = 'conv_alpha1234';
const SESSION = 'ses_alpha1234';
const STAMP = '2026-08-24T10:00:00Z';
const DIGEST = `sha256:${'a'.repeat(64)}`;
const ROUTES = Object.freeze({
multimodal: ['/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items',
'/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}',
'/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}/transcript-corrections',
'/hux/v1/projects/{project_id}/conversations/{id}/capture-intents'],
onboarding: ['/hux/v1/projects/{project_id}/conversations/{id}/suggestions/evaluate',
'/hux/v1/projects/{project_id}/conversations/{id}/suggestions/{suggestion_id}/decisions',
'/hux/v1/projects/{project_id}/conversations/{id}/suggestions/states'],
release: ['/hux/v1/projects/{project_id}/conversations/{id}/releases',
'/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}',
'/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}/transitions'],
});
function capability(routes = ROUTES, overrides = {}) {
const cards = [
['HUX-11', 'hux.foundation'], ['HUX-01', 'hux.activity_timeline'],
['HUX-03', 'hux.projects'], ['HUX-04', 'hux.artifacts'],
['HUX-05', 'hux.autonomy'], ['HUX-06', 'hux.friendly_modes'], ['HUX-07', 'hux.multimodal'],
['HUX-09', 'hux.onboarding'], ['HUX-10', 'hux.privacy'],
['HUX-12', 'hux.release_followthrough'],
].map(([card, flag]) => ({card, flag, enabled: true, routes: card === 'HUX-07' ? routes.multimodal :
card === 'HUX-09' ? routes.onboarding : card === 'HUX-12' ? routes.release :
[`/hux/v1/${card.toLowerCase()}`]}));
return {schema: 'hux.capabilities.v1', contract_version: '1.1.0', identity: RAW_IDENTITY,
cards, server: {}, ...overrides};
}
function mediaPage(overrides = {}) {
return {items: [{schema: 'hux.multimodal_item.v1', id: 'mmi_alpha1234', owner: IDENTITY.userRef,
project_id: PROJECT, conversation_id: CONVERSATION, kind: 'image', source: 'upload',
filename: 'photo.png', mime: 'image/png', bytes: 2048, hash: DIGEST,
approval_id: 'apr_alpha1234', status: 'metadata_only', created_at: STAMP, revision: 1}],
next: null, ...overrides};
}
function evaluation(overrides = {}) {
const trigger = {surface: 'chat', context: 'after_artifact'};
return {suggestion: {schema: 'hux.suggestion.v1', id: 'sug_alpha1234', kind: 'workflow', trigger,
title: 'Run this every week?', body: 'Save this task as a weekly workflow.',
action: {type: 'start_workflow', payload: {intent: 'schedule_weekly'}}, priority: 20,
suppression: {dismissable: true, max_shows: 3, cooldown_seconds: 3600,
never_again_supported: true}},
state: {schema: 'hux.suggestion_state.v1', owner: IDENTITY.userRef,
suggestion_id: 'sug_alpha1234', shows: 1, last_shown_at: STAMP, never_again: false},
revision: 1, stored: true, ...overrides};
}
function decision(overrides = {}) {
return {state: {schema: 'hux.suggestion_state.v1', owner: IDENTITY.userRef,
suggestion_id: 'sug_alpha1234', shows: 1, never_again: false},
revision: 2, decision: 'acted', ...overrides};
}
function release(state = 'live_verified', overrides = {}) {
return {schema: 'hux.release.v1', id: 'rel_alpha1234', workload: 'hermes-webui',
commit: 'b'.repeat(40), state, evidence: {image_digest: DIGEST, harbor_digest: DIGEST,
pod_digest: DIGEST, flux_revision: `main@sha1:${'c'.repeat(40)}`,
health_check: {url: 'https://chat.bstein.dev/healthz', status: 'pass', at: STAMP}},
transitions: [{from: 'converged', to: 'live_verified', at: STAMP,
by: {type: 'system', id: 'probe'}}], ...overrides};
}
function releasePage(items = [release()], overrides = {}) {
const views = items && items.map((item, index) => ({release: item,
scope: {project_id: PROJECT, conversation_id: CONVERSATION}, revision: index + 1,
ledger_hash: `sha256:${String(index + 1).repeat(64).slice(0, 64)}`}));
return {items: views, next: null, ...overrides};
}
function response(status, body, headers = {}) {
const normalized = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
return {status, ok: status >= 200 && status < 300, json: async () => body,
headers: {get: (name) => normalized[name.toLowerCase()] || null}};
}
class FakeNode {
constructor(tag) {
this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = []; this.dataset = {};
this.listeners = {}; this.parentNode = null; this.textContent = ''; this.hidden = false; this.value = '';
}
setAttribute(name, value) {
this.attributes[name] = String(value);
if (name.startsWith('data-')) this.dataset[name.slice(5).replace(/-([a-z])/g,
(_, letter) => letter.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; }
trigger(name, extra = {}) { return this.listeners[name] && this.listeners[name]({target: this,
preventDefault() {}, ...extra}); }
click() { return this.trigger('click'); }
focus() { this.focused = true; }
}
const document = {createElement: (tag) => new FakeNode(tag)};
function descendants(node) { return node.children.flatMap((child) => [child, ...descendants(child)]); }
function allText(node) { return [node, ...descendants(node)].map((item) => item.textContent).join(' '); }
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)); }
function makeClient(flags = []) {
const enabled = new Set(flags);
return {identity: IDENTITY, enabled: (flag) => enabled.has(flag), request: async () => ({body: {}})};
}
test('capability routes are canonical, dependency-aware, and fail closed', () => {
const cards = api.normalizeCapabilityCards(capability(), IDENTITY);
assert.deepEqual(cards.multimodal.routes, ROUTES.multimodal);
assert(Object.isFrozen(cards));
const flags = ['hux.foundation', 'hux.projects', 'hux.artifacts', 'hux.autonomy',
'hux.friendly_modes', 'hux.multimodal', 'hux.onboarding', 'hux.release_followthrough'];
const client = makeClient(flags);
assert.equal(api.capabilityGap(client, cards, 'multimodal'), null);
assert.equal(api.capabilityGap(client, cards, 'onboarding'), null);
assert.equal(api.capabilityGap(client, cards, 'release'), null);
assert.equal(api.capabilityGap(makeClient(flags.filter((flag) => flag !== 'hux.artifacts')),
cards, 'multimodal'), 'feature flag or dependency is off');
assert.equal(api.capabilityGap(client, {...cards, onboarding: {...cards.onboarding, routes: []}},
'onboarding'), 'server route is not available');
assert.equal(api.capabilityGap(client, {}, 'release'), 'feature flag or dependency is off');
assert.throws(() => api.normalizeCapabilityCards(capability(ROUTES, {cards:
capability().cards.filter((card) => card.card !== 'HUX-07')}), IDENTITY), /HUX-07/);
assert.throws(() => api.normalizeCapabilityCards(capability(ROUTES, {cards:
capability().cards.map((card) => card.card === 'HUX-09' ? {...card, flag: 'hux.projects'} : card)}),
IDENTITY), /HUX-09|Canonical/);
assert.throws(() => api.normalizeCapabilityCards(capability(ROUTES, {cards:
capability().cards.map((card) => card.card === 'HUX-12' ? {...card, enabled: 'yes'} : card)}),
IDENTITY), /capability/);
});
test('multimodal adapter binds the exact backend page and scoped metadata records', () => {
const page = api.normalizeMediaPage(mediaPage(), IDENTITY, PROJECT, CONVERSATION);
assert.deepEqual(page, {media: [{id: 'mmi_alpha1234', kind: 'image', fileName: 'photo.png', bytes: 2048}]});
const item = mediaPage().items[0];
const invalid = [null, {items: [], next: null, extra: true}, mediaPage({next: 'cursor'}),
mediaPage({items: null}), mediaPage({items: Array(201).fill(item)}), mediaPage({items: [null]}),
mediaPage({items: [{...item, schema: 'old'}]}), mediaPage({items: [{...item, id: 'bad'}]}),
mediaPage({items: [{...item, owner: 'usr_ffffffffffffffff'}]}),
mediaPage({items: [{...item, project_id: 'prj_other1234'}]}),
mediaPage({items: [{...item, conversation_id: 'conv_other1234'}]}),
mediaPage({items: [{...item, kind: 'binary'}]}), mediaPage({items: [{...item, bytes: 0}]}),
mediaPage({items: [{...item, filename: ''}]}), mediaPage({items: [{...item, filename: '../secret'}]}),
mediaPage({items: [{...item, status: 'uploaded'}]}), mediaPage({items: [{...item, revision: 0}]}),
mediaPage({items: [{...item, created_at: 'today'}]}), mediaPage({items: [{...item, extra: true}]})];
invalid.forEach((raw) => assert.throws(() =>
api.normalizeMediaPage(raw, IDENTITY, PROJECT, CONVERSATION), /Multimodal|Media/));
});
test('onboarding adapter accepts only backend suggestion and suppression-state shapes', () => {
const trigger = api.normalizeTrigger({surface: 'chat', context: 'after_artifact'}, 'chat');
assert.deepEqual(trigger, {surface: 'chat', context: 'after_artifact'});
for (const raw of [null, {surface: 'chat'}, {surface: 'chat', context: 'idle', extra: true},
{surface: 'worker', context: 'idle'}, {surface: 'chat', context: 'surprise'}]) {
assert.throws(() => api.normalizeTrigger(raw, 'chat'), /trigger/);
}
const item = api.normalizeEvaluation(evaluation(), IDENTITY, SESSION, CONVERSATION, trigger, true);
assert.equal(item.action, 'start_workflow');
assert(Object.isFrozen(item));
assert.equal(api.normalizeEvaluation({suggestion: null, reason: 'private_mode', stored: false},
IDENTITY, SESSION, CONVERSATION, trigger, true), null);
const invalid = [null, {suggestion: null, reason: 'private_mode', stored: false, extra: true},
evaluation({stored: 'yes'}), evaluation({revision: 0}), evaluation({extra: true}),
evaluation({suggestion: null}), evaluation({suggestion: {...evaluation().suggestion, schema: 'old'}}),
evaluation({suggestion: {...evaluation().suggestion, id: 'bad'}}),
evaluation({suggestion: {...evaluation().suggestion, trigger: {surface: 'chat', context: 'idle'}}}),
evaluation({suggestion: {...evaluation().suggestion, action: {type: 'surprise'}}}),
evaluation({suggestion: {...evaluation().suggestion, suppression: null}}),
evaluation({suggestion: {...evaluation().suggestion, suppression:
{...evaluation().suggestion.suppression, dismissable: false}}}),
evaluation({suggestion: {...evaluation().suggestion, suppression:
{...evaluation().suggestion.suppression, never_again_supported: false}}}),
evaluation({suggestion: {...evaluation().suggestion, suppression:
{...evaluation().suggestion.suppression, max_shows: 0}}}),
evaluation({state: null}), evaluation({state: {...evaluation().state, schema: 'old'}}),
evaluation({state: {...evaluation().state, owner: 'usr_ffffffffffffffff'}}),
evaluation({state: {...evaluation().state, suggestion_id: 'sug_other1234'}}),
evaluation({state: {...evaluation().state, shows: 0}}),
evaluation({state: {...evaluation().state, shows: 4}}),
evaluation({state: {...evaluation().state, never_again: true}}),
evaluation({suggestion: {...evaluation().suggestion, title: ''}}),
evaluation({suggestion: {...evaluation().suggestion, body: ''}})];
invalid.forEach((raw) => assert.throws(() =>
api.normalizeEvaluation(raw, IDENTITY, SESSION, CONVERSATION, trigger, true), /Suggestion/));
const memory = evaluation({suggestion: {...evaluation().suggestion, action: {type: 'open_memory'}}});
assert.throws(() => api.normalizeEvaluation(memory, IDENTITY, SESSION, CONVERSATION, trigger, false), /eligible/);
assert.equal(api.normalizeEvaluation(memory, IDENTITY, SESSION, CONVERSATION, trigger, true).action,
'open_memory');
assert.equal(api.actionLabel('open_mode'), 'Choose a mode');
assert.equal(api.actionLabel('create_project'), 'Add to a project');
assert.equal(api.actionLabel('open_memory'), 'Review memory');
assert.equal(api.actionLabel('open_artifacts'), 'View artifacts');
assert.equal(api.actionLabel('start_workflow'), 'Use suggestion');
assert.equal(api.actionLabel('none'), null);
});
test('release adapter makes a live claim only from exact end-to-end evidence', () => {
const live = api.releaseEvidence(release());
assert.equal(live.live, true);
assert.equal(live.digest, DIGEST);
assert.deepEqual(api.releaseEvidence(release('merged')), {id: 'rel_alpha1234', workload: 'hermes-webui',
commit: 'b'.repeat(40), live: false});
const bad = [null, release('live_verified', {schema: 'old'}),
release('live_verified', {id: 'bad'}), release('live_verified', {workload: 'unknown'}),
release('live_verified', {commit: 'short'}), release('live_verified', {evidence: null}),
release('live_verified', {transitions: null}), release('live_verified', {evidence:
{...release().evidence, image_digest: 'bad'}}), release('live_verified', {evidence:
{...release().evidence, harbor_digest: `sha256:${'d'.repeat(64)}`}}),
release('live_verified', {evidence: {...release().evidence, flux_revision: 'queued'}}),
release('live_verified', {evidence: {...release().evidence, health_check: null}}),
release('live_verified', {evidence: {...release().evidence, health_check:
{...release().evidence.health_check, status: 'fail'}}}),
release('live_verified', {evidence: {...release().evidence, health_check:
{...release().evidence.health_check, at: 'today'}}}),
release('live_verified', {transitions: []}),
release('live_verified', {transitions: [{from: 'deployed', to: 'live_verified', at: STAMP}]}),
release('live_verified', {transitions: [{from: 'converged', to: 'deployed', at: STAMP}]}),
release('live_verified', {transitions: [{from: 'converged', to: 'live_verified', at: 'today'}]})];
bad.forEach((raw) => assert.equal(api.releaseEvidence(raw), null));
const page = api.normalizeReleasePage(releasePage([release(), bad[1], release('merged')]),
IDENTITY, PROJECT, CONVERSATION);
assert.equal(page.items.length, 2);
assert.equal(page.withheld, 1);
const invalidPages = [null, {items: [], next: null, extra: true}, releasePage([], {next: 'cursor'}),
releasePage(null), releasePage(Array(101).fill(release()))];
invalidPages.forEach((raw) => assert.throws(() =>
api.normalizeReleasePage(raw, IDENTITY, PROJECT, CONVERSATION), /Release/));
});
test('all three extensions expose inert accessible capability gaps without calls', () => {
const cards = api.normalizeCapabilityCards(capability({multimodal: [], onboarding: [], release: []}), IDENTITY);
const client = makeClient(['hux.foundation', 'hux.projects', 'hux.artifacts', 'hux.autonomy',
'hux.friendly_modes', 'hux.multimodal', 'hux.onboarding', 'hux.release_followthrough']);
let calls = 0; client.request = async () => { calls += 1; return {body: {}}; };
const options = {document, projectId: PROJECT, conversationId: CONVERSATION, sessionId: SESSION,
cards: () => cards, idempotencyKey: () => 'suggest:key:0001'};
const panels = [new FakeNode('div'), new FakeNode('div'), new FakeNode('div')];
api.multimodalExtension(options).render({client, container: panels[0]});
const onboarding = api.onboardingExtension(options);
onboarding.extension.render({client, container: panels[1]});
api.releaseExtension(options).render({client, container: panels[2]});
assert.equal(calls, 0);
panels.forEach((panel) => {
assert.match(allText(panel), /Not available/);
assert.equal(descendants(panel).some((node) => node.tagName === 'BUTTON'), false);
});
return onboarding.present({surface: 'chat', context: 'idle'}).then((shown) => assert.equal(shown, false));
});
test('complete runtime renders read-only media and release evidence and saves explicit suggestion choices', async () => {
const calls = [];
const actions = [];
const fetcher = async (url, init) => {
calls.push({url, init});
if (url.endsWith('/capabilities')) return response(200, capability());
if (url.endsWith('/multimodal/items')) return response(200, mediaPage());
if (url.endsWith('/releases')) return response(200, releasePage([release(), release('merged')]));
if (url.endsWith('/suggestions/evaluate')) return response(200, evaluation());
if (url.endsWith('/decisions')) return response(200, decision());
throw new Error(`unexpected ${url}`);
};
const runtime = api.createWaveCRuntime({document, fetcher, expectedIdentity: IDENTITY,
projectId: PROJECT, conversationId: CONVERSATION, sessionId: SESSION,
idempotencyKey: () => 'suggest:key:0001', onSuggestionAction: async (intent) => actions.push(intent)});
const root = new FakeNode('main');
assert.equal(await runtime.presentSuggestion({surface: 'chat', context: 'idle'}), false);
await runtime.mount(root); await flush();
assert.match(allText(root), /photo.png/);
assert.match(allText(root), /Live verified/);
assert.match(allText(root), /not live verified/);
assert.equal(allText(root).includes('merged'), false);
assert.equal(calls.filter((call) => call.url.endsWith('/suggestions/evaluate')).length, 0);
assert.equal(await runtime.presentSuggestion({surface: 'chat', context: 'after_artifact'}), true);
assert.match(allText(root), /Run this every week/);
byText(root, 'Use suggestion').click(); await flush();
assert.deepEqual(actions, [{type: 'start_workflow', conversationId: CONVERSATION}]);
const mutation = calls.find((call) => call.url.endsWith('/decisions'));
assert.equal(mutation.init.headers['Idempotency-Key'], 'suggest:key:0001');
assert.equal(mutation.init.credentials, 'same-origin');
assert.equal(mutation.init.cache, 'no-store');
assert.deepEqual(JSON.parse(mutation.init.body), {decision: 'acted', clicked: true});
assert.equal(mutation.init.headers['If-Match'], '1');
assert.match(allText(root), /Suggestion completed/);
assert.equal(await runtime.presentSuggestion({surface: 'chat', context: 'after_artifact'}), false);
await assert.rejects(() => runtime.mount(root), /already mounted/);
runtime.destroy();
});
test('suggestions never auto-act and failures never claim success', async () => {
const calls = [];
const client = makeClient(['hux.foundation', 'hux.projects', 'hux.friendly_modes',
'hux.onboarding', 'hux.privacy']);
client.request = async (path, init) => {
calls.push({path, init});
if (path.endsWith('/suggestions/evaluate')) return {body: evaluation()};
if (path.endsWith('/decisions')) return {body: decision({state:
{...decision().state, owner: 'usr_ffffffffffffffff'}})};
throw new Error('unexpected');
};
const cards = api.normalizeCapabilityCards(capability(), IDENTITY);
const panel = new FakeNode('div');
const onboarding = api.onboardingExtension({document, projectId: PROJECT, conversationId: CONVERSATION,
sessionId: SESSION, cards: () => cards, idempotencyKey: () => 'suggest:key:0002'});
onboarding.extension.render({client, container: panel});
assert.equal(calls.length, 0);
await onboarding.present({surface: 'chat', context: 'after_artifact'});
byText(panel, 'Use suggestion').click(); await flush();
assert.equal(calls.length, 1);
assert.match(allText(panel), /not connected/);
await onboarding.present({surface: 'chat', context: 'after_artifact'});
byText(panel, 'Not now').click(); await flush();
assert.match(allText(panel), /could not be saved/);
});
test('renderers withhold invalid network records and use alert states', async () => {
const client = makeClient(['hux.foundation', 'hux.projects', 'hux.artifacts', 'hux.autonomy',
'hux.multimodal', 'hux.release_followthrough']);
const cards = api.normalizeCapabilityCards(capability(), IDENTITY);
const panels = [new FakeNode('div'), new FakeNode('div')];
client.request = async (path) => ({body: path.endsWith('/releases') ?
releasePage([], {next: 'forged'}) : mediaPage({identity: null})});
const options = {document, projectId: PROJECT, conversationId: CONVERSATION, cards: () => cards};
api.multimodalExtension(options).render({client, container: panels[0]});
api.releaseExtension(options).render({client, container: panels[1]});
await flush();
panels.forEach((panel) => assert.equal(descendants(panel)[0].getAttribute('role'), 'alert'));
});
test('runtime validates scope, capability responses, and browser export', async () => {
for (const options of [{}, {document, fetcher: async () => response(404, {}), expectedIdentity: IDENTITY,
projectId: 'bad', conversationId: CONVERSATION, sessionId: SESSION},
{document, fetcher: async () => response(404, {}), expectedIdentity: IDENTITY,
projectId: PROJECT, conversationId: 'bad', sessionId: SESSION},
{document, fetcher: async () => response(404, {}), expectedIdentity: IDENTITY,
projectId: PROJECT, conversationId: CONVERSATION, sessionId: 'bad'}]) {
assert.throws(() => api.createWaveCRuntime(options), /requires/);
}
const runtime = api.createWaveCRuntime({document, fetcher: async () => response(404, {}),
expectedIdentity: IDENTITY, projectId: PROJECT, conversationId: CONVERSATION, sessionId: SESSION});
const target = new FakeNode('main');
await runtime.mount(target);
assert.equal(runtime.client.getState().phase, 'disabled');
runtime.destroy();
const source = fs.readFileSync('dockerfiles/hermes-webui-hux/runtime/wave_c_multimodal_onboarding_release.js', 'utf8');
const context = {globalThis: {}, HermesHuxFoundation: {}, HermesHuxShell: {}, HermesHuxWaveAContract: {}};
context.globalThis = context; vm.runInNewContext(source, context);
assert.equal(typeof context.HermesHuxWaveC.createWaveCRuntime, 'function');
});