Skip to content

Commit 56d7ef9

Browse files
committed
test(memory): add real workspace quality cleanup regression fixture
1 parent 7427221 commit 56d7ef9

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { LongTermMemoryEntry } from "../../src/types.ts";
2+
3+
export type RealWorkspaceFixtureEntry = LongTermMemoryEntry & {
4+
expectedAfterMigration: "active" | "superseded";
5+
expectation: string;
6+
};
7+
8+
const baseTimestamp = "2026-04-28T00:00:00.000Z";
9+
10+
function mem(
11+
id: string,
12+
type: LongTermMemoryEntry["type"],
13+
text: string,
14+
expectedAfterMigration: "active" | "superseded",
15+
expectation: string,
16+
): RealWorkspaceFixtureEntry {
17+
return {
18+
id,
19+
type,
20+
text,
21+
source: "compaction",
22+
confidence: 0.75,
23+
status: "active",
24+
createdAt: baseTimestamp,
25+
updatedAt: baseTimestamp,
26+
staleAfterDays: type === "feedback" ? undefined : 45,
27+
expectedAfterMigration,
28+
expectation,
29+
};
30+
}
31+
32+
export const REAL_WORKSPACE_FIXTURES: Record<string, RealWorkspaceFixtureEntry[]> = {
33+
"medical-atlas": [
34+
mem("ma_ui_rule", "feedback", "UI 要統一風格:兩個表格都要 scrollable,約 20 rows", "active", "durable UI rule without user preference keyword"),
35+
mem("ma_csp_rule", "feedback", "架構師建議中期將 CSP 改為 nonce/hash,而非 'unsafe-inline'", "active", "durable architecture recommendation"),
36+
mem("ma_form_rule", "decision", "Form 添加防御性 action/method 屬性,避免 JS 失效時 GET 首頁", "active", "declarative design rule"),
37+
mem("ma_logging_rule", "decision", "Cloud Logging filter 需支援多種 log 格式(jsonPayload.event_type, jsonPayload.message, textPayload)", "active", "durable spec using 需支援"),
38+
],
39+
"opencode-record": [
40+
mem("or_phase_snapshot", "project", "後端健康改進計劃已完成 Phase 1-4", "superseded", "progress snapshot"),
41+
mem("or_test_snapshot", "project", "測試套件:1237 tests pass, 226 suites", "superseded", "test count snapshot"),
42+
mem("or_sync_snapshot", "project", "USB 同步:37 個文件(bundles, server, frontend, tests, docs)", "superseded", "file sync snapshot"),
43+
],
44+
"agent-reports": [
45+
mem("ar_plan_decision", "feedback", "架構師建議執行 P3 前先確認有實際需求", "active", "durable plan decision"),
46+
mem("ar_reviewer_fallback", "feedback", "`comprehensive-code-reviewer` subagent unreliable; use `phase-verifier` as fallback", "active", "durable workaround rule"),
47+
mem("ar_wave_rule", "feedback", "每個 Wave 結束要找 verifier 確認,全部結束找 code review", "active", "durable workflow rule"),
48+
mem("ar_remote_headers", "decision", "Remote headers 透過 `requestInit: { headers }` 傳入 `StreamableHTTPClientTransport`", "active", "declarative API rule"),
49+
mem("ar_signal_order", "decision", "Graceful process cleanup signal order: SIGINT (300ms) → SIGTERM (700ms) → SIGKILL", "active", "durable process cleanup spec"),
50+
mem("ar_ownership", "decision", "`McpRuntimeState` ownership model: CLI owns both runtime and mcpRuntime, dispose order is runtime first", "active", "durable ownership model"),
51+
mem("ar_retry_policy", "decision", "Recovery retry policy: only once per tool call, only for transport/session failures", "active", "durable retry policy"),
52+
],
53+
"pdf-extraction": [
54+
mem("pe_user_cycle", "feedback", "User 要求完整的 plan-review-feedback-modify-verify 循環,不是直接執行", "active", "mixed-language user workflow preference"),
55+
mem("pe_ollama_batch", "feedback", "Ollama 大批量嵌入需要控制批次大小(20-50)和請求間隔", "active", "durable operational knowledge"),
56+
mem("pe_option_b", "decision", "Phase 2 Fix 採用 Option B:multi-profile search grouping", "active", "design decision using 採用"),
57+
mem("pe_single_source", "decision", "MCP source 維持單一 `book`,書籍身份在 source ID", "active", "design constraint using 維持"),
58+
mem("pe_endpoint", "decision", "Ollama endpoint is `/api/embed` (not `/api/embeddings`) with `\"input\"` field", "active", "declarative API fact"),
59+
mem("pe_filter_pipeline", "decision", "Filter pipeline: pre-chunk filtering (not post-chunk) to prevent embedding contamination", "active", "durable architecture rule"),
60+
mem("pe_do_not_delete", "decision", "不刪除孤立的 reference-like 行(正文中的 \"et al.\" 等是合法引用)", "active", "do-not rule not matching current 不要 pattern"),
61+
],
62+
"self-repo": [
63+
mem("sr_author_credit", "feedback", "User insists on preserving external contributor author credit and uses merge workflow", "active", "durable preference using insists"),
64+
mem("sr_branding", "decision", "Product branding is \"OpenCode Working Memory\" without \"Plugin\" in the name", "active", "durable branding rule"),
65+
mem("sr_changelog", "decision", "CHANGELOG version scope follows git tags: changes from v1.2.3 tag through HEAD belong to next version", "active", "durable release rule"),
66+
],
67+
};

