atlas-iac/testing/tests/test_hermes_hux_ui_memory.mjs

335 lines
14 KiB
JavaScript
Raw Normal View History

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,
);
});