mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
221 lines
9.2 KiB
TypeScript
221 lines
9.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { appendPendingMemories } from "../src/pending-journal.ts";
|
|
import { saveSessionState } from "../src/session-state.ts";
|
|
import { MEMORY_TYPE_ORDER } from "../src/memory-kind-policy.ts";
|
|
import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "../src/types.ts";
|
|
import { workspaceMemoryPath } from "../src/paths.ts";
|
|
import { saveWorkspaceMemory } from "../src/workspace-memory.ts";
|
|
import {
|
|
formatMemoryHelp,
|
|
formatMemoryList,
|
|
formatMemoryStatus,
|
|
getMemoryList,
|
|
getMemoryStatus,
|
|
renderMemoryCommand,
|
|
} from "../src/memory-visibility.ts";
|
|
|
|
async function tempRoot(): Promise<string> {
|
|
return mkdtemp(join(tmpdir(), "memory-visibility-test-"));
|
|
}
|
|
|
|
function memory(id: string, text: string, overrides: Partial<LongTermMemoryEntry> = {}): LongTermMemoryEntry {
|
|
const now = new Date().toISOString();
|
|
return {
|
|
id,
|
|
type: "decision",
|
|
text,
|
|
source: "compaction",
|
|
confidence: 0.8,
|
|
status: "active",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("formats status counts from workspace, session, and pending journal stores", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const now = new Date().toISOString();
|
|
const store: WorkspaceMemoryStore = {
|
|
version: 1,
|
|
workspace: { root, key: "test" },
|
|
limits: { maxRenderedChars: 115, maxEntries: 28 },
|
|
entries: [
|
|
memory("mem-short", "Keep tests focused."),
|
|
memory("mem-long", "Long memory with password: sushi ".repeat(20), { type: "reference" }),
|
|
memory("mem-old", "Superseded memory should not be active.", { status: "superseded" }),
|
|
],
|
|
migrations: [],
|
|
updatedAt: now,
|
|
};
|
|
await saveWorkspaceMemory(root, store);
|
|
await saveSessionState(root, {
|
|
version: 1,
|
|
sessionID: "ses_status",
|
|
turn: 1,
|
|
updatedAt: now,
|
|
activeFiles: [],
|
|
openErrors: [{
|
|
id: "err-1",
|
|
category: "typecheck",
|
|
summary: "Typecheck failed",
|
|
fingerprint: "typecheck",
|
|
status: "open",
|
|
firstSeen: Date.now(),
|
|
lastSeen: Date.now(),
|
|
seenCount: 1,
|
|
}],
|
|
recentDecisions: [{ id: "dec-1", text: "Prefer local rendering", source: "user", createdAt: Date.now() }],
|
|
pendingMemories: [memory("pending-session", "Pending for this session", { source: "explicit", pendingOwnerSessionID: "ses_status" })],
|
|
compactionMemoryRefs: [],
|
|
});
|
|
await appendPendingMemories(root, [memory("pending-journal", "Pending in durable journal", { source: "explicit", pendingOwnerSessionID: "ses_status" })]);
|
|
|
|
const output = formatMemoryStatus(await getMemoryStatus(root, "ses_status"));
|
|
|
|
assert.match(output, /^## Memory status/);
|
|
assert.match(output, /Workspace:/);
|
|
assert.match(output, /- Active memories: 2/);
|
|
assert.match(output, /- Rendered in prompt: 1/);
|
|
assert.match(output, /- Omitted active memories: 1/);
|
|
assert.match(output, /- Superseded memories: 1/);
|
|
assert.match(output, /Pending:/);
|
|
assert.match(output, /- Pending in this session: 1/);
|
|
assert.match(output, /- Pending journal memories: 1/);
|
|
assert.match(output, /Session:/);
|
|
assert.match(output, /- Open errors: 1/);
|
|
assert.match(output, /- Recent decisions: 1/);
|
|
assert.match(output, /Use \/memory → Current memories to browse current \[M1\]-\[M28\] memory refs\./);
|
|
assert.match(output, /Local only: no LLM request was made\./);
|
|
assert.equal(output.includes("Recent active memory previews"), false);
|
|
assert.equal(output.includes("sushi"), false, "status output should not include memory previews");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("getMemoryStatus redacts previews without rewriting workspace memory", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const now = new Date().toISOString();
|
|
const path = await workspaceMemoryPath(root);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
const store: WorkspaceMemoryStore = {
|
|
version: 1,
|
|
workspace: { root, key: "test" },
|
|
limits: { maxRenderedChars: 3600, maxEntries: 28 },
|
|
entries: [memory("mem-secret", "Remember password: sushi for the fake test fixture.", { createdAt: now, updatedAt: now })],
|
|
migrations: [],
|
|
updatedAt: now,
|
|
};
|
|
const before = JSON.stringify(store, null, 2);
|
|
await writeFile(path, before, "utf8");
|
|
|
|
const output = formatMemoryStatus(await getMemoryStatus(root, "ses_readonly"));
|
|
const after = await readFile(path, "utf8");
|
|
|
|
assert.match(output, /- Active memories: 1/);
|
|
assert.equal(output.includes("Recent active memory previews"), false);
|
|
assert.equal(output.includes("sushi"), false, "status output should not include memory previews");
|
|
assert.equal(after, before, "status display must not persist normalization, migration, or redaction changes");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("formats current workspace memories grouped by type with display-local refs", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const now = new Date().toISOString();
|
|
await saveWorkspaceMemory(root, {
|
|
version: 1,
|
|
workspace: { root, key: "test" },
|
|
limits: { maxRenderedChars: 3600, maxEntries: 28 },
|
|
entries: [
|
|
memory("mem-feedback", "Remember password: sushi for the fake test.", { type: "feedback" }),
|
|
memory("mem-project", "Project memory should render in its own group.", { type: "project" }),
|
|
memory("mem-decision", "Decision memory should render in its own group.", { type: "decision" }),
|
|
memory("mem-reference", "Reference memory should render in its own group.", { type: "reference" }),
|
|
memory("mem-superseded", "Superseded memory should not be active", { type: "reference", status: "superseded" }),
|
|
],
|
|
migrations: [],
|
|
updatedAt: now,
|
|
});
|
|
|
|
const output = formatMemoryList(await getMemoryList(root));
|
|
|
|
assert.match(output, /^## Current workspace memories/);
|
|
assert.match(output, /Display refs are local to this output/);
|
|
assert.match(output, /feedback:\n- \[M\d+\]/);
|
|
assert.match(output, /project:\n- \[M\d+\]/);
|
|
assert.match(output, /decision:\n- \[M\d+\]/);
|
|
assert.match(output, /reference:\n- \[M\d+\]/);
|
|
const groupIndexes = MEMORY_TYPE_ORDER.map(type => output.indexOf(`${type}:`));
|
|
assert.equal(groupIndexes.every(index => index >= 0), true, "all memory type groups should render");
|
|
assert.deepEqual(groupIndexes, [...groupIndexes].sort((a, b) => a - b), "memory list groups should follow shared memory type order");
|
|
assert.match(output, /Shown: \d+ of \d+ active memories\./);
|
|
assert.match(output, /Shown: 4 of 4 active memories\./);
|
|
assert.match(output, /Omitted active memories: 0\./);
|
|
assert.equal(output.includes("[M1]"), true, "at least one display-local ref should render");
|
|
assert.equal(output.includes("sushi"), false, "list previews should redact credential-like text");
|
|
assert.equal(output.includes("Superseded memory should not be active"), false);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("formats empty memory list state", () => {
|
|
const output = formatMemoryList({
|
|
activeMemories: 0,
|
|
renderedMemories: 0,
|
|
omittedActiveMemories: 0,
|
|
groups: { feedback: [], project: [], decision: [], reference: [] },
|
|
});
|
|
assert.match(output, /^## Current workspace memories/);
|
|
assert.match(output, /No active workspace memories are stored yet\./);
|
|
assert.match(output, /Local only: no LLM request was made\./);
|
|
assert.equal(output.includes("feedback:"), false);
|
|
});
|
|
|
|
test("formats help text for available display commands", () => {
|
|
const output = formatMemoryHelp();
|
|
assert.match(output, /^## Memory help/);
|
|
assert.match(output, /\/memory — open the local memory menu\./);
|
|
assert.match(output, /Status — show local memory statistics\./);
|
|
assert.match(output, /Current memories — browse active workspace memories as display-local \[M1\]-\[M28\] refs\./);
|
|
assert.match(output, /Help — show this help\./);
|
|
for (const removedCommand of ["/memory-" + "status", "/memory-" + "list", "/memory-" + "help"]) {
|
|
assert.equal(output.includes(removedCommand), false);
|
|
}
|
|
assert.equal(output.includes("/memory activity"), false);
|
|
assert.equal(output.includes("/memory last"), false);
|
|
assert.equal(output.includes("/memory status"), false);
|
|
assert.equal(output.includes("/memory help"), false);
|
|
assert.match(output, /do not call the LLM/);
|
|
});
|
|
|
|
test("renderMemoryCommand routes list output", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const output = await renderMemoryCommand(root, "ses_list", "list");
|
|
assert.match(output, /^## Current workspace memories/);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("renderMemoryCommand falls back to help for unknown command values", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const output = await renderMemoryCommand(root, "ses_unknown", "unknown" as never);
|
|
assert.match(output, /^## Memory help/);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|