2026-09-19 18:16:17 +03:00
|
|
|
/**
|
|
|
|
|
* 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";
|
2026-09-25 23:32:13 +03:00
|
|
|
import { Type } from "typebox";
|
|
|
|
|
import { questionResult } from "../src/agent/tool-execute.ts";
|
2026-09-19 18:16:17 +03:00
|
|
|
import { createProberAgent, createTeacherAgent } from "../src/agent/teacher-agent.ts";
|
2026-09-22 16:22:57 +03:00
|
|
|
import type { ProberToolContext } from "../src/agent/tools.ts";
|
|
|
|
|
import { jobStore } from "../src/store/jobs.ts";
|
2026-09-19 18:16:17 +03:00
|
|
|
import { parseAgentSettingsBody } from "../src/server/settings-body.ts";
|
|
|
|
|
import { chatStore } from "../src/store/chats.ts";
|
2026-09-22 16:58:25 +03:00
|
|
|
import { createFauxCore } from "@earendil-works/pi-ai";
|
|
|
|
|
import type { PiEngine } from "../src/agent/models.ts";
|
|
|
|
|
|
|
|
|
|
/** A scripted pi engine — the agent builds need an injected engine since
|
|
|
|
|
* BYOK removed the factory pins (owner, 2026-09-22). */
|
|
|
|
|
function fauxEngine(): PiEngine {
|
|
|
|
|
const faux = createFauxCore({});
|
|
|
|
|
return { model: faux.getModel(), streamFn: faux.streamSimple };
|
|
|
|
|
}
|
2026-09-19 18:16:17 +03:00
|
|
|
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.
|
2026-09-22 16:58:25 +03:00
|
|
|
const bare = createTeacherAgent(undefined, { engine: fauxEngine(), toolToggles: { research: false } });
|
2026-09-19 18:16:17 +03:00
|
|
|
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;
|
2026-09-22 16:22:57 +03:00
|
|
|
const ctx: ProberToolContext = { chatId, chatStore, deliver: () => {}, jobStore, spawn: () => {} };
|
2026-09-22 16:58:25 +03:00
|
|
|
const prober = createProberAgent(ctx, { engine: fauxEngine(), toolToggles: { ask_user_question: false } });
|
2026-09-19 18:16:17 +03:00
|
|
|
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, {
|
2026-09-22 16:58:25 +03:00
|
|
|
engine: fauxEngine(),
|
2026-09-19 18:16:17 +03:00
|
|
|
tools: {
|
2026-09-25 23:32:13 +03:00
|
|
|
quiz: {
|
|
|
|
|
name: "quiz",
|
|
|
|
|
label: "quiz",
|
|
|
|
|
description: "test",
|
|
|
|
|
parameters: Type.Object({}),
|
|
|
|
|
execute: async () => questionResult(),
|
|
|
|
|
},
|
2026-09-19 18:16:17 +03:00
|
|
|
},
|
|
|
|
|
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 });
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-27 08:55:13 +03:00
|
|
|
it("the quizzing-mechanics key (explain) is writable (owner 2026-09-27)", () => {
|
|
|
|
|
const r = parse({ explain: true });
|
|
|
|
|
assert.equal(r.ok, true);
|
|
|
|
|
assert.deepEqual(r.ok && r.settings.tools, { explain: true });
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-19 18:16:17 +03:00
|
|
|
it("unknown keys are rejected with the supported list (the quiz wedge never persists)", () => {
|
|
|
|
|
const r = parse({ quiz: false });
|
|
|
|
|
assert.equal(r.ok, false);
|
2026-09-27 08:55:13 +03:00
|
|
|
assert.match(!r.ok && r.error, /unknown tool "quiz" \(supported: research, demo, visual, pdf, explain\)/);
|
2026-09-19 18:16:17 +03:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|