import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { createMemoryEndpointContract } from "../../dockerfiles/hermes-webui-hux/memory/endpoints.ts"; import { FOUNDATION_FLAG, MEMORY_CONTROL_FLAG, MEMORY_TOPICS, PRIVACY_FLAG, memoryControlEnabled, normalizeMemoryPage, } from "../../dockerfiles/hermes-webui-hux/memory/model.ts"; import { MemoryContractError, isControlReference, isOpaqueMemoryId, isUtcTimestamp, normalizeIdentity, safeMemoryText, sameIdentity, } from "../../dockerfiles/hermes-webui-hux/memory/security.ts"; const IDENTITY = { tenantRef: "tnt_0123456789abcdef", userRef: "usr_0123456789abcdef", surface: "chat", }; const FLAGS = [FOUNDATION_FLAG, PRIVACY_FLAG, MEMORY_CONTROL_FLAG]; function entry(index = 1, extra = {}) { return { schema: "hux.memory.v1", id: `mem_test${String(index).padStart(4, "0")}`, owner: IDENTITY.userRef, scope: { level: "conversation", scope_id: "conv_test1234" }, kind: "preference", content: `Concise answer preference ${index}`, status: "active", approval_mode: "ask", sensitivity: "personal", topic: "general", ttl: { policy: "decay", decay_days: 180 }, source: { kind: "message", id: `msg-${index}`, uri: "https://secret" }, provenance: { surface: "chat", actor: { type: "assistant", id: "hermes", display: "do not copy" }, recorded_at: "2026-08-24T10:00:00Z", route: { requested: "thoughtful", resolved_target: "secret-model" }, }, created_at: "2026-08-24T10:00:00Z", updated_at: `2026-08-24T10:00:${String(index % 60).padStart(2, "0")}Z`, audit: [ { at: "2026-08-24T10:00:00Z", action: "approved", actor: { type: "user", id: "private-subject" }, note: "Useful response preference", }, ], ...extra, }; } function page(entries, extra = {}) { return { schema: "hux.memory_page.v1", identity: { tenant_ref: IDENTITY.tenantRef, user_ref: IDENTITY.userRef, surface: IDENTITY.surface, }, entries, ...extra, }; } test("memory control requires foundation, privacy, and memory flags", () => { assert.equal(memoryControlEnabled(), false); assert.equal(memoryControlEnabled([]), false); assert.equal(memoryControlEnabled([MEMORY_CONTROL_FLAG]), false); assert.equal(memoryControlEnabled([FOUNDATION_FLAG, MEMORY_CONTROL_FLAG]), false); assert.equal(memoryControlEnabled([FOUNDATION_FLAG, PRIVACY_FLAG]), false); assert.equal(memoryControlEnabled(FLAGS), true); }); test("memory topics stay aligned with the existing memory and privacy schemas", () => { const memory = JSON.parse( readFileSync("services/hermes/contracts/hux/memory.schema.json"), ); const privacy = JSON.parse( readFileSync("services/hermes/contracts/hux/privacy.schema.json"), ); assert.deepEqual(MEMORY_TOPICS, memory.properties.topic.enum); assert.deepEqual( MEMORY_TOPICS.filter((topic) => topic !== "general"), privacy.$defs.topic.enum, ); }); test("display text scrubs credentials, controls, and object payloads", () => { assert.equal(safeMemoryText({}, "fallback"), "fallback"); assert.equal(safeMemoryText(" \n ", "fallback"), "fallback"); assert.equal(safeMemoryText("abc\u0000def", "fallback"), "abc def"); assert.equal(safeMemoryText("abcdef", "fallback", 4), "abc…"); assert.equal(safeMemoryText("abcdef", "fallback", 0), "…"); const scrubbed = safeMemoryText( [ "password=hunter2", "Bearer abc.DEF_123", "eyJabcdefgh.abcdefghijkl.abcdefghijkl", "https://user:pass@example.test", "Abcdefghijklmnopqrstuvwxyz1234567890ABCD", "-----BEGIN PRIVATE KEY----- nope -----END PRIVATE KEY-----", ].join(" "), "fallback", 1000, ); assert.doesNotMatch(scrubbed, /hunter2|abc\.DEF|user:pass|1234567890ABCD|nope/); assert.match(scrubbed, /redacted/); }); test("scope primitives reject malformed identities, ids, and timestamps", () => { assert.equal(isOpaqueMemoryId("mem_abcd"), true); assert.equal(isOpaqueMemoryId("evt_abcd"), false); assert.equal(isOpaqueMemoryId(4), false); assert.equal(isControlReference("msg-42"), true); assert.equal(isControlReference("conv_test1234", "conv"), true); assert.equal(isControlReference("msg-42", "conv"), false); assert.equal(isControlReference("../message"), false); assert.equal(isControlReference(4), false); assert.equal(isUtcTimestamp("2026-08-24T10:00:00.123Z"), true); assert.equal(isUtcTimestamp("2026-90-24T10:00:00Z"), false); assert.equal(isUtcTimestamp(4), false); assert.deepEqual(normalizeIdentity(IDENTITY), IDENTITY); assert.equal(sameIdentity(IDENTITY, { ...IDENTITY }), true); assert.equal(sameIdentity(IDENTITY, { ...IDENTITY, surface: "worker" }), false); for (const bad of [null, [], {}, { ...IDENTITY, tenantRef: "tenant" }, { ...IDENTITY, userRef: "user" }, { ...IDENTITY, surface: "web" }]) { assert.throws(() => normalizeIdentity(bad), MemoryContractError); } }); test("a valid ledger exposes useful labels but no raw provenance or source ids", () => { const result = normalizeMemoryPage( page([ entry(1), entry(2, { scope: { level: "global" }, ttl: { policy: "never" }, kind: "fact", topic: "finance", source: { kind: "file", id: "/private/path" }, provenance: { surface: "chat", actor: { type: "user", id: "private-user" }, recorded_at: "2026-08-24T10:00:00Z", }, audit: [{ action: "approved", actor: { type: "user", id: "user" } }], }), entry(3, { scope: { level: "project", scope_id: "prj_test1234" }, ttl: { policy: "expires_at", expires_at: "2026-09-24T10:00:00Z" }, kind: "instruction", status: "proposed", source: { kind: "approval", id: "apr-secret" }, audit: [{ note: "token=do-not-display" }], }), ], { next_cursor: "cursor.2" }), IDENTITY, ); assert.equal(result.invalid, 0); assert.equal(result.nextCursor, "cursor.2"); assert.deepEqual(result.entries.map((item) => item.id), ["mem_test0003", "mem_test0002", "mem_test0001"]); assert.equal(result.entries[0].scopeLabel, "This project"); assert.equal(result.entries[0].ttlLabel, "Expires 2026-09-24T10:00:00Z"); assert.equal(result.entries[0].reason, "token=[redacted]"); assert.equal(result.entries[1].scopeLabel, "All conversations"); assert.equal(result.entries[1].ttlLabel, "Kept until you forget it"); assert.equal(result.entries[1].reason, "Saved for future help"); assert.equal(result.entries[1].sourceLabel, "Workspace file"); assert.equal(result.entries[1].sourceIsMessage, false); assert.equal(result.entries[2].sourceIsMessage, true); assert.equal(result.entries[1].provenanceLabel, "user via chat"); const serialized = JSON.stringify(result.entries); assert.doesNotMatch(serialized, /private\/path|private-user|private-subject|secret-model|apr-secret|do-not-display/); }); test("forgotten and sensitive values fail closed without leaking reasons", () => { const forbidden = "kidney stone and account secret"; const result = normalizeMemoryPage( page([ entry(1, { status: "forgotten", content: forbidden, audit: [{ note: forbidden }] }), entry(2, { sensitivity: "sensitive", topic: "health", content: forbidden, audit: [{ note: forbidden }] }), entry(3, { sensitivity: "restricted", topic: "credentials", content: forbidden }), entry(4, { sensitivity: "public", content: "visible" }), ]), IDENTITY, ); const [publicItem, restricted, sensitive, forgotten] = result.entries; assert.equal(publicItem.content, "visible"); assert.equal(restricted.content, null); assert.equal(sensitive.content, null); assert.equal(forgotten.content, null); assert.equal(forgotten.contentNotice, "Content permanently removed"); assert.match(sensitive.contentNotice, /privacy controls/); assert.doesNotMatch(JSON.stringify(result), new RegExp(forbidden)); assert.equal(sensitive.reason, "Reason hidden by privacy controls"); }); test("an omitted optional topic defaults to general without changing approval", () => { const item = normalizeMemoryPage( page([ entry(8, { topic: undefined, status: "proposed", audit: [{ action: "proposed", actor: { type: "assistant", id: "hermes" } }], }), ]), IDENTITY, ).entries[0]; assert.equal(item.topic, "general"); assert.equal(item.approvalMode, "ask"); assert.equal(item.reason, "Hermes suggested this for your approval"); }); test("malformed or cross-owner entries are withheld independently", () => { const invalid = [ entry(1, { schema: "hux.memory.v0" }), entry(2, { id: "evt_bad" }), entry(3, { owner: "usr_ffffffffffffffff" }), entry(4, { kind: "password" }), entry(5, { status: "deleted" }), entry(6, { sensitivity: "secret" }), entry(7, { topic: "politics" }), entry(8, { approval_mode: "yes" }), entry(9, { created_at: "today" }), entry(10, { updated_at: "today" }), entry(11, { provenance: null }), entry(12, { provenance: { surface: "worker", actor: { type: "assistant" }, recorded_at: "2026-08-24T10:00:00Z" } }), entry(13, { provenance: { surface: "chat", actor: { type: "assistant" }, recorded_at: "today" } }), entry(14, { provenance: { surface: "chat", actor: null, recorded_at: "2026-08-24T10:00:00Z" } }), entry(15, { provenance: { surface: "chat", actor: { type: "intruder" }, recorded_at: "2026-08-24T10:00:00Z" } }), entry(16, { source: null }), entry(17, { source: { kind: "raw_tool_log", id: "secret" } }), entry(18, { source: { kind: "message", id: "" } }), entry(19, { source: { kind: "message", id: "x".repeat(201) } }), entry(20, { ttl: { policy: "expires_at", expires_at: "today" } }), entry(21, { ttl: { policy: "decay", decay_days: 0 } }), entry(22, { ttl: { policy: "decay", decay_days: 3651 } }), entry(23, { ttl: {} }), entry(24, { scope: null }), entry(25, { scope: { level: "global", scope_id: "should-not-exist" } }), entry(26, { scope: { level: "project" } }), entry(27, { scope: { level: "conversation" } }), entry(28, { content: {} }), entry(29, { content: "x".repeat(2001) }), entry(30, { audit: [] }), entry(31, { audit: null }), entry(32, { provenance: { surface: "chat", actor: { type: "assistant", id: "" }, recorded_at: "2026-08-24T10:00:00Z" } }), entry(33, { provenance: { surface: "chat", actor: { type: "assistant", id: "x".repeat(121) }, recorded_at: "2026-08-24T10:00:00Z" } }), ]; const result = normalizeMemoryPage(page([entry(99), ...invalid]), IDENTITY); assert.equal(result.entries.length, 1); assert.equal(result.invalid, invalid.length); assert.equal(result.nextCursor, null); }); test("response identity mismatches fail the whole page closed", () => { assert.throws(() => normalizeMemoryPage(null, IDENTITY), MemoryContractError); assert.throws(() => normalizeMemoryPage({ schema: "old" }, IDENTITY), MemoryContractError); assert.throws( () => normalizeMemoryPage(page([], { identity: { ...page([]).identity, tenant_ref: "tnt_ffffffffffffffff" } }), IDENTITY), /scope boundary/, ); assert.throws( () => normalizeMemoryPage(page([], { identity: { ...page([]).identity, user_ref: "usr_ffffffffffffffff" } }), IDENTITY), /scope boundary/, ); assert.throws( () => normalizeMemoryPage(page([], { identity: { ...page([]).identity, surface: "worker" } }), IDENTITY), /scope boundary/, ); assert.throws( () => normalizeMemoryPage(page(null), IDENTITY), /entries are missing/, ); }); test("page bounds retain newest valid ledger entries", () => { const many = Array.from({ length: 205 }, (_, index) => entry(index + 1)); const result = normalizeMemoryPage(page(many, { next_cursor: 7 }), IDENTITY); assert.equal(result.entries.length, 200); assert.equal(result.truncated, 5); assert.equal(result.invalid, 0); assert.equal(result.nextCursor, null); assert.ok(result.entries[0].updatedAt >= result.entries.at(-1).updatedAt); }); test("endpoint contract is inert by default and describes same-origin calls only", () => { const client = { apiVersion: "hux.v1", identity: IDENTITY, endpoint(path) { assert.match(path, /^\/[a-z0-9/_-]+$/); return `/hux/v1${path}`; }, }; assert.equal(createMemoryEndpointContract(client), null); assert.equal(createMemoryEndpointContract(client, [MEMORY_CONTROL_FLAG]), null); const contract = createMemoryEndpointContract(client, FLAGS); assert.deepEqual(contract.list, { method: "GET", path: "/hux/v1/memories", responseSchema: "hux.memory_page.v1", }); assert.deepEqual(contract.identity, IDENTITY); assert.throws(() => { contract.identity.surface = "worker"; }, TypeError); assert.equal(contract.suggestOnly.method, "PUT"); assert.equal(contract.doNotRemember.path, "/hux/v1/memory-blocks"); assert.equal(contract.action("mem_abcd", "approve").path, "/hux/v1/memories/mem_abcd/approve"); assert.equal(contract.action("mem_abcd", "reject").method, "POST"); assert.equal(contract.action("mem_abcd", "forget").responseSchema, "hux.memory.v1"); assert.equal(contract.revise("mem_abcd").path, "/hux/v1/memories/mem_abcd/revisions"); assert.throws(() => contract.action("bad", "approve"), /Opaque/); assert.throws(() => contract.action("mem_abcd", "delete"), /Unsupported/); assert.throws( () => createMemoryEndpointContract({ ...client, apiVersion: "hux.v2" }, FLAGS), /HUX v1/, ); assert.throws( () => createMemoryEndpointContract({ ...client, identity: { ...IDENTITY, userRef: "bad" } }, FLAGS), MemoryContractError, ); });