Files
opencode-working-memory/tests/workspace-memory.test.ts
T
Ralph Chang 1bba0511bb fix: PR-1 memory plugin quality fixes
## Task 1: Fix exitCode undefined false positive
- Add `typeof exitCode !== "number"` check in plugin.ts
- Only extract errors when exitCode is explicitly non-zero
- Prevent git-log/cat with "errors" text from creating false positives

## Task 2: Fix workspace memory XML truncation
- Budget-aware line-by-line rendering
- Always include closing </workspace_memory> tag
- Return empty string when budget too small
- Bonus: canonical exact deduplication with source priority

## Task 3: Remove "always" as trigger
- Replace "always" with "going forward" in patterns
- Add word boundary via `g` flag and matchAll loop
- "from now on" still works as expected

## Task 4: Verification
- 22 tests passing
- typecheck passing

Tests cover:
- git log/cat with loose "errors" ignored
- TS2345/TypeError strong signals captured
- undefined exitCode: no create, no clear
- exitCode 0: clears errors
- exitCode non-zero: creates error
- XML never truncated mid-tag
- "always" not a trigger
2026-04-26 12:52:21 +08:00

210 lines
6.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "../src/types.ts";
import { renderWorkspaceMemory, enforceLongTermLimits } from "../src/workspace-memory.ts";
function entry(id: string, text: string, type: LongTermMemoryEntry["type"] = "decision"): LongTermMemoryEntry {
const now = new Date().toISOString();
return {
id,
type,
text,
source: "compaction",
confidence: 0.75,
status: "active",
createdAt: now,
updatedAt: now,
};
}
// ============================================
// Task 2: renderWorkspaceMemory tests
// ============================================
test("renderWorkspaceMemory never truncates closing XML tag", () => {
const entries = Array.from({ length: 28 }, (_, i) =>
entry(`mem_${i}`, `Long durable memory entry ${i} `.repeat(20))
);
const store: WorkspaceMemoryStore = {
version: 1,
workspace: { root: "/repo", key: "abc" },
limits: { maxRenderedChars: 700, maxEntries: 28 },
entries,
updatedAt: new Date().toISOString(),
};
const rendered = renderWorkspaceMemory(store);
assert.ok(rendered.endsWith("</workspace_memory>"),
`Rendered memory must end with closing tag. Got: ...${rendered.slice(-50)}`);
assert.ok(rendered.length <= 700,
`Rendered memory must not exceed maxChars. Got: ${rendered.length}`);
});
test("renderWorkspaceMemory returns empty string when maxChars too small", () => {
const store: WorkspaceMemoryStore = {
version: 1,
workspace: { root: "/repo", key: "abc" },
limits: { maxRenderedChars: 50, maxEntries: 28 },
entries: [entry("test", "test memory")],
updatedAt: new Date().toISOString(),
};
const rendered = renderWorkspaceMemory(store);
assert.equal(rendered, "",
"When maxChars too small for even minimal envelope, return empty string");
});
test("renderWorkspaceMemory respects budget and fits entries", () => {
// Create entries that would overflow a small budget
const entries = [
entry("a", "First memory entry that is reasonably long"),
entry("b", "Second memory entry that is also reasonably long"),
entry("c", "Third memory entry that is also reasonably long"),
];
const store: WorkspaceMemoryStore = {
version: 1,
workspace: { root: "/repo", key: "abc" },
limits: { maxRenderedChars: 200, maxEntries: 28 },
entries,
updatedAt: new Date().toISOString(),
};
const rendered = renderWorkspaceMemory(store);
assert.ok(rendered.endsWith("</workspace_memory>"),
"Must end with closing tag even when truncating entries");
assert.ok(rendered.length <= 200,
`Must respect maxChars limit. Got: ${rendered.length}`);
});
test("renderWorkspaceMemory returns empty for no entries", () => {
const store: WorkspaceMemoryStore = {
version: 1,
workspace: { root: "/repo", key: "abc" },
limits: { maxRenderedChars: 5200, maxEntries: 28 },
entries: [],
updatedAt: new Date().toISOString(),
};
const rendered = renderWorkspaceMemory(store);
assert.equal(rendered, "");
});
// ============================================
// PR-2 Task 5 tests (for enforceLongTermLimits)
// ============================================
test("enforceLongTermLimits dedupes with canonical text", () => {
const now = new Date().toISOString();
const a: LongTermMemoryEntry = {
id: "a",
type: "decision",
text: "OpenCode uses NPM CACHE for plugin loading",
source: "compaction",
confidence: 0.75,
status: "active",
createdAt: now,
updatedAt: now,
};
const b: LongTermMemoryEntry = {
id: "b",
type: "decision",
text: "opencode uses npm cache for plugin loading!!!",
source: "compaction",
confidence: 0.8,
status: "active",
createdAt: now,
updatedAt: now,
};
const kept = enforceLongTermLimits([a, b]);
assert.equal(kept.length, 1, "Should dedupe similar texts");
assert.equal(kept[0].confidence, 0.8, "Higher confidence should win for same source");
});
test("enforceLongTermLimits preserves explicit over compaction", () => {
const now = new Date().toISOString();
const explicit: LongTermMemoryEntry = {
id: "explicit",
type: "decision",
text: "Use pnpm for this project",
source: "explicit",
confidence: 0.5,
status: "active",
createdAt: now,
updatedAt: now,
};
const compaction: LongTermMemoryEntry = {
id: "compaction",
type: "decision",
text: "Use pnpm for this project",
source: "compaction",
confidence: 0.9,
status: "active",
createdAt: now,
updatedAt: now,
};
const kept = enforceLongTermLimits([explicit, compaction]);
assert.equal(kept.length, 1);
assert.equal(kept[0].source, "explicit",
"Explicit source should win over compaction even with lower confidence");
assert.equal(kept[0].confidence, 0.5, "Original explicit confidence preserved");
});
test("enforceLongTermLimits same source higher confidence wins", () => {
const now = new Date().toISOString();
const a: LongTermMemoryEntry = {
id: "a",
type: "decision",
text: "Project uses TypeScript",
source: "compaction",
confidence: 0.7,
status: "active",
createdAt: now,
updatedAt: now,
};
const b: LongTermMemoryEntry = {
id: "b",
type: "decision",
text: "Project uses TypeScript",
source: "compaction",
confidence: 0.9,
status: "active",
createdAt: now,
updatedAt: now,
};
const kept = enforceLongTermLimits([a, b]);
assert.equal(kept.length, 1);
assert.equal(kept[0].confidence, 0.9, "Higher confidence wins for same source");
});
test("enforceLongTermLimits respects maxEntries limit", () => {
const now = new Date().toISOString();
const entries = Array.from({ length: 50 }, (_, i) => ({
id: `mem_${i}`,
type: "decision" as const,
text: `Unique memory entry number ${i}`,
source: "compaction" as const,
confidence: 0.75,
status: "active" as const,
createdAt: now,
updatedAt: now,
}));
const kept = enforceLongTermLimits(entries);
assert.ok(kept.length <= 28, `Should respect maxEntries. Got: ${kept.length}`);
});