mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
f9acfd6136
P0a: Parser now accepts both - [type] text and - type text formats P0b: Prompt adds durable-content guidance to avoid session-specific snapshots P0c: Parser quality gate rejects exact test counts, file counts, phase progress - Only rejects phase progress when it appears early in the string (snapshot) - Stable config values with numbers (Admin PIN, Scrypt) still pass - Adds 7 new tests covering bracketless parsing and snapshot rejection
331 lines
11 KiB
TypeScript
331 lines
11 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 = `
|
|
## Memory Candidates
|
|
- [decision] short text
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects git commit hash", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [project] abc123def456 is the commit hash
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects raw error", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [feedback] TypeError: Cannot read property 'x' of undefined
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects stack trace", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [reference] at foo (bar.ts:10:5)
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects commit prefix", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [project] fix: add new feature
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects path-heavy facts", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [project] files at /src/a.ts /src/b.ts /src/c.ts are important
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts valid decision", () => {
|
|
const summary = `
|
|
## Memory Candidates
|
|
- [decision] Use pnpm instead of npm for package management
|
|
`;
|
|
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 = `
|
|
## Memory Candidates
|
|
- [project] This project uses TypeScript for all source files
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0].type, "project");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts plain text label format (no Markdown)", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- [decision] Use plain text labels to avoid purple Markdown headers
|
|
- [project] This repo uses pnpm for package management
|
|
`;
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 2);
|
|
assert.equal(items[0].type, "decision");
|
|
assert.equal(items[1].type, "project");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts bracketless candidate format", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project pathology-playground 後端健康改進計劃已完成 Phase 1-4
|
|
- reference Scrypt 參數必須是 N=16384, r=8, p=1
|
|
- feedback 端口 9473 可能被舊進程佔用,需殺掉後重啟
|
|
- decision Use output.prompt to replace the default compaction template
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
|
|
assert.equal(items.length, 4, "Should parse all 4 bracketless candidates");
|
|
assert.deepEqual(items.map(i => i.type), [
|
|
"project",
|
|
"reference",
|
|
"feedback",
|
|
"decision",
|
|
]);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects unknown bracketless candidate type", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- note this should not be parsed as memory
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects bracketless very short body", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project short
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates does not match bracketless type as substring", () => {
|
|
// "projectile" should NOT match "project"
|
|
const summary = `
|
|
Memory candidates:
|
|
- projectile launcher should not be parsed as a project memory
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0);
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects exact test count snapshots", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project 1237 tests pass, 226 suites
|
|
- project 500 tests pass today
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0, "Exact test counts are session snapshots, not durable memory");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects exact file count snapshots", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project USB 同步 37 個文件
|
|
- project 42 files synced
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0, "Exact file counts are session snapshots");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates rejects phase progress snapshots", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project Phase 1-4 已完成
|
|
- project Phase 3 completed
|
|
- project Completed phase 1
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 0, "Phase progress is session snapshot, not durable milestone");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts durable project facts", () => {
|
|
const summary = `
|
|
Memory candidates:
|
|
- project Backend health improvements organized into phased milestones
|
|
- project USB sync covers bundles, server, frontend, tests, and docs
|
|
- project Test suite expected to pass before handoff
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 3, "Durable project facts should pass");
|
|
});
|
|
|
|
test("parseWorkspaceMemoryCandidates accepts durable reference values with numbers", () => {
|
|
// Scrypt has sufficient length (>20 chars) and no paths - should pass quality gate
|
|
// Admin PIN too short (<20 chars) - intentionally omitted to isolate the test
|
|
const summary = `
|
|
Memory candidates:
|
|
- reference Scrypt 參數必須是 N=16384, r=8, p=1,必須嚴格遵守
|
|
- reference Admin PIN 456123 是系統管理員的預設登入密碼
|
|
`;
|
|
|
|
const items = parseWorkspaceMemoryCandidates(summary);
|
|
assert.equal(items.length, 2, "Both durable reference values with numbers should pass quality gate");
|
|
assert.deepEqual(items.map(i => i.type), ["reference", "reference"]);
|
|
}); |