atlas-iac/testing/tests/test_hermes_hux_ui_organization.mjs
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

418 lines
20 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
createOrganizationClient,
OrganizationContractError,
} from "../../dockerfiles/hermes-webui-hux/organization/client.ts";
import {
FOUNDATION_FLAG,
PROJECTS_FLAG,
attachmentViews,
branchLineage,
normalizeConversation,
normalizeProject,
normalizeSnapshot,
preservesConversation,
projectsEnabled,
} from "../../dockerfiles/hermes-webui-hux/organization/model.ts";
import {
boundedLimit,
isOpaqueId,
isUtc,
normalizeIdentity,
safeCursor,
safeTags,
safeText,
sameIdentity,
scopedPath,
} from "../../dockerfiles/hermes-webui-hux/organization/security.ts";
const IDENTITY = Object.freeze({
tenantRef: "tnt_0123456789abcdef",
userRef: "usr_0123456789abcdef",
surface: "chat",
});
const RAW_IDENTITY = Object.freeze({
tenant_ref: IDENTITY.tenantRef,
user_ref: IDENTITY.userRef,
surface: IDENTITY.surface,
});
const PROJECT_ID = "prj_alpha1234";
function project(extra = {}) {
return {
schema: "hux.project.v1",
id: PROJECT_ID,
owner: IDENTITY.userRef,
name: "Kitchen work",
description: "Plans",
tags: ["home"],
pinned: true,
archived: false,
default_mode: "research",
created_at: "2026-08-20T09:00:00Z",
updated_at: "2026-08-24T09:00:00Z",
...extra,
};
}
function conversation(extra = {}) {
return {
schema: "hux.conversation.v1",
id: "conv_child1234",
owner: IDENTITY.userRef,
project_id: PROJECT_ID,
title: "Compare suppliers",
tags: ["suppliers"],
pinned: false,
archived: false,
mode: "research",
branch: {
parent_conversation_id: "conv_parent1234",
branch_point_message_id: "msg-12",
},
artifact_ids: ["art_quote1234", "art_plan1234"],
last_message_at: "2026-08-24T09:00:01Z",
created_at: "2026-08-20T09:00:00Z",
updated_at: "2026-08-24T09:00:01Z",
...extra,
};
}
function envelope(schema, items, extra = {}) {
return {schema, api_version: "hux.v1", identity: RAW_IDENTITY,
items, next_cursor: "next_page", total: items.length, ...extra};
}
function mutation(schema, item, extra = {}) {
return {schema, api_version: "hux.v1", identity: RAW_IDENTITY, item, ...extra};
}
function harness(responses, flags = [FOUNDATION_FLAG, PROJECTS_FLAG]) {
const calls = [];
const enabled = new Set(flags);
const foundation = {
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 response = responses.shift();
if (!response) throw new Error("unexpected request");
return {ok: response.ok ?? true, status: response.status ?? 200,
json: async () => response.body};
};
return {calls, client: createOrganizationClient({client: foundation, fetcher}), foundation};
}
test("HUX-03 is default-off and requires the foundation", () => {
assert.equal(projectsEnabled(), false);
assert.equal(projectsEnabled([]), false);
assert.equal(projectsEnabled([PROJECTS_FLAG]), false);
assert.equal(projectsEnabled([FOUNDATION_FLAG]), false);
assert.equal(projectsEnabled([FOUNDATION_FLAG, PROJECTS_FLAG]), true);
});
test("security primitives reject scope/path/query confusion", () => {
assert.equal(isOpaqueId("conv_abcd", "conv"), true);
assert.equal(isOpaqueId("prj_abcd", "conv"), false);
assert.equal(isOpaqueId("bad"), false);
assert.equal(isOpaqueId(4), false);
assert.equal(isUtc("2026-08-24T09:00:00.123Z"), true);
assert.equal(isUtc("2026-99-24T09:00:00Z"), false);
assert.equal(isUtc(4), false);
assert.equal(safeText({}, "fallback", 5), "fallback");
assert.equal(safeText(" \u0000 ", "fallback", 5), "fallback");
assert.equal(safeText("abcdef", "fallback", 4), "abc…");
assert.equal(safeText("okay", "fallback", 8), "okay");
assert.deepEqual(safeTags(["one", "two-2"]), ["one", "two-2"]);
assert.equal(safeTags("one"), null);
assert.equal(safeTags(Array(33).fill("x")), null);
assert.equal(safeTags(["Bad"]), null);
assert.equal(safeTags(["same", "same"]), null);
assert.equal(safeTags([4]), null);
assert.equal(safeCursor(undefined), null);
assert.equal(safeCursor(""), null);
assert.equal(safeCursor("next_page"), "next_page");
assert.equal(safeCursor("bad/cursor"), undefined);
assert.equal(boundedLimit(22), 22);
assert.equal(boundedLimit(200), 100);
assert.equal(boundedLimit(0), 40);
assert.equal(boundedLimit(undefined, 20), 20);
assert.equal(scopedPath("/projects/", PROJECT_ID), `/projects/${PROJECT_ID}`);
for (const base of ["https://evil", "//evil", "/hux?x", "/hux\\x", "/../x"]) {
assert.throws(() => scopedPath(base, PROJECT_ID), /same-origin/);
}
assert.throws(() => scopedPath("/projects", "../bad"), /identifier/);
});
test("identity normalization is exact and includes the current surface", () => {
const normalized = normalizeIdentity(RAW_IDENTITY);
assert.deepEqual(normalized, IDENTITY);
assert.equal(sameIdentity(normalized, IDENTITY), true);
assert.equal(sameIdentity(normalized, {...IDENTITY, surface: "worker"}), false);
assert.equal(sameIdentity(normalized, {...IDENTITY, userRef: "usr_aaaaaaaaaaaaaaaa"}), false);
assert.equal(sameIdentity(normalized, {...IDENTITY, tenantRef: "tnt_aaaaaaaaaaaaaaaa"}), false);
assert.equal(normalizeIdentity(null), null);
assert.equal(normalizeIdentity({...RAW_IDENTITY, tenant_ref: "raw"}), null);
assert.equal(normalizeIdentity({...RAW_IDENTITY, user_ref: "raw"}), null);
assert.equal(normalizeIdentity({...RAW_IDENTITY, surface: "browser"}), null);
});
test("project normalization follows project.schema and owner binding", () => {
const item = normalizeProject(project(), IDENTITY.userRef);
assert.equal(item.name, "Kitchen work");
assert.equal(item.description, "Plans");
const invalid = [
{schema: "old"}, {id: "bad"}, {owner: "usr_aaaaaaaaaaaaaaaa"},
{tags: ["Bad"]}, {pinned: "yes"}, {archived: 0}, {created_at: "today"},
{updated_at: "today"}, {default_mode: "turbo"}, {default_mode: 7},
{name: ""},
];
invalid.forEach((change) => assert.equal(normalizeProject(project(change), IDENTITY.userRef), null));
assert.equal(normalizeProject(project({description: {}}), IDENTITY.userRef).description, "");
assert.equal(normalizeProject(project({default_mode: undefined}), IDENTITY.userRef).id, PROJECT_ID);
});
test("conversation normalization binds project, lineage, and attachments", () => {
const projects = new Set([PROJECT_ID]);
const item = normalizeConversation(conversation(), IDENTITY.userRef, projects);
assert.deepEqual(item.artifactIds, ["art_quote1234", "art_plan1234"]);
assert.equal(item.branch.parentConversationId, "conv_parent1234");
assert.equal(item.lastMessageAt, "2026-08-24T09:00:01Z");
assert.equal(normalizeConversation(conversation({project_id: undefined}), IDENTITY.userRef, projects).projectId, null);
assert.equal(normalizeConversation(conversation({branch: undefined}), IDENTITY.userRef, projects).branch, null);
assert.equal(normalizeConversation(conversation({last_message_at: undefined, mode: undefined}),
IDENTITY.userRef, projects).lastMessageAt, null);
const invalid = [
{schema: "old"}, {id: "bad"}, {owner: "usr_aaaaaaaaaaaaaaaa"}, {tags: ["Bad"]},
{branch: null}, {branch: []}, {branch: {parent_conversation_id: "bad", branch_point_message_id: "x"}},
{branch: {parent_conversation_id: "conv_parent1234", branch_point_message_id: 3}},
{branch: {parent_conversation_id: "conv_parent1234", branch_point_message_id: ""}},
{branch: {parent_conversation_id: "conv_child1234", branch_point_message_id: "x"}},
{pinned: "no"}, {archived: "no"}, {artifact_ids: null},
{artifact_ids: ["bad"]}, {artifact_ids: ["art_same1234", "art_same1234"]},
{artifact_ids: Array.from({length: 501}, (_, index) => `art_${String(index).padStart(4, "0")}`)},
{created_at: "today"}, {updated_at: "today"}, {last_message_at: "today"},
{mode: "turbo"}, {mode: 7}, {project_id: "prj_other1234"}, {project_id: "bad"},
{title: ""},
];
invalid.forEach((change) => assert.equal(
normalizeConversation(conversation(change), IDENTITY.userRef, projects), null));
});
test("snapshot collections are bounded and cross-scope items are counted", () => {
const projects = Array.from({length: 101}, (_, index) => project({id: `prj_${String(index).padStart(4, "0")}`}));
const conversations = Array.from({length: 501}, (_, index) => conversation({
id: `conv_${String(index).padStart(4, "0")}`,
project_id: index === 500 ? "prj_0100" : `prj_${String(index % 100).padStart(4, "0")}`,
branch: undefined,
}));
const result = normalizeSnapshot(projects, conversations, IDENTITY.userRef);
assert.equal(result.projects.length, 100);
assert.equal(result.conversations.length, 500);
assert.equal(result.rejected, 2);
const rejected = normalizeSnapshot([project({owner: "usr_aaaaaaaaaaaaaaaa"})],
[conversation({owner: "usr_aaaaaaaaaaaaaaaa"})], IDENTITY.userRef);
assert.equal(rejected.rejected, 2);
const duplicated = normalizeSnapshot([project(), project()], [conversation(), conversation()], IDENTITY.userRef);
assert.equal(duplicated.projects.length, 1);
assert.equal(duplicated.conversations.length, 1);
assert.equal(duplicated.rejected, 2);
});
test("attachments never disappear when their metadata is unavailable", () => {
const item = normalizeConversation(conversation(), IDENTITY.userRef, new Set([PROJECT_ID]));
const views = attachmentViews(item, [
{id: "art_quote1234", title: " Quote \u0000", kind: "document"},
{id: "bad", title: "Bad", kind: "bad"},
]);
assert.equal(views.length, item.artifactIds.length);
assert.deepEqual(views.map((view) => view.missing), [false, true]);
assert.equal(views[0].title, "Quote");
assert.equal(views[1].title, "Attachment details unavailable");
assert.equal(attachmentViews(item, [{id: "art_quote1234", title: {}, kind: {}}])[0].title,
"Untitled artifact");
});
test("lineage is visualizable even when an ancestor is absent or cyclic", () => {
const child = normalizeConversation(conversation(), IDENTITY.userRef, new Set([PROJECT_ID]));
const parent = normalizeConversation(conversation({id: "conv_parent1234", title: "Root",
branch: undefined}), IDENTITY.userRef, new Set([PROJECT_ID]));
const complete = branchLineage(child.id, [child, parent]);
assert.deepEqual(complete.map((node) => node.title), ["Root", "Compare suppliers"]);
assert.deepEqual(complete.map((node) => node.depth), [0, 1]);
const missing = branchLineage(child.id, [child]);
assert.equal(missing[0].missing, true);
assert.equal(missing[1].branchPointMessageId, "msg-12");
const cyclicParent = {...parent, branch: {parentConversationId: child.id,
branchPointMessageId: "msg-cycle"}};
const cycle = branchLineage(child.id, [child, cyclicParent]);
assert.equal(cycle[0].cycle, true);
assert.deepEqual(branchLineage("bad", [child]), []);
});
test("moves preserve stable resume identity, branch, and ordered attachments", () => {
const before = normalizeConversation(conversation(), IDENTITY.userRef, new Set([PROJECT_ID]));
assert.equal(preservesConversation(before, {...before, projectId: null, title: "Renamed"}), true);
assert.equal(preservesConversation(before, {...before, id: "conv_other1234"}), false);
assert.equal(preservesConversation(before, {...before, owner: "usr_aaaaaaaaaaaaaaaa"}), false);
assert.equal(preservesConversation(before, {...before, branch: null}), false);
assert.equal(preservesConversation(before, {...before, artifactIds: ["art_quote1234"]}), false);
assert.equal(preservesConversation(before, {...before,
artifactIds: [...before.artifactIds].reverse()}), false);
});
test("list and search calls use bounded encoded same-origin queries", async () => {
const responses = [
{body: envelope("hux.projects.page.v1", [project()])},
{body: envelope("hux.conversations.page.v1", [conversation()])},
{body: envelope("hux.conversations.search.v1", [conversation()], {next_cursor: null})},
];
const {client, calls} = harness(responses);
const projects = await client.listProjects("cursor_1", 500);
assert.equal(projects.items[0].id, PROJECT_ID);
assert.equal(projects.nextCursor, "next_page");
assert.equal(projects.total, 1);
const ids = new Set([PROJECT_ID]);
await client.listConversations(PROJECT_ID, ids, null, 12);
await client.searchConversations({query: " cabinets & quotes ", projectId: PROJECT_ID,
tags: ["home", "suppliers"], pinned: false, limit: 20}, ids);
assert.match(calls[0].url, /^\/hux\/v1\/projects\?cursor=cursor_1&limit=100$/);
assert.match(calls[1].url, /\/projects\/prj_alpha1234\/conversations\?limit=12$/);
assert.match(calls[2].url, /q=cabinets\+%26\+quotes/);
assert.match(calls[2].url, /project_id=prj_alpha1234/);
assert.match(calls[2].url, /tag=home&tag=suppliers/);
assert.match(calls[2].url, /pinned=false/);
calls.forEach(({init}) => {
assert.equal(init.credentials, "same-origin");
assert.equal(init.cache, "no-store");
assert.equal(init.headers.Accept, "application/vnd.hermes.hux+json; version=1");
});
});
test("transport rejects invalid capability, identity, pagination, records, and boundaries", async () => {
const disabled = harness([{body: {}}], [FOUNDATION_FLAG]);
await assert.rejects(disabled.client.listProjects(), /not enabled/);
disabled.foundation.apiVersion = "hux.v0";
await assert.rejects(disabled.client.listProjects(), /not enabled/);
assert.throws(() => createOrganizationClient({client: null, fetcher: async () => ({})}), /requires/);
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = undefined;
assert.throws(() => createOrganizationClient({client: disabled.foundation}), /requires/);
} finally { globalThis.fetch = originalFetch; }
const failures = [
{body: null},
{body: []},
{body: envelope("wrong", [])},
{body: envelope("hux.projects.page.v1", [], {api_version: "hux.v0"})},
{body: envelope("hux.projects.page.v1", [], {identity: {...RAW_IDENTITY, surface: "worker"}})},
{body: envelope("hux.projects.page.v1", [], {next_cursor: "bad/cursor"})},
{body: envelope("hux.projects.page.v1", [], {total: -1})},
{body: envelope("hux.projects.page.v1", Array(101).fill(project()))},
{body: envelope("hux.projects.page.v1", [project({id: "bad"})])},
{body: envelope("hux.projects.page.v1", [project(), project()])},
];
for (const response of failures) {
const current = harness([response]);
await assert.rejects(current.client.listProjects(), OrganizationContractError);
}
const http = harness([{ok: false, status: 503}]);
await assert.rejects(http.client.listProjects(), /failed \(503\)/);
const conflict = harness([{ok: false, status: 409}]);
await assert.rejects(conflict.client.listProjects(), /changed/);
const badCursor = harness([]);
await assert.rejects(badCursor.client.listProjects("bad/cursor"), /cursor/);
await assert.rejects(badCursor.client.listConversations("prj_other1234",
new Set([PROJECT_ID])), /outside/);
await assert.rejects(badCursor.client.searchConversations({query: "", limit: 1}, new Set()), /1 to 200/);
await assert.rejects(badCursor.client.searchConversations({query: "x".repeat(201)}, new Set()), /1 to 200/);
await assert.rejects(badCursor.client.searchConversations({query: "x", projectId: "prj_other1234"},
new Set([PROJECT_ID])), /outside/);
await assert.rejects(badCursor.client.searchConversations({query: "x", tags: ["Bad"]},
new Set()), /tags/);
});
test("conversation pages cannot cross their requested project", async () => {
const ids = new Set([PROJECT_ID, "prj_other1234"]);
const wrong = harness([{body: envelope("hux.conversations.page.v1",
[conversation({project_id: "prj_other1234"})])}]);
await assert.rejects(wrong.client.listConversations(PROJECT_ID, ids), /project boundary/);
const invalid = harness([{body: envelope("hux.conversations.search.v1", [conversation({id: "bad"})])}]);
await assert.rejects(invalid.client.searchConversations({query: "x"}, ids), /Invalid conversation/);
const oversized = harness([{body: envelope("hux.conversations.search.v1", Array(101).fill(conversation()))}]);
await assert.rejects(oversized.client.searchConversations({query: "x"}, ids), /exceeds/);
const duplicate = harness([{body: envelope("hux.conversations.search.v1", [conversation(), conversation()])}]);
await assert.rejects(duplicate.client.searchConversations({query: "x"}, ids), /Invalid conversation/);
});
test("project mutations send only scoped fields and verify stable identity", async () => {
const before = normalizeProject(project(), IDENTITY.userRef);
const changed = project({name: "New name", tags: ["new"], pinned: false,
updated_at: "2026-08-24T10:00:00Z"});
const {client, calls} = harness([{body: mutation("hux.project.response.v1", changed)}]);
const after = await client.updateProject(before, {name: " New name ", tags: ["new"], pinned: false,
expectedUpdatedAt: before.updatedAt});
assert.equal(after.name, "New name");
const body = JSON.parse(calls[0].init.body);
assert.deepEqual(body, {expected_updated_at: before.updatedAt, name: "New name", tags: ["new"], pinned: false});
assert.equal("tenant_ref" in body, false);
assert.equal(calls[0].init.method, "PATCH");
for (const [patch, message] of [
[{expectedUpdatedAt: "bad"}, /outside/],
[{expectedUpdatedAt: before.updatedAt, name: ""}, /invalid/],
[{expectedUpdatedAt: before.updatedAt, tags: ["Bad"]}, /invalid/],
]) {
await assert.rejects(client.updateProject(before, patch), message);
}
await assert.rejects(client.updateProject({...before, owner: "usr_aaaaaaaaaaaaaaaa"},
{expectedUpdatedAt: before.updatedAt}), /outside/);
const noItem = harness([{body: mutation("hux.project.response.v1", changed, {item: undefined})}]);
await assert.rejects(noItem.client.updateProject(before,
{expectedUpdatedAt: before.updatedAt}), /no item/);
const changedId = harness([{body: mutation("hux.project.response.v1", project({id: "prj_other1234"}))}]);
await assert.rejects(changedId.client.updateProject(before,
{expectedUpdatedAt: before.updatedAt}), /identity changed/);
});
test("conversation mutation proves atomic move invariants", async () => {
const ids = new Set([PROJECT_ID, "prj_other1234"]);
const before = normalizeConversation(conversation(), IDENTITY.userRef, ids);
const moved = conversation({project_id: "prj_other1234", title: "Renamed", tags: ["new"], pinned: true,
updated_at: "2026-08-24T10:00:00Z"});
const {client, calls} = harness([{body: mutation("hux.conversation.response.v1", moved)}]);
const after = await client.updateConversation(before, {projectId: "prj_other1234", title: " Renamed ",
tags: ["new"], pinned: true, expectedUpdatedAt: before.updatedAt}, ids);
assert.equal(after.projectId, "prj_other1234");
assert.deepEqual(JSON.parse(calls[0].init.body), {expected_updated_at: before.updatedAt,
title: "Renamed", project_id: "prj_other1234", tags: ["new"], pinned: true});
for (const [candidate, error] of [
[{...moved, id: "conv_other1234"}, /lineage/],
[{...moved, branch: undefined}, /lineage/],
[{...moved, artifact_ids: ["art_quote1234"]}, /lineage/],
[{...moved, owner: "usr_aaaaaaaaaaaaaaaa"}, /lineage/],
]) {
const current = harness([{body: mutation("hux.conversation.response.v1", candidate)}]);
await assert.rejects(current.client.updateConversation(before,
{expectedUpdatedAt: before.updatedAt}, ids), error);
}
const invalid = harness([]);
await assert.rejects(invalid.client.updateConversation({...before, owner: "usr_aaaaaaaaaaaaaaaa"},
{expectedUpdatedAt: before.updatedAt}, ids), /outside/);
await assert.rejects(invalid.client.updateConversation({...before, projectId: "prj_missing1234"},
{expectedUpdatedAt: before.updatedAt}, ids), /outside/);
await assert.rejects(invalid.client.updateConversation(before,
{expectedUpdatedAt: "bad"}, ids), /outside/);
await assert.rejects(invalid.client.updateConversation(before,
{expectedUpdatedAt: before.updatedAt, projectId: "prj_missing1234"}, ids), /outside/);
await assert.rejects(invalid.client.updateConversation(before,
{expectedUpdatedAt: before.updatedAt, title: ""}, ids), /invalid/);
await assert.rejects(invalid.client.updateConversation(before,
{expectedUpdatedAt: before.updatedAt, tags: ["Bad"]}, ids), /invalid/);
});