mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
1bba0511bb
## 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
210 lines
6.1 KiB
TypeScript
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}`);
|
|
}); |