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 Backend health improvements organized into phased milestones - 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 rejects wave/sprint/milestone/task progress snapshots", () => { const summary = ` Memory candidates: - project Waves 1-5 已完成,Wave 6 deferred - project Sprint 3 completed - project Milestone 2 done - project Task 8 finished `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 0, "Wave/Sprint/Milestone/Task progress should be rejected as snapshots"); }); test("parseWorkspaceMemoryCandidates keeps file limits but rejects file sync snapshots", () => { const summary = ` Memory candidates: - project Upload limit is 10 files per request - project USB uploaded 37 files for sync verification `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 1, "Should keep static file-limit facts and reject processed file-count snapshots"); assert.match(items[0].text, /Upload limit is 10 files/); }); 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 short Admin PIN reference entry", () => { // Real Admin PIN is <20 chars — should pass via config value allowlist const summary = ` Memory candidates: - reference Admin PIN 是 456123 `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 1, "Short config reference should pass via allowlist"); assert.equal(items[0].type, "reference"); }); test("parseWorkspaceMemoryCandidates accepts Scrypt config reference", () => { // Scrypt parameters with numbers should pass const summary = ` Memory candidates: - reference Scrypt 參數必須是 N=16384, r=8, p=1 `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 1, "Scrypt config values should pass"); assert.equal(items[0].type, "reference"); }); test("parseWorkspaceMemoryCandidates rejects Chinese file count snapshot", () => { // Real Chinese file count with counter word 個 const summary = ` Memory candidates: - project USB 同步:37 個文件(bundles, server, frontend, tests, docs) `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 0, "Chinese file count with 個 should be rejected"); }); test("parseWorkspaceMemoryCandidates rejects real phase snapshot mid-description", () => { // Real phase snapshot where Phase appears deep in the string const summary = ` Memory candidates: - project pathology-playground 後端健康改進計劃已完成 Phase 1-4 `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 0, "Phase snapshot mid-description should still be rejected"); }); test("parseWorkspaceMemoryCandidates extracts Japanese triggers", () => { const summary = ` Memory candidates: - project 覚えて: このプロジェクトは pnpm を使う - project 覚えておいて: 日本語でメモ `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 2); assert.match(items[0].text, /pnpm/); }); test("parseWorkspaceMemoryCandidates extracts Korean triggers", () => { const summary = ` Memory candidates: - project 기억해: 이 프로젝트는 pnpm을 사용한다 - project 메모해줘: 한국어 메모 `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 2); }); test("parseWorkspaceMemoryCandidates rejects negated Japanese triggers", () => { const summary = ` Memory candidates: - project 覚えないで 覚えて: 一時的なメモ `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 0, "Negated Japanese trigger should be rejected"); }); test("parseWorkspaceMemoryCandidates rejects negated Korean triggers", () => { const summary = ` Memory candidates: - project 기억하지 마 기억해: 일시적인 메모 `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items.length, 0, "Negated Korean trigger should be rejected"); }); test("parseWorkspaceMemoryCandidates body extraction excludes trigger suffix", () => { const summary = ` Memory candidates: - project 覚えておいて: このプロジェクトは pnpm を使う `; const items = parseWorkspaceMemoryCandidates(summary); assert.equal(items[0].text, "このプロジェクトは pnpm を使う"); assert.equal(items[0].text.includes("おいて"), false); });