Files
Emil 18b8a3247d fix: allow research runs to restart after stop; improve FTS search, agent limits, and timeouts
- AgentTree.restart() revives a stopped run in the same session: resets the
  root, archives old branches, and restores spawnability (stop no longer
  cancels the root).
- ResearchLoop.start() restarts from stopped/completed and resets iteration
  counters; setGoal is allowed in terminal states; supervisor 'start' revives
  the tree when the root is not spawnable.
- Cancelled/failed/archived children no longer count toward the per-agent
  child limit and archived agents are ignored by the duplicate-task guard.
- cancel() no longer overwrites an already recorded agent result.
- Add agent_timeout_seconds (default 1800) so hung agent sessions fail
  instead of blocking wait() forever.
- FTS search now prefix-matches tokens (tolerates inflections) and safely
  handles punctuation/FTS5 metacharacters instead of returning false
  negatives or throwing.
- Add tests for restart semantics, child limits, result preservation,
  agent timeouts, and FTS morphology/special characters (38/38 passing).
2026-07-31 22:57:54 +03:00

14 lines
2.7 KiB
TypeScript

import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { ResearchMemory } from "../src/research-memory.js";
describe("ResearchMemory", () => {
it("persists findings and performs rebuildable full-text search", () => { const memory = new ResearchMemory(mkdtempSync(resolve(tmpdir(), "hm-memory-"))); const id = memory.save({ type: "fact", status: "corroborated", createdBy: "verifier", runId: "run-1", title: "Catalyst result", statement: "Catalyst alpha improves the measured yield.", evidence: "Independent measurements agree.", sources: ["source-a", "source-b"], limitations: "Small sample" }); expect(memory.search("catalyst")[0]?.id).toBe(id); memory.close(); expect(memory.rebuildIndex()).toBe(1); expect(memory.search("yield")[0]?.id).toBe(id); memory.close(); });
it("does not allow unsourced claims to become corroborated facts", () => { const memory = new ResearchMemory(mkdtempSync(resolve(tmpdir(), "hm-memory-"))); expect(() => memory.save({ type: "fact", status: "corroborated", createdBy: "agent", runId: "run", title: "Claim", statement: "Unsupported" })).toThrow(/requires sources/); memory.close(); });
it("keeps negative results searchable", () => { const memory = new ResearchMemory(mkdtempSync(resolve(tmpdir(), "hm-memory-"))); memory.save({ type: "experiment_result", status: "rejected", createdBy: "runner", runId: "run", title: "Null replication", statement: "No measurable effect", negativeResult: true }); expect(memory.search("replication")).toHaveLength(1); memory.close(); });
it("prefixes tokens so inflected forms of the same word match", () => { const memory = new ResearchMemory(mkdtempSync(resolve(tmpdir(), "hm-memory-"))); const id = memory.save({ type: "fact", status: "observed", createdBy: "agent", runId: "run", title: "Ribosome dynamics", statement: "Рибосомами управляют рибосомные белки в рибосоме." }); const hits = memory.search("рибосома"); expect(hits.some((row) => row.id === id)).toBe(true); expect(hits[0]?.title).toBe("Ribosome dynamics"); memory.close(); });
it("survives queries with punctuation and FTS metacharacters", () => { const memory = new ResearchMemory(mkdtempSync(resolve(tmpdir(), "hm-memory-"))); memory.save({ type: "fact", status: "observed", createdBy: "agent", runId: "run", title: "Code expansion", statement: "non-AUG starts and C++ style operators are searched." }); expect(memory.search("non-AUG").some((row) => row.title === "Code expansion")).toBe(true); expect(memory.search("C++").some((row) => row.title === "Code expansion")).toBe(true); expect(memory.search("NOT")).toHaveLength(0); memory.close(); });
});