Files
learnharness/tests/tool-toggles-bridge.test.ts
T
amosblomqvist d85d8a58a0 fix(tools): wire the searchAvailable gate + research/pdf in the probe phase
Fix wave (2026-09-20), two owner-approved fixes:

Fix 1 — the searchAvailable gate was dead code on the live wiring path:
createTeacherTools RECEIVED ctx.searchAvailable but dropped it when
building the inner JobToolContext, so jobTools' research gate always took
its ?? true default (jobTools-direct tests passed; the production path
never saw the signal). Threaded through — the gate now reads the live
server signal on the teacher registry too.

Fix 2 — the prober registry gains the research + pdf dispatch tools
(owner ruling, 2026-09-20): these two are phase-omnipresent — the prober
can verify facts or read learner PDFs while assessing. demo/visual stay
teach-phase-only (they need the lesson's plan context). Same gating as
the teacher side: toggle-filtered, research subject to the search gate
(honestly absent when no engine is live — the prober's dispatch closes
over the SAME jobTools builder, one gate, one semantics). probe_done
stays always-on-by-construction; the learner-fs set untouched.

- ProberToolContext (extends ProbeDoneContext with the job wiring);
  createProberAgent takes it; chat-handler injects jobStore + spawn +
  searchAvailable into the prober ctx.
- TeacherToolContext.spawn's stale input union fixed (PdfInput was
  missing from the TYPE — runtime already passed it).
- Tests: the threaded-gate describe in web-search.test.ts pins the LIVE
  factory path both ways (teacher + prober); question-tools' registry
  key-sets gain research/pdf; the prober toggle test pins the
  research:false path.

Gates: typecheck, 807/807 tests, lint, build — all green.
2026-09-22 16:22:57 +03:00

80 lines
3.5 KiB
TypeScript

/**
* The P2 tool-toggle bridge (2026-09-19): AgentBuildOptions.toolToggles is
* first-class — the agent constructors thread it to the factories' enabled?
* param (an explicit opts.tools registry is never filtered), and the
* settings write side rejects unknown tool keys against TOGGLEABLE_TOOLS
* (the definitive adjudicated list — a persisted `quiz: false` would
* genuinely drop the quiz once the bridge honors the delta).
*
* Parser tests are PURE (no fs — parallel-test discipline); the agent
* construction tests use an in-memory chat fixture like
* question-tools.test.ts.
*/
import assert from "node:assert/strict";
import { z } from "zod";
import { describe, it } from "node:test";
import { createProberAgent, createTeacherAgent } from "../src/agent/teacher-agent.ts";
import type { ProberToolContext } from "../src/agent/tools.ts";
import { jobStore } from "../src/store/jobs.ts";
import { parseAgentSettingsBody } from "../src/server/settings-body.ts";
import { chatStore } from "../src/store/chats.ts";
import type { AgentSettings } from "../src/core/settings.ts";
describe("AgentBuildOptions.toolToggles (the P2 bridge)", () => {
const toolNames = (agent: { state: { tools: Array<{ name: string }> } }) =>
agent.state.tools.map((t) => t.name).sort();
it("flows into the teacher's registry: a job-tool delta is a no-op on the bare set", () => {
// No ctx → bare agent: quiz/ask only. A job-tool delta names tools
// that don't exist there — the bridge must not crash on it.
const bare = createTeacherAgent(undefined, { toolToggles: { research: false } });
assert.deepEqual(toolNames(bare), ["ask_user_question", "quiz"]);
});
it("flows into the prober's registry; probe_done survives everything", () => {
const chatId = chatStore.createChat("Toggle bridge test").id;
const ctx: ProberToolContext = { chatId, chatStore, deliver: () => {}, jobStore, spawn: () => {} };
const prober = createProberAgent(ctx, { toolToggles: { ask_user_question: false } });
const names = toolNames(prober);
assert.ok(!names.includes("ask_user_question"), "the delta dropped the tool");
assert.ok(names.includes("probe_done"), "probe_done always-on");
});
it("an explicitly injected opts.tools registry is NEVER toggle-filtered", () => {
const teacher = createTeacherAgent(undefined, {
tools: {
quiz: { name: "quiz", description: "test", clientQuestion: true, schema: z.object({}) },
},
toolToggles: { quiz: false },
});
assert.deepEqual(toolNames(teacher), ["quiz"]);
});
});
describe("parseAgentSettingsBody: tool keys validate at write time", () => {
const parse = (tools: Record<string, boolean>) =>
parseAgentSettingsBody({ tools }) as
| { ok: true; settings: AgentSettings }
| { ok: false; error: string };
it("a valid job-tool delta parses", () => {
const r = parse({ research: false, pdf: false });
assert.equal(r.ok, true);
assert.deepEqual(r.ok && r.settings.tools, { research: false, pdf: false });
});
it("unknown keys are rejected with the supported list (the quiz wedge never persists)", () => {
const r = parse({ quiz: false });
assert.equal(r.ok, false);
assert.match(!r.ok && r.error, /unknown tool "quiz" \(supported: research, demo, visual, pdf\)/);
const typo = parse({ reasearch: false });
assert.equal(typo.ok, false);
assert.match(!typo.ok && typo.error, /unknown tool "reasearch"/);
});
it("absent tools field is fine (absent = enabled everywhere)", () => {
const r = parseAgentSettingsBody({}) as { ok: boolean };
assert.equal(r.ok, true);
});
});