tests/workspace-memory.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import {
1616
workspaceMemoryIdentityKey,
1717
redactCredentials,
1818
runMigrationP0Cleanup,
19+
runMigrationQualityCleanup,
1920
loadWorkspaceMemory,
2021
saveWorkspaceMemory,
2122
updateWorkspaceMemoryWithAccounting,
2223
} from "../src/workspace-memory.ts";
2324
import { assessMemoryQuality, isHardQualityReason, isProgressSnapshotViolation } from "../src/memory-quality.ts";
2425
import { reviewerCurrent28Fixture } from "./fixtures/memory-quality-current-28.ts";
26+
import { REAL_WORKSPACE_FIXTURES } from "./fixtures/real-workspaces-snapshot.ts";
2527

2628
function entry(id: string, text: string, type: LongTermMemoryEntry["type"] = "decision"): LongTermMemoryEntry {
2729
const now = new Date().toISOString();
@@ -1078,6 +1080,43 @@ test("quality cleanup migration writes audit log for hard supersedes", async ()
10781080
}
10791081
});
10801082

1083+
test("quality cleanup migration regression against real workspace samples", async () => {
1084+
const failures: string[] = [];
1085+
const now = "2026-04-28T00:00:00.000Z";
1086+
1087+
for (const [workspaceName, fixtureEntries] of Object.entries(REAL_WORKSPACE_FIXTURES)) {
1088+
const root = `/fixture/${workspaceName}`;
1089+
const store = {
1090+
version: 1,
1091+
workspace: { root, key: workspaceName.padEnd(16, "0").slice(0, 16) },
1092+
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
1093+
entries: fixtureEntries.map(({ expectedAfterMigration, expectation, ...entry }) => entry),
1094+
migrations: [],
1095+
updatedAt: now,
1096+
};
1097+
1098+
const result = runMigrationQualityCleanup(store, now).store;
1099+
const byId = new Map(result.entries.map(entry => [entry.id, entry]));
1100+
1101+
for (const original of fixtureEntries) {
1102+
const after = byId.get(original.id);
1103+
if (!after) {
1104+
failures.push(`${workspaceName}/${original.id}: missing after migration`);
1105+
continue;
1106+
}
1107+
if (after.status !== original.expectedAfterMigration) {
1108+
failures.push(
1109+
`${workspaceName}/${original.id}: expected ${original.expectedAfterMigration}, got ${after.status}\n` +
1110+
` text: ${original.text.slice(0, 120)}\n` +
1111+
` why: ${original.expectation}`,
1112+
);
1113+
}
1114+
}
1115+
}
1116+
1117+
assert.equal(failures.length, 0, `Regression failures:\n${failures.join("\n")}`);
1118+
});
1119+
10811120
test("quality cleanup migration supersedes only hard violations from current fixture", async () => {
10821121
const root = await mkdtemp(join(tmpdir(), "wm-quality-cleanup-"));
10831122
try {

0 commit comments

Comments
 (0)