mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
ff4639d153
## Task 5: Canonical exact dedupe - Already implemented in PR-1 with enforceLongTermLimits() - Source priority: explicit > manual > compaction - Same source: higher confidence wins ## Task 6: Structured negative guard - Add isNegatedMemoryRequest() for adjacency detection - "不要記住" / "don't remember" are now properly ignored - "not forget to remember" no longer false positive (not a directive) - Restrict patterns to line-start (^|\n) to avoid mid-sentence matches ## Task 7: Compaction quality gate - Add shouldAcceptWorkspaceMemoryCandidate() predicate - Reject low-quality candidates: git hashes, errors, stack traces - Reject temporary progress, code signatures, path-heavy facts - Only accept entries with >= 20 chars ## Pattern improvements - All patterns use matchAll() with proper g flag - Dedupe by canonical text in extractExplicitMemories() - Line-start anchor prevents "to remember" mid-sentence matches - Add more trigger patterns: save/add to memory, commit to memory Tests: 36 passing
215 lines
7.1 KiB
TypeScript
215 lines
7.1 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { extractErrorsFromBash, extractExplicitMemories } from "../src/extractors.ts";
|
|
|
|
// ============================================
|
|
// Task 1: extractErrorsFromBash tests
|
|
// ============================================
|
|
|
|
test("git log output mentioning errors is ignored", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"cd /repo && rtk git log --oneline -5",
|
|
"4832b38 fix: silence memory load errors in working-memory"
|
|
);
|
|
assert.equal(errors.length, 0);
|
|
});
|
|
|
|
test("cat session json with openErrors is ignored", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"rtk cat ~/.local/share/opencode-working-memory/session.json",
|
|
'"openErrors": []'
|
|
);
|
|
assert.equal(errors.length, 0);
|
|
});
|
|
|
|
test("typecheck failure is captured", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"npm run typecheck",
|
|
"src/index.ts(10,3): error TS2345: bad type"
|
|
);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].category, "typecheck");
|
|
});
|
|
|
|
test("runtime Error prefix is captured for failed unknown command", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"node script.js",
|
|
"Error: Cannot find module './missing'"
|
|
);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].category, "runtime");
|
|
});
|
|
|
|
test("unknown command with loose error words is ignored", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"some-unknown-command",
|
|
"this output has errors in it but no clear signal"
|
|
);
|
|
assert.equal(errors.length, 0);
|
|
});
|
|
|
|
test("TypeError prefix is captured", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"node script.js",
|
|
"TypeError: Cannot read property 'x' of undefined"
|
|
);
|
|
assert.equal(errors.length, 1);
|
|
});
|
|
|
|
test("TS error pattern is always captured", () => {
|
|
const errors = extractErrorsFromBash(
|
|
"cat some-file.txt", // unknown command, but TS error is strong signal
|
|
"src/index.ts(10,3): error TS2345: Argument of type 'string' is not assignable"
|
|
);
|
|
assert.equal(errors.length, 1);
|
|
assert.equal(errors[0].category, "runtime");
|
|
});
|
|
|
|
// ============================================
|
|
// Task 3: extractExplicitMemories tests
|
|
// ============================================
|
|
|
|
test("extractExplicitMemories does not treat always as memory trigger", () => {
|
|
const items = extractExplicitMemories("tests always fail on CI when cache is stale");
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("extractExplicitMemories still captures going forward", () => {
|
|
const items = extractExplicitMemories("going forward: use pnpm instead of npm");
|
|
assert.equal(items.length, 1);
|
|
assert.match(items[0].text, /pnpm/);
|
|
});
|
|
|
|
test("extractExplicitMemories captures from now on", () => {
|
|
const items = extractExplicitMemories("from now on: reply in Traditional Chinese");
|
|
assert.equal(items.length, 1);
|
|
assert.match(items[0].text, /Traditional Chinese/);
|
|
});
|
|
|
|
// ============================================
|
|
// Task 6: Negative memory request tests
|
|
// ============================================
|
|
|
|
test("extractExplicitMemories ignores Chinese negative request", () => {
|
|
const items = extractExplicitMemories("不要記住:這個 repo 使用 npm cache");
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("extractExplicitMemories ignores English negative request", () => {
|
|
const items = extractExplicitMemories("please don't remember this: use npm cache");
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("extractExplicitMemories does not false positive on 'not forget'", () => {
|
|
// "remember this" in middle of sentence should NOT match (not a directive)
|
|
const items = extractExplicitMemories("I will not forget to remember this: use pnpm");
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("extractExplicitMemories captures remember at line start", () => {
|
|
// "remember this" at line start IS a directive
|
|
const items = extractExplicitMemories("remember this: use pnpm for all packages");
|
|
assert.equal(items.length, 1);
|
|
assert.match(items[0].text, /pnpm/);
|
|
});
|
|
|
|
test("extractExplicitMemories still captures positive request after negative", () => {
|
|
// Ensure negative guard doesn't block positive requests
|
|
const items = extractExplicitMemories("記住:使用 pnpm 來管理套件");
|
|
assert.equal(items.length, 1);
|
|
assert.match(items[0].text, /pnpm/);
|
|
});
|
|
|
|
test("extractExplicitMemories captures multiple memories in same message", () => {
|
|
const items = extractExplicitMemories("請記住:使用 pnpm 管理套件\n記住這點:用 TypeScript 撰寫程式碼");
|
|
assert.equal(items.length, 2);
|
|
});
|
|
|
|
// ============================================
|
|
// Task 7: Compaction quality gate tests
|
|
// ============================================
|
|
|
|
import { parseWorkspaceMemoryCandidates } from "../src/extractors.ts";
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects short text", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [decision] short text
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects git commit hash", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [project] abc123def456 is the commit hash
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects raw error", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [feedback] TypeError: Cannot read property 'x' of undefined
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects stack trace", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [reference] at foo (bar.ts:10:5)
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects commit prefix", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [project] fix: add new feature
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects path-heavy facts", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [project] files at /src/a.ts /src/b.ts /src/c.ts are important
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts valid decision", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [decision] Use pnpm instead of npm for package management
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0].type, "decision");
|
|
assert.match(items[0].text, /pnpm/);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts valid project info", () => {
|
|
const summary = `
|
|
<workspace_memory_candidates>
|
|
- [project] This project uses TypeScript for all source files
|
|
</workspace_memory_candidates>
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0].type, "project");
|
|
}); |