869 lines
29 KiB
TypeScript
869 lines
29 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
vi.mock("../src/logger.js", () => ({
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
}));
|
|
|
|
import { registerDiagnosticsFunction } from "../src/functions/diagnostics.js";
|
|
import type {
|
|
Action,
|
|
ActionEdge,
|
|
DiagnosticCheck,
|
|
Lease,
|
|
Sentinel,
|
|
Sketch,
|
|
Signal,
|
|
Session,
|
|
Memory,
|
|
MeshPeer,
|
|
} from "../src/types.js";
|
|
import { KV } from "../src/state/schema.js";
|
|
|
|
function mockKV() {
|
|
const store = new Map<string, Map<string, unknown>>();
|
|
return {
|
|
get: async <T>(scope: string, key: string): Promise<T | null> => {
|
|
return (store.get(scope)?.get(key) as T) ?? null;
|
|
},
|
|
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
|
|
if (!store.has(scope)) store.set(scope, new Map());
|
|
store.get(scope)!.set(key, data);
|
|
return data;
|
|
},
|
|
delete: async (scope: string, key: string): Promise<void> => {
|
|
store.get(scope)?.delete(key);
|
|
},
|
|
list: async <T>(scope: string): Promise<T[]> => {
|
|
const entries = store.get(scope);
|
|
return entries ? (Array.from(entries.values()) as T[]) : [];
|
|
},
|
|
};
|
|
}
|
|
|
|
function mockSdk() {
|
|
const functions = new Map<string, Function>();
|
|
return {
|
|
registerFunction: (idOrOpts: string | { id: string }, handler: Function) => {
|
|
const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id;
|
|
functions.set(id, handler);
|
|
},
|
|
registerTrigger: () => {},
|
|
trigger: async (idOrInput: string | { function_id: string; payload: unknown }, data?: unknown) => {
|
|
const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id;
|
|
const payload = typeof idOrInput === "string" ? data : idOrInput.payload;
|
|
const fn = functions.get(id);
|
|
if (!fn) throw new Error(`No function: ${id}`);
|
|
return fn(payload);
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeAction(overrides: Partial<Action> = {}): Action {
|
|
return {
|
|
id: `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
title: "Test action",
|
|
description: "",
|
|
status: "pending",
|
|
priority: 5,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
createdBy: "agent-1",
|
|
tags: [],
|
|
sourceObservationIds: [],
|
|
sourceMemoryIds: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeLease(overrides: Partial<Lease> = {}): Lease {
|
|
return {
|
|
id: `lease_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
actionId: "act_missing",
|
|
agentId: "agent-1",
|
|
acquiredAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
status: "active",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeEdge(overrides: Partial<ActionEdge> = {}): ActionEdge {
|
|
return {
|
|
id: `ae_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
type: "requires",
|
|
sourceActionId: "src",
|
|
targetActionId: "tgt",
|
|
createdAt: new Date().toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeSentinel(overrides: Partial<Sentinel> = {}): Sentinel {
|
|
return {
|
|
id: `sen_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
name: "Test sentinel",
|
|
type: "timer",
|
|
status: "watching",
|
|
config: {},
|
|
createdAt: new Date().toISOString(),
|
|
linkedActionIds: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeSketch(overrides: Partial<Sketch> = {}): Sketch {
|
|
return {
|
|
id: `sk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
title: "Test sketch",
|
|
description: "",
|
|
status: "active",
|
|
actionIds: [],
|
|
createdAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeSignal(overrides: Partial<Signal> = {}): Signal {
|
|
return {
|
|
id: `sig_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
from: "agent-1",
|
|
type: "info",
|
|
content: "test",
|
|
createdAt: new Date().toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeSession(overrides: Partial<Session> = {}): Session {
|
|
return {
|
|
id: `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
project: "test",
|
|
cwd: "/tmp",
|
|
startedAt: new Date().toISOString(),
|
|
status: "active",
|
|
observationCount: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeMemory(overrides: Partial<Memory> = {}): Memory {
|
|
return {
|
|
id: `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
type: "fact",
|
|
title: "Test memory",
|
|
content: "content",
|
|
concepts: [],
|
|
files: [],
|
|
sessionIds: [],
|
|
strength: 1,
|
|
version: 1,
|
|
isLatest: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makePeer(overrides: Partial<MeshPeer> = {}): MeshPeer {
|
|
return {
|
|
id: `peer_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
url: "http://localhost:3111",
|
|
name: "Test peer",
|
|
status: "connected",
|
|
sharedScopes: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("Diagnostics Functions", () => {
|
|
let sdk: ReturnType<typeof mockSdk>;
|
|
let kv: ReturnType<typeof mockKV>;
|
|
|
|
beforeEach(() => {
|
|
sdk = mockSdk();
|
|
kv = mockKV();
|
|
registerDiagnosticsFunction(sdk as never, kv as never);
|
|
});
|
|
|
|
describe("mem::diagnose", () => {
|
|
it("empty system passes all checks", async () => {
|
|
const result = (await sdk.trigger("mem::diagnose", {})) as {
|
|
success: boolean;
|
|
checks: DiagnosticCheck[];
|
|
summary: { pass: number; warn: number; fail: number; fixable: number };
|
|
};
|
|
|
|
expect(result.success).toBe(true);
|
|
// 15 = 8 original (actions, leases, sentinels, sketches, signals,
|
|
// sessions, memories, mesh) + 6 added in #lesson-visibility
|
|
// (lessons, summaries, semantic, procedural, crystals, insights) +
|
|
// 1 added in #memory-project-scope (memory-project-coverage).
|
|
expect(result.summary.pass).toBe(15);
|
|
expect(result.summary.warn).toBe(0);
|
|
expect(result.summary.fail).toBe(0);
|
|
expect(result.summary.fixable).toBe(0);
|
|
expect(result.checks.every((c) => c.status === "pass")).toBe(true);
|
|
});
|
|
|
|
it("active action with no lease produces warn", async () => {
|
|
const action = makeAction({ status: "active" });
|
|
await kv.set(KV.actions, action.id, action);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["actions"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("active-no-lease:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("blocked action with all deps done produces fail (fixable)", async () => {
|
|
const dep = makeAction({ status: "done" });
|
|
const blocked = makeAction({ status: "blocked" });
|
|
const edge = makeEdge({
|
|
sourceActionId: blocked.id,
|
|
targetActionId: dep.id,
|
|
type: "requires",
|
|
});
|
|
await kv.set(KV.actions, dep.id, dep);
|
|
await kv.set(KV.actions, blocked.id, blocked);
|
|
await kv.set(KV.actionEdges, edge.id, edge);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["actions"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("blocked-deps-done:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("pending action with unsatisfied deps produces fail (fixable)", async () => {
|
|
const dep = makeAction({ status: "active" });
|
|
const pending = makeAction({ status: "pending" });
|
|
const edge = makeEdge({
|
|
sourceActionId: pending.id,
|
|
targetActionId: dep.id,
|
|
type: "requires",
|
|
});
|
|
await kv.set(KV.actions, dep.id, dep);
|
|
await kv.set(KV.actions, pending.id, pending);
|
|
await kv.set(KV.actionEdges, edge.id, edge);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["actions"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("pending-unsatisfied-deps:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("expired active lease produces fail (fixable)", async () => {
|
|
const action = makeAction({ status: "active" });
|
|
const lease = makeLease({
|
|
actionId: action.id,
|
|
status: "active",
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.actions, action.id, action);
|
|
await kv.set(KV.leases, lease.id, lease);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["leases"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("expired-lease:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("orphaned lease (action gone) produces fail (fixable)", async () => {
|
|
const lease = makeLease({
|
|
actionId: "act_gone",
|
|
status: "active",
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.leases, lease.id, lease);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["leases"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("orphaned-lease:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("expired watching sentinel produces fail (fixable)", async () => {
|
|
const sentinel = makeSentinel({
|
|
status: "watching",
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.sentinels, sentinel.id, sentinel);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["sentinels"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("expired-sentinel:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("sentinel referencing missing action produces warn", async () => {
|
|
const sentinel = makeSentinel({
|
|
linkedActionIds: ["act_nonexistent"],
|
|
});
|
|
await kv.set(KV.sentinels, sentinel.id, sentinel);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["sentinels"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("sentinel-missing-action:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("expired active sketch produces fail (fixable)", async () => {
|
|
const sketch = makeSketch({
|
|
status: "active",
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.sketches, sketch.id, sketch);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["sketches"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("expired-sketch:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("expired signal produces fail (fixable)", async () => {
|
|
const signal = makeSignal({
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.signals, signal.id, signal);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["signals"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("expired-signal:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("active session older than 24h produces warn", async () => {
|
|
const session = makeSession({
|
|
status: "active",
|
|
startedAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(),
|
|
});
|
|
await kv.set(KV.sessions, session.id, session);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["sessions"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("abandoned-session:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("memory with stale isLatest produces fail (fixable)", async () => {
|
|
const oldMemory = makeMemory({ isLatest: true });
|
|
const newMemory = makeMemory({ supersedes: [oldMemory.id] });
|
|
await kv.set(KV.memories, oldMemory.id, oldMemory);
|
|
await kv.set(KV.memories, newMemory.id, newMemory);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["memories"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("memory-stale-latest:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("fail");
|
|
expect(check!.fixable).toBe(true);
|
|
});
|
|
|
|
it("memory superseding non-existent produces warn", async () => {
|
|
const memory = makeMemory({ supersedes: ["mem_gone"] });
|
|
await kv.set(KV.memories, memory.id, memory);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["memories"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("memory-missing-supersedes:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("stale mesh peer produces warn", async () => {
|
|
const peer = makePeer({
|
|
lastSyncAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
|
});
|
|
await kv.set(KV.mesh, peer.id, peer);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["mesh"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("stale-peer:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("error mesh peer produces warn", async () => {
|
|
const peer = makePeer({ status: "error" });
|
|
await kv.set(KV.mesh, peer.id, peer);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["mesh"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const check = result.checks.find((c) =>
|
|
c.name.startsWith("error-peer:"),
|
|
);
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe("warn");
|
|
expect(check!.fixable).toBe(false);
|
|
});
|
|
|
|
it("filters by categories", async () => {
|
|
const action = makeAction({ status: "active" });
|
|
await kv.set(KV.actions, action.id, action);
|
|
|
|
const signal = makeSignal({
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.signals, signal.id, signal);
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["signals"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
expect(result.checks.every((c) => c.category === "signals")).toBe(true);
|
|
expect(
|
|
result.checks.some((c) => c.category === "actions"),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("mem::heal", () => {
|
|
it("unblocks stuck blocked action", async () => {
|
|
const dep = makeAction({ status: "done" });
|
|
const blocked = makeAction({ status: "blocked", title: "Stuck task" });
|
|
const edge = makeEdge({
|
|
sourceActionId: blocked.id,
|
|
targetActionId: dep.id,
|
|
type: "requires",
|
|
});
|
|
await kv.set(KV.actions, dep.id, dep);
|
|
await kv.set(KV.actions, blocked.id, blocked);
|
|
await kv.set(KV.actionEdges, edge.id, edge);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["actions"],
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("Unblocked");
|
|
|
|
const updated = await kv.get<Action>(KV.actions, blocked.id);
|
|
expect(updated!.status).toBe("pending");
|
|
});
|
|
|
|
it("blocks pending action with unsatisfied deps", async () => {
|
|
const dep = makeAction({ status: "active" });
|
|
const pending = makeAction({
|
|
status: "pending",
|
|
title: "Should be blocked",
|
|
});
|
|
const edge = makeEdge({
|
|
sourceActionId: pending.id,
|
|
targetActionId: dep.id,
|
|
type: "requires",
|
|
});
|
|
await kv.set(KV.actions, dep.id, dep);
|
|
await kv.set(KV.actions, pending.id, pending);
|
|
await kv.set(KV.actionEdges, edge.id, edge);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["actions"],
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("Blocked");
|
|
|
|
const updated = await kv.get<Action>(KV.actions, pending.id);
|
|
expect(updated!.status).toBe("blocked");
|
|
});
|
|
|
|
it("expires stale lease and resets action", async () => {
|
|
const action = makeAction({
|
|
status: "active",
|
|
assignedTo: "agent-1",
|
|
});
|
|
const lease = makeLease({
|
|
actionId: action.id,
|
|
agentId: "agent-1",
|
|
status: "active",
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.actions, action.id, action);
|
|
await kv.set(KV.leases, lease.id, lease);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["leases"],
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("Expired lease");
|
|
|
|
const updatedLease = await kv.get<Lease>(KV.leases, lease.id);
|
|
expect(updatedLease!.status).toBe("expired");
|
|
|
|
const updatedAction = await kv.get<Action>(KV.actions, action.id);
|
|
expect(updatedAction!.status).toBe("pending");
|
|
expect(updatedAction!.assignedTo).toBeUndefined();
|
|
});
|
|
|
|
it("deletes orphaned lease", async () => {
|
|
const lease = makeLease({
|
|
actionId: "act_gone",
|
|
status: "released",
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.leases, lease.id, lease);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["leases"],
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("Deleted orphaned lease");
|
|
|
|
const deleted = await kv.get<Lease>(KV.leases, lease.id);
|
|
expect(deleted).toBeNull();
|
|
});
|
|
|
|
it("expires stale sentinel", async () => {
|
|
const sentinel = makeSentinel({
|
|
status: "watching",
|
|
name: "Stale watcher",
|
|
expiresAt: new Date(Date.now() - 60_000).toISOString(),
|
|
});
|
|
await kv.set(KV.sentinels, sentinel.id, sentinel);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["sentinels"],
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("Expired sentinel");
|
|
|
|
const updated = await kv.get<Sentinel>(KV.sentinels, sentinel.id);
|
|
expect(updated!.status).toBe("expired");
|
|
});
|
|
|
|
it("dry run reports but does not fix", async () => {
|
|
const dep = makeAction({ status: "done" });
|
|
const blocked = makeAction({ status: "blocked", title: "Stuck task" });
|
|
const edge = makeEdge({
|
|
sourceActionId: blocked.id,
|
|
targetActionId: dep.id,
|
|
type: "requires",
|
|
});
|
|
await kv.set(KV.actions, dep.id, dep);
|
|
await kv.set(KV.actions, blocked.id, blocked);
|
|
await kv.set(KV.actionEdges, edge.id, edge);
|
|
|
|
const result = (await sdk.trigger("mem::heal", {
|
|
categories: ["actions"],
|
|
dryRun: true,
|
|
})) as { success: boolean; fixed: number; details: string[] };
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.fixed).toBe(1);
|
|
expect(result.details[0]).toContain("[dry-run]");
|
|
|
|
const unchanged = await kv.get<Action>(KV.actions, blocked.id);
|
|
expect(unchanged!.status).toBe("blocked");
|
|
});
|
|
});
|
|
|
|
describe("per-store tally categories (#lesson-visibility)", () => {
|
|
it("lessons category: passes with valid live lessons + ignores tombstoned", async () => {
|
|
await kv.set(KV.lessons, "lsn_live", {
|
|
id: "lsn_live", content: "x", context: "", confidence: 0.8,
|
|
reinforcements: 0, source: "manual", sourceIds: [], tags: [],
|
|
createdAt: "", updatedAt: "", decayRate: 0.05,
|
|
});
|
|
await kv.set(KV.lessons, "lsn_tomb", {
|
|
id: "lsn_tomb", content: "x", context: "", confidence: 0.5,
|
|
reinforcements: 0, source: "manual", sourceIds: [], tags: [],
|
|
createdAt: "", updatedAt: "", decayRate: 0.05, deleted: true,
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["lessons"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const ok = result.checks.find((c) => c.name === "lessons-ok");
|
|
expect(ok?.status).toBe("pass");
|
|
expect(ok?.message).toMatch(/All 1 lessons.*1 tombstoned/);
|
|
});
|
|
|
|
it("lessons category: warns on out-of-range confidence", async () => {
|
|
await kv.set(KV.lessons, "lsn_bad", {
|
|
id: "lsn_bad", content: "x", context: "", confidence: 1.5,
|
|
reinforcements: 0, source: "manual", sourceIds: [], tags: [],
|
|
createdAt: "", updatedAt: "", decayRate: 0.05,
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["lessons"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("lesson-bad-confidence:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("summaries category: warns on missing title", async () => {
|
|
await kv.set(KV.summaries, "ses_1", {
|
|
sessionId: "ses_1", project: "p", createdAt: "", title: "",
|
|
narrative: "n", keyDecisions: [], filesModified: [], concepts: [],
|
|
observationCount: 1,
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["summaries"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("summary-missing-title:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("procedural category: warns on empty steps", async () => {
|
|
await kv.set(KV.procedural, "proc_1", {
|
|
id: "proc_1", name: "noop", steps: [], triggerCondition: "x",
|
|
frequency: 1, sourceSessionIds: [], strength: 0.5,
|
|
createdAt: "", updatedAt: "",
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["procedural"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("procedural-empty-steps:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("crystals category: warns on empty narrative", async () => {
|
|
await kv.set(KV.crystals, "cry_1", {
|
|
id: "cry_1", narrative: "", keyOutcomes: [], filesAffected: [],
|
|
lessons: [], sourceActionIds: [], createdAt: "",
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["crystals"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("crystal-empty-narrative:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("insights category: warns on out-of-range confidence", async () => {
|
|
await kv.set(KV.insights, "ins_bad", {
|
|
id: "ins_bad", title: "t", content: "c", confidence: -0.1,
|
|
reinforcements: 0, sourceConceptCluster: [], sourceMemoryIds: [],
|
|
sourceLessonIds: [], sourceCrystalIds: [], tags: [],
|
|
createdAt: "", updatedAt: "", decayRate: 0.05,
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["insights"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("insight-bad-confidence:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("semantic category: warns on out-of-range confidence", async () => {
|
|
await kv.set(KV.semantic, "sem_bad", {
|
|
id: "sem_bad", fact: "f", confidence: 2.0, sourceSessionIds: [],
|
|
sourceMemoryIds: [], accessCount: 0, lastAccessedAt: "",
|
|
strength: 0, createdAt: "", updatedAt: "",
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["semantic"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("semantic-bad-confidence:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("categories filter accepts new categories and skips others", async () => {
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["lessons", "summaries"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
expect(result.checks.every((c) => c.category === "lessons" || c.category === "summaries")).toBe(true);
|
|
expect(result.checks.some((c) => c.category === "lessons")).toBe(true);
|
|
expect(result.checks.some((c) => c.category === "summaries")).toBe(true);
|
|
});
|
|
|
|
describe("defensive row-shape handling (CodeRabbit #473 review)", () => {
|
|
it("NaN/Infinity confidence on a lesson is flagged as warn, not silently passed", async () => {
|
|
await kv.set(KV.lessons, "lsn_nan", {
|
|
id: "lsn_nan", content: "x", context: "", confidence: NaN,
|
|
reinforcements: 0, source: "manual", sourceIds: [], tags: [],
|
|
createdAt: "", updatedAt: "", decayRate: 0.05,
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["lessons"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
const warn = result.checks.find((c) => c.name.startsWith("lesson-bad-confidence:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("non-string summary title doesn't throw — surfaces as warn", async () => {
|
|
await kv.set(KV.summaries, "ses_bad_title", {
|
|
sessionId: "ses_bad_title",
|
|
project: "p",
|
|
createdAt: "",
|
|
title: null as unknown as string, // simulate corrupted row
|
|
narrative: "n",
|
|
keyDecisions: [],
|
|
filesModified: [],
|
|
concepts: [],
|
|
observationCount: 1,
|
|
});
|
|
|
|
// The bug to guard against: the old code called .trim() unconditionally,
|
|
// which throws on null/number, which aborts the whole diagnose run and
|
|
// any later category check never executes. Verify diagnose completes
|
|
// AND surfaces the bad row.
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["summaries", "lessons"],
|
|
})) as { checks: DiagnosticCheck[]; success?: boolean };
|
|
|
|
expect(result.success).toBe(true);
|
|
const warn = result.checks.find((c) => c.name.startsWith("summary-missing-title:"));
|
|
expect(warn?.status).toBe("warn");
|
|
// Later category still ran:
|
|
expect(result.checks.some((c) => c.category === "lessons")).toBe(true);
|
|
});
|
|
|
|
it("non-string crystal narrative doesn't throw — surfaces as warn", async () => {
|
|
await kv.set(KV.crystals, "cry_bad", {
|
|
id: "cry_bad",
|
|
narrative: undefined as unknown as string,
|
|
keyOutcomes: [],
|
|
filesAffected: [],
|
|
lessons: [],
|
|
sourceActionIds: [],
|
|
createdAt: "",
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["crystals"],
|
|
})) as { checks: DiagnosticCheck[]; success?: boolean };
|
|
|
|
expect(result.success).toBe(true);
|
|
const warn = result.checks.find((c) => c.name.startsWith("crystal-empty-narrative:"));
|
|
expect(warn?.status).toBe("warn");
|
|
});
|
|
|
|
it("Infinity confidence on insight + semantic both flagged", async () => {
|
|
await kv.set(KV.insights, "ins_inf", {
|
|
id: "ins_inf",
|
|
title: "t",
|
|
content: "c",
|
|
confidence: Infinity,
|
|
reinforcements: 0,
|
|
sourceConceptCluster: [],
|
|
sourceMemoryIds: [],
|
|
sourceLessonIds: [],
|
|
sourceCrystalIds: [],
|
|
tags: [],
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
decayRate: 0.05,
|
|
});
|
|
await kv.set(KV.semantic, "sem_nan", {
|
|
id: "sem_nan",
|
|
fact: "f",
|
|
confidence: NaN,
|
|
sourceSessionIds: [],
|
|
sourceMemoryIds: [],
|
|
accessCount: 0,
|
|
lastAccessedAt: "",
|
|
strength: 0,
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
});
|
|
|
|
const result = (await sdk.trigger("mem::diagnose", {
|
|
categories: ["insights", "semantic"],
|
|
})) as { checks: DiagnosticCheck[] };
|
|
|
|
expect(result.checks.find((c) => c.name === "insight-bad-confidence:ins_inf")?.status).toBe("warn");
|
|
expect(result.checks.find((c) => c.name === "semantic-bad-confidence:sem_nan")?.status).toBe("warn");
|
|
});
|
|
});
|
|
});
|
|
});
|