Files
opencode-working-memory/tests/extractors.test.ts
T
Ralph Chang f9acfd6136 fix: parser accepts bracketless format, rejects project snapshots, adds durable-content prompt
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
2026-04-26 16:28:55 +08:00

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"]);
});