import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { ACTIVITY_TIMELINE_FLAG, FOUNDATION_FLAG, activityTimelineEnabled, mergeActivityEvents, normalizeCancellationReceipt, surfacePolicy, } from "../../dockerfiles/hermes-webui-hux/activity/model.ts"; import { isOpaqueId, isRfc3339Utc, safeEvidenceLabel, safeText, } from "../../dockerfiles/hermes-webui-hux/activity/security.ts"; const CONVERSATION = "conv_test1234"; const KINDS = JSON.parse( readFileSync("services/hermes/contracts/hux/event.schema.json"), ).properties.kind.enum; function event(seq, kind = "tool.call", extra = {}) { return { schema: "hux.event.v1", id: `evt_test${String(seq).padStart(4, "0")}`, seq, ts: `2026-08-24T10:${String(Math.floor(seq / 60)).padStart(2, "0")}:${String(seq % 60).padStart(2, "0")}Z`, conversation_id: CONVERSATION, kind, summary: `Safe update ${seq}`, evidence: [], provenance: { surface: "chat" }, sensitivity: "personal", redaction: { level: "none" }, ...extra, }; } test("feature remains default-off and depends on the foundation", () => { assert.equal(activityTimelineEnabled(), false); assert.equal(activityTimelineEnabled([]), false); assert.equal(activityTimelineEnabled([ACTIVITY_TIMELINE_FLAG]), false); assert.equal(activityTimelineEnabled([FOUNDATION_FLAG]), false); assert.equal( activityTimelineEnabled([FOUNDATION_FLAG, ACTIVITY_TIMELINE_FLAG]), true, ); }); test("Chat policy is intentionally calmer and every returned policy is isolated", () => { const chat = surfacePolicy("chat"); const worker = surfacePolicy("worker"); assert.ok(chat.maxItems < worker.maxItems); assert.ok(chat.initialItems < worker.initialItems); assert.ok(chat.evidenceLimit < worker.evidenceLimit); assert.equal(chat.expandRoutineEvents, false); assert.equal(worker.expandRoutineEvents, true); chat.maxItems = 1; assert.equal(surfacePolicy("chat").maxItems, 80); assert.deepEqual(surfacePolicy("api"), worker); assert.ok( surfacePolicy("voice").maxItems < surfacePolicy("telegram").maxItems, ); }); test("plain-text security redacts credential shapes without serializing objects", () => { assert.equal(safeText({ token: "nope" }, "fallback"), "fallback"); assert.equal(safeText(" \n\t ", "fallback"), "fallback"); assert.equal(safeText("abc\u0000def", "fallback"), "abc def"); assert.equal(safeText("abcdef", "fallback", 4), "abc…"); assert.equal(safeText("abcdef", "fallback", 0), "…"); const unsafe = [ "password=hunter2", "Authorization: Basic-abcd", "Bearer abc.DEF_123", "eyJabcdefgh.abcdefghijkl.abcdefghijkl", "https://user:pass@example.test/path", "Abcdefghijklmnopqrstuvwxyz1234567890ABCD", "-----BEGIN PRIVATE KEY----- top secret -----END PRIVATE KEY-----", ].join(" "); const scrubbed = safeText(unsafe, "fallback", 1000); assert.doesNotMatch( scrubbed, /hunter2|Basic-abcd|abc\.DEF_123|user:pass|top secret|1234567890ABCD/, ); assert.match(scrubbed, /redacted/); }); test("only allow-listed evidence labels cross the display boundary", () => { assert.deepEqual(safeEvidenceLabel("tool_result"), { kind: "tool_result", label: "Tool result", }); assert.deepEqual(safeEvidenceLabel("url"), { kind: "url", label: "Website" }); assert.equal(safeEvidenceLabel("raw_tool_arguments"), null); assert.equal(safeEvidenceLabel(7), null); assert.equal(isOpaqueId("evt_abcd", "evt"), true); assert.equal(isOpaqueId("evt_abcd"), true); assert.equal(isOpaqueId("mem_abcd", "evt"), false); assert.equal(isOpaqueId("not opaque"), false); assert.equal(isOpaqueId(7), false); assert.equal(isRfc3339Utc("2026-08-24T10:00:00.123Z"), true); assert.equal(isRfc3339Utc("today"), false); assert.equal(isRfc3339Utc("2026-99-99T10:00:00Z"), false); assert.equal(isRfc3339Utc(7), false); }); test("every contract event maps to a compact semantic phase", () => { const records = KINDS.map((kind, seq) => event(seq, kind, { parent_event_id: kind === "run.started" ? "evt_parent1234" : undefined, }), ); const result = mergeActivityEvents([], records, CONVERSATION, "worker"); assert.equal(result.invalid, 0); assert.equal(result.items.length, KINDS.length); const phases = new Set(result.items.map((item) => item.phase)); assert.deepEqual( phases, new Set([ "intent", "action", "evidence", "delegation", "decision", "completion", "failure", ]), ); assert.equal( result.items.find((item) => item.kind === "run.failed").tone, "warning", ); assert.equal( result.items.find((item) => item.kind === "run.completed").tone, "success", ); assert.equal( result.items.find((item) => item.kind === "approval.requested").tone, "warning", ); assert.equal( result.items.find((item) => item.kind === "tool.call").routine, true, ); assert.equal( result.items.find((item) => item.kind === "message.user").routine, false, ); }); test("root runs are actions while child runs are explicit delegations", () => { const result = mergeActivityEvents( [], [ event(1, "run.started"), event(2, "run.started", { parent_event_id: "evt_parent1234" }), ], CONVERSATION, "chat", ); assert.deepEqual( result.items.map((item) => item.phase), ["action", "delegation"], ); assert.deepEqual( result.items.map((item) => item.tone), ["active", "active"], ); }); test("normalization redacts summaries and never projects detail, IDs, or URIs", () => { const raw = event(1, "tool.result", { summary: "token=secret-value result is ready", run_id: "run-safe", detail: { arguments: "--token secret", result: "raw secret result" }, evidence: [ { kind: "tool_result", id: "secret-id", uri: "https://user:pass@example.test", }, { kind: "file", id: "/secret/path" }, { kind: "bogus", id: "raw" }, { kind: "url", id: "https://secret" }, ], }); const item = mergeActivityEvents([], [raw], CONVERSATION, "chat").items[0]; const serialized = JSON.stringify(item); assert.equal(item.runId, "run-safe"); assert.deepEqual( item.evidence.map((entry) => entry.label), ["Tool result", "File", "Website"], ); assert.doesNotMatch( serialized, /secret-value|raw secret result|secret-id|secret\/path|user:pass/, ); assert.equal("detail" in item, false); const invalidRun = mergeActivityEvents( [], [event(2, "tool.call", { run_id: { raw: true } })], CONVERSATION, "chat", ); assert.equal(invalidRun.items[0].runId, undefined); }); test("full or restricted redaction ignores even an otherwise safe summary", () => { const result = mergeActivityEvents( [], [ event(1, "tool.call", { summary: "do not show", redaction: { level: "full" }, }), event(2, "run.failed", { summary: "health detail", sensitivity: "restricted", }), event(3, "run.completed", { summary: {}, redaction: null }), event(4, "citation.attached", { summary: "", redaction: { level: "partial" }, }), ], CONVERSATION, "chat", ); assert.deepEqual( result.items.map((item) => item.summary), [ "Hermes used a tool", "The run failed; sensitive detail is hidden", "The run completed", "Activity recorded; sensitive detail is hidden", ], ); }); test("reconnect merge is ordered, idempotent, and fail-closed on conflicts", () => { const first = mergeActivityEvents( [], [event(3), event(1)], CONVERSATION, "chat", ); const replay = { ...event(3), summary: "attempted rewrite" }; const idConflict = { ...event(4), id: first.items[0].id }; const seqConflict = { ...event(1), id: "evt_conflict1" }; const result = mergeActivityEvents( first.items, [replay, idConflict, seqConflict, event(2)], CONVERSATION, "chat", ); assert.deepEqual( result.items.map((item) => item.seq), [1, 2, 3], ); assert.equal(result.items.at(-1).summary, "Safe update 3"); assert.equal(result.replays, 1); assert.equal(result.conflicts, 2); assert.equal(result.afterSeq, 3); assert.equal(result.invalid, 0); }); test("an initial same-sequence conflict resolves deterministically", () => { const higher = { ...event(7), id: "evt_zulu1234" }; const lower = { ...event(7), id: "evt_alpha1234", summary: "Deterministic owner", }; const forward = mergeActivityEvents( [], [higher, lower], CONVERSATION, "chat", ); const reverse = mergeActivityEvents( [], [lower, higher], CONVERSATION, "chat", ); assert.deepEqual(forward.items, reverse.items); assert.equal(forward.items[0].id, "evt_alpha1234"); assert.equal(forward.conflicts, 1); }); test("malformed and cross-conversation events are rejected", () => { const bad = [ { ...event(1), schema: "hux.event.v0" }, { ...event(2), id: "bad" }, { ...event(3), seq: -1 }, { ...event(4), ts: "later" }, { ...event(5), conversation_id: "conv_other1234" }, { ...event(6), kind: "tool.raw" }, ]; const result = mergeActivityEvents([], bad, CONVERSATION, "chat"); assert.deepEqual(result.items, []); assert.equal(result.invalid, bad.length); assert.equal(result.afterSeq, -1); }); test("retention is bounded per surface while preserving the resume watermark", () => { const records = Array.from({ length: 85 }, (_, seq) => event(seq)); const result = mergeActivityEvents([], records, CONVERSATION, "chat"); assert.equal(result.items.length, 80); assert.equal(result.items[0].seq, 5); assert.equal(result.afterSeq, 84); assert.equal(result.truncated, 5); const replayed = mergeActivityEvents( result.items, [event(84)], CONVERSATION, "chat", ); assert.equal(replayed.replays, 1); assert.equal(replayed.truncated, 0); }); test("evidence display and cancellation receipts obey surface bounds", () => { const evidence = Array.from({ length: 10 }, (_, index) => ({ kind: index % 2 ? "file" : "source", id: `secret-${index}`, uri: `https://token-${index}`, })); assert.equal( mergeActivityEvents( [], [event(1, "citation.attached", { evidence })], CONVERSATION, "chat", ).items[0].evidence.length, 3, ); assert.equal( mergeActivityEvents( [], [event(1, "citation.attached", { evidence })], CONVERSATION, "worker", ).items[0].evidence.length, 8, ); const receipt = { schema: "hux.cancel_receipt.v1", id: "rcpt_test1234", run_id: "run_1", requested_at: "2026-08-24T10:00:00Z", acknowledged_at: "2026-08-24T10:00:01Z", completed_at: "not-a-time", outcome: "cancelled", side_effects: Array.from({ length: 7 }, (_, index) => ({ description: index === 0 ? "token=do-not-show" : `Effect ${index}`, reverted: index % 2 === 0, evidence: { kind: "file", id: "secret" }, })), }; const safe = normalizeCancellationReceipt(receipt, "chat"); assert.equal(safe.sideEffects.length, 4); assert.equal(safe.omittedEffects, 3); assert.equal(safe.completedAt, undefined); assert.equal(safe.acknowledgedAt, receipt.acknowledged_at); assert.doesNotMatch(JSON.stringify(safe), /do-not-show|"evidence"|"secret"/); }); test("cancellation parsing rejects bad identity, time, run, and outcome", () => { const base = { schema: "hux.cancel_receipt.v1", id: "rcpt_test1234", run_id: "run_1", requested_at: "2026-08-24T10:00:00Z", outcome: "already_complete", side_effects: [{ description: {}, reverted: false }], }; const safe = normalizeCancellationReceipt(base, "worker"); assert.equal(safe.sideEffects[0].description, "A side effect was recorded"); assert.equal(safe.completedAt, undefined); assert.equal( normalizeCancellationReceipt({ ...base, schema: "old" }, "chat"), null, ); assert.equal( normalizeCancellationReceipt({ ...base, id: "bad" }, "chat"), null, ); assert.equal( normalizeCancellationReceipt({ ...base, run_id: 3 }, "chat"), null, ); assert.equal( normalizeCancellationReceipt({ ...base, requested_at: "bad" }, "chat"), null, ); assert.equal( normalizeCancellationReceipt({ ...base, outcome: "unknown" }, "chat"), null, ); assert.equal( normalizeCancellationReceipt({ ...base, side_effects: null }, "chat") .sideEffects.length, 0, ); });