357 lines
20 KiB
TypeScript
357 lines
20 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
import { loadEnv } from "../src/config/env.js";
|
|
import { compareAutoModeProjectRows, createProjectBrainMcpServer } from "../src/mcp/autonomyServer.js";
|
|
import { autonomyRetryDelayMinutes, canResumeAutonomyLease, classifyAutonomyBlocker, enqueueAutonomyTaskInState, evaluateAutonomyRunBudget, isAutonomyLeaseOwner, normalizeAutonomyState, parseRoadmapMarkdown, releaseActiveAutonomyLease, reorderAutonomyQueueInState, resetAutonomyQueueOrderInState, retryAutonomyBlockerIfDue } from "../src/repositories/autonomyRepository.js";
|
|
import { autonomyHeartbeatSlotId, compactAutonomyState, resolveAutonomyHeartbeat, summarizeAutonomyProject } from "../src/services/autonomyRuntime.js";
|
|
|
|
const env = loadEnv({
|
|
NODE_ENV: "test",
|
|
DATABASE_URL: "postgresql://example.invalid/projectbrain",
|
|
PROJECTBRAIN_DATA_SOURCE: "database",
|
|
PROJECTBRAIN_OWNER_USERNAME: "owner",
|
|
PROJECTBRAIN_OWNER_PASSWORD: "correct-horse-battery-staple",
|
|
PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters",
|
|
PROJECTBRAIN_MCP_ENABLED: "true",
|
|
PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com",
|
|
PROJECTBRAIN_GITEA_URL: "https://gitea.example.com",
|
|
PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters",
|
|
});
|
|
|
|
describe("Auto Mode roadmap parser", () => {
|
|
it("turns markdown phases and list items into ordered durable work", () => {
|
|
const roadmap = parseRoadmapMarkdown(`# Product roadmap
|
|
|
|
## Fundament
|
|
Maak de deliverylaag hervatbaar.
|
|
- [x] Bestaande status importeren
|
|
- [ ] Leasecontract toevoegen
|
|
|
|
## Product
|
|
Voltooi de gebruikersflow.
|
|
1. Auto toggle bouwen
|
|
2. Voortgang tonen`);
|
|
|
|
expect(roadmap.title).toBe("Product roadmap");
|
|
expect(roadmap.phases).toHaveLength(2);
|
|
expect(roadmap.phases[0]).toMatchObject({ title: "Fundament", objective: "Maak de deliverylaag hervatbaar.", status: "active" });
|
|
expect(roadmap.phases[0].items.map((item) => [item.title, item.status])).toEqual([
|
|
["Bestaande status importeren", "completed"],
|
|
["Leasecontract toevoegen", "pending"],
|
|
]);
|
|
expect(roadmap.phases[1].status).toBe("pending");
|
|
expect(roadmap.phases[1].items.map((item) => item.title)).toEqual(["Auto toggle bouwen", "Voortgang tonen"]);
|
|
});
|
|
|
|
it("accepts a flat task list by creating one execution phase", () => {
|
|
const roadmap = parseRoadmapMarkdown("- Analyseer repo\n- Bouw oplossing\n- Valideer resultaat", "MCP automation");
|
|
|
|
expect(roadmap.title).toBe("MCP automation");
|
|
expect(roadmap.phases).toHaveLength(1);
|
|
expect(roadmap.phases[0].title).toBe("Execution");
|
|
expect(roadmap.phases[0].items).toHaveLength(3);
|
|
});
|
|
|
|
it("allows an empty phase so the brainstorm loop can discover its work", () => {
|
|
const roadmap = parseRoadmapMarkdown("# Roadmap\n\n## Quality convergence\nZoek autonoom naar resterende gaten.");
|
|
|
|
expect(roadmap.phases[0].items).toEqual([]);
|
|
expect(roadmap.phases[0].brainstormConverged).toBe(false);
|
|
expect(roadmap.phases[0].objective).toBe("Zoek autonoom naar resterende gaten.");
|
|
});
|
|
});
|
|
|
|
describe("Auto Mode state safety", () => {
|
|
it("keeps an hourly ChatGPT Chat executor fresh between scheduled runs", () => {
|
|
expect(env.PROJECTBRAIN_AUTONOMY_EXECUTOR_STALE_SECONDS).toBe(5_400);
|
|
});
|
|
|
|
it("stores a user assignment durably even while Auto Mode is off", () => {
|
|
const current = normalizeAutonomyState({}, "owner");
|
|
const queued = enqueueAutonomyTaskInState(current, { instruction: "Verbeter de mobiele navigatie\nmet regressietests." }, "owner");
|
|
|
|
expect(queued.state.status).toBe("off");
|
|
expect(queued.task).toMatchObject({
|
|
title: "Verbeter de mobiele navigatie",
|
|
instruction: "Verbeter de mobiele navigatie\nmet regressietests.",
|
|
source: "user_assignment",
|
|
status: "pending",
|
|
});
|
|
expect(queued.state.roadmap?.phases[0].items).toEqual([queued.task]);
|
|
expect(queued.state.events.at(-1)?.type).toBe("autonomy.task.enqueued");
|
|
});
|
|
|
|
it("prioritizes a new user assignment in the current claimable phase instead of a phase already passed", () => {
|
|
const roadmap = parseRoadmapMarkdown("## Eerder\n- [ ] Eerste opdracht\n\n## Huidig\n- [ ] Lopend werk");
|
|
roadmap.phases[0].id = "projectbrain-opdrachten-old";
|
|
roadmap.phases[0].status = "completed";
|
|
roadmap.phases[1].status = "active";
|
|
roadmap.currentPhaseIndex = 1;
|
|
const current = normalizeAutonomyState({ enabled: true, status: "running", roadmap }, "owner");
|
|
|
|
const queued = enqueueAutonomyTaskInState(current, { instruction: "Nieuwe opdracht" }, "owner");
|
|
|
|
expect(queued.state.status).toBe("running");
|
|
expect(queued.state.roadmap?.phases).toHaveLength(2);
|
|
expect(queued.state.roadmap?.phases[0].status).toBe("completed");
|
|
expect(queued.state.roadmap?.phases[1].items[0]).toEqual(queued.task);
|
|
expect(queued.state.roadmap?.phases[1].items[1].title).toBe("Lopend werk");
|
|
});
|
|
|
|
it("persists an exact owner-defined order for pending executable work only", () => {
|
|
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] A\n- [ ] B\n- [ ] C");
|
|
roadmap.phases[0].items[2].scope = "future";
|
|
const current = normalizeAutonomyState({ enabled: true, roadmap }, "owner");
|
|
const [a, b] = current.roadmap!.phases[0].items;
|
|
|
|
const reordered = reorderAutonomyQueueInState(current, [b.id, a.id], "owner");
|
|
|
|
expect(reordered.roadmap?.phases[0].items[0].manualOrder).toBe(1);
|
|
expect(reordered.roadmap?.phases[0].items[1].manualOrder).toBe(0);
|
|
expect(reordered.roadmap?.phases[0].items[2].manualOrder).toBeUndefined();
|
|
expect(reordered.events.at(-1)?.type).toBe("autonomy.queue.reordered");
|
|
expect(() => reorderAutonomyQueueInState(current, [a.id], "owner")).toThrow("AUTO_MODE_QUEUE_CHANGED");
|
|
|
|
const automatic = resetAutonomyQueueOrderInState(reordered, "owner");
|
|
expect(automatic.roadmap?.phases.flatMap((phase) => phase.items).every((item) => item.manualOrder === undefined)).toBe(true);
|
|
expect(automatic.events.at(-1)?.type).toBe("autonomy.queue.order_reset");
|
|
});
|
|
|
|
it("derives the public status from durable facts instead of stale stored copy", () => {
|
|
expect(normalizeAutonomyState({ enabled: true, status: "running" }).status).toBe("waiting_roadmap");
|
|
expect(normalizeAutonomyState({}).profile).toBe("normal");
|
|
expect(normalizeAutonomyState({ profile: "unsupported" }).profile).toBe("normal");
|
|
const disabled = normalizeAutonomyState({ enabled: false, status: "running", activeLease: { token: "old", kind: "implement", phaseId: "phase" } });
|
|
expect(disabled.status).toBe("off");
|
|
expect(disabled.activeLease).toBeUndefined();
|
|
});
|
|
|
|
it("releases in-progress work safely when Auto Mode is disabled", () => {
|
|
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
|
|
roadmap.phases[0].status = "executing";
|
|
roadmap.phases[0].items[0].status = "in_progress";
|
|
const released = releaseActiveAutonomyLease({
|
|
version: 1,
|
|
profile: "normal",
|
|
enabled: true,
|
|
status: "running",
|
|
autoBrainstorm: true,
|
|
maxBrainstormIterations: 5,
|
|
maxConsecutiveFailures: 3,
|
|
leaseMinutes: 30,
|
|
maxWorkItemsPerRun: 8,
|
|
maxRunMinutes: 45,
|
|
runs: [],
|
|
roadmap,
|
|
activeLease: { token: "lease", kind: "implement", actor: "codex", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString() },
|
|
completedLeases: [],
|
|
events: [],
|
|
updatedAt: new Date().toISOString(),
|
|
updatedBy: "owner",
|
|
}, "owner");
|
|
|
|
expect(released.activeLease).toBeUndefined();
|
|
expect(released.roadmap?.phases[0]).toMatchObject({ status: "active", items: [expect.objectContaining({ status: "pending" })] });
|
|
expect(released.events.at(-1)?.type).toBe("autonomy.lease.released");
|
|
});
|
|
|
|
it("separates claimable work from a fresh or expired executor lease", () => {
|
|
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
|
|
const base = normalizeAutonomyState({ enabled: true, roadmap });
|
|
const fresh = summarizeAutonomyProject({
|
|
projectId: "project-id", projectSlug: "project", projectName: "Project",
|
|
autonomy: { ...base, activeLease: { token: "lease", kind: "implement", actor: "executor", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: "2026-09-02T10:00:00.000Z", expiresAt: "2026-09-02T10:30:00.000Z" } },
|
|
}, Date.parse("2026-09-02T10:15:00.000Z"));
|
|
const expired = summarizeAutonomyProject({
|
|
projectId: "project-id", projectSlug: "project", projectName: "Project",
|
|
autonomy: { ...base, activeLease: { token: "lease", kind: "implement", actor: "executor", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: "2026-09-02T10:00:00.000Z", expiresAt: "2026-09-02T10:30:00.000Z" } },
|
|
}, Date.parse("2026-09-02T10:31:00.000Z"));
|
|
|
|
expect(fresh).toMatchObject({ activeWorkKind: "implement", pendingWorkCount: 1, lifecycle: "running" });
|
|
expect(expired.activeWorkKind).toBeUndefined();
|
|
expect(expired.lifecycle).toBe("ready");
|
|
});
|
|
|
|
it("defaults and bounds the continuous executor run budget", () => {
|
|
expect(normalizeAutonomyState({})).toMatchObject({ maxWorkItemsPerRun: 8, maxRunMinutes: 45 });
|
|
expect(normalizeAutonomyState({ maxWorkItemsPerRun: 100, maxRunMinutes: 2 })).toMatchObject({ maxWorkItemsPerRun: 20, maxRunMinutes: 5 });
|
|
});
|
|
|
|
it("only lets the owning scheduled run resume a live lease", () => {
|
|
const lease = {
|
|
token: "lease",
|
|
kind: "implement" as const,
|
|
actor: "chatgpt-web",
|
|
runId: "run-1",
|
|
roadmapId: "roadmap",
|
|
phaseId: "phase",
|
|
createdAt: "2026-09-02T10:00:00.000Z",
|
|
expiresAt: "2026-09-02T10:30:00.000Z",
|
|
};
|
|
|
|
expect(canResumeAutonomyLease(lease, "chatgpt-web", "run-1")).toBe(true);
|
|
expect(canResumeAutonomyLease(lease, "chatgpt-web", "run-2")).toBe(false);
|
|
expect(canResumeAutonomyLease(lease, "another-executor", "run-1")).toBe(false);
|
|
expect(canResumeAutonomyLease(lease, "chatgpt-web")).toBe(false);
|
|
expect(isAutonomyLeaseOwner(lease, "chatgpt-web")).toBe(true);
|
|
expect(isAutonomyLeaseOwner(lease, "another-executor")).toBe(false);
|
|
});
|
|
|
|
it("enforces item and elapsed-time budgets from durable run state", () => {
|
|
const base = normalizeAutonomyState({
|
|
maxWorkItemsPerRun: 2,
|
|
maxRunMinutes: 10,
|
|
runs: [{ id: "item-run", actor: "chatgpt-web", startedAt: "2026-09-03T10:00:00.000Z", reportedWorkItems: 2 }],
|
|
});
|
|
expect(evaluateAutonomyRunBudget(base, "chatgpt-web", "item-run", Date.parse("2026-09-03T10:01:00.000Z"))).toMatchObject({ allowed: false, reason: "item_limit" });
|
|
|
|
const timed = normalizeAutonomyState({
|
|
maxWorkItemsPerRun: 8,
|
|
maxRunMinutes: 10,
|
|
runs: [{ id: "time-run", actor: "chatgpt-web", startedAt: "2026-09-03T10:00:00.000Z", reportedWorkItems: 1 }],
|
|
});
|
|
expect(evaluateAutonomyRunBudget(timed, "chatgpt-web", "time-run", Date.parse("2026-09-03T10:10:00.000Z"))).toMatchObject({ allowed: false, reason: "time_limit" });
|
|
expect(evaluateAutonomyRunBudget(timed, "other-actor", "time-run", Date.parse("2026-09-03T10:01:00.000Z"))).toMatchObject({ allowed: false, reason: "owner_mismatch" });
|
|
});
|
|
|
|
it("classifies technical failures as retryable and reserves human blockers for real owner input", () => {
|
|
expect(classifyAutonomyBlocker("Untrusted non-HTTPS Gitea server URL")).toBe("retryable");
|
|
expect(classifyAutonomyBlocker("Runner unavailable during validation")).toBe("retryable");
|
|
expect(classifyAutonomyBlocker("Missing credential required for production deployment")).toBe("human");
|
|
expect(classifyAutonomyBlocker("Owner decision required between two product directions")).toBe("human");
|
|
expect(classifyAutonomyBlocker("Temporary provider failure", "human")).toBe("retryable");
|
|
});
|
|
|
|
it("backs off repeated technical failures without requiring a human decision", () => {
|
|
expect(autonomyRetryDelayMinutes(1)).toBe(15);
|
|
expect(autonomyRetryDelayMinutes(2)).toBe(30);
|
|
expect(autonomyRetryDelayMinutes(3)).toBe(60);
|
|
expect(autonomyRetryDelayMinutes(20)).toBeLessThanOrEqual(1_440);
|
|
});
|
|
|
|
it("makes a due retryable blocker claimable without clearing failure counters", () => {
|
|
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
|
|
roadmap.status = "blocked";
|
|
roadmap.phases[0].status = "blocked";
|
|
roadmap.phases[0].items[0].status = "blocked";
|
|
roadmap.phases[0].items[0].attempts = 2;
|
|
const blocked = normalizeAutonomyState({
|
|
enabled: true,
|
|
roadmap,
|
|
blocker: { at: "2026-09-03T10:00:00.000Z", retryAt: "2026-09-03T11:00:00.000Z", kind: "retryable", phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, reason: "Temporary provider failure" },
|
|
});
|
|
|
|
const retried = retryAutonomyBlockerIfDue(blocked, Date.parse("2026-09-03T11:00:00.000Z"));
|
|
|
|
expect(retried).toMatchObject({ status: "running", roadmap: { status: "active", phases: [{ status: "active", items: [{ status: "pending", attempts: 2 }] }] } });
|
|
expect(retried.blocker).toBeUndefined();
|
|
expect(retried.events.at(-1)?.type).toBe("autonomy.blocker.retry_ready");
|
|
});
|
|
|
|
it("fences heartbeat ownership between overlapping scheduled runs", () => {
|
|
const current = { runId: "new-run", status: "working", startedAt: new Date("2026-09-03T10:00:00.000Z") };
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "old-run", status: "idle" })).toMatchObject({ accepted: false });
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", status: "degraded" })).toMatchObject({ accepted: false, reason: "run_owner_mismatch" });
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "new-run", status: "idle" })).toMatchObject({ accepted: true, startedAt: current.startedAt });
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:10:00.000Z"))).toMatchObject({ accepted: false, reason: "active_run" });
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T11:00:00.000Z"))).toMatchObject({ accepted: true, startedAt: new Date("2026-09-03T11:00:00.000Z") });
|
|
expect(resolveAutonomyHeartbeat({ ...current, status: "idle" }, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:10:00.000Z"))).toMatchObject({ accepted: true, startedAt: new Date("2026-09-03T10:10:00.000Z") });
|
|
expect(resolveAutonomyHeartbeat({ ...current, activeProjectId: "project-a" }, { instanceId: "ordinary-chat", runId: "new-run", status: "working", activeProjectId: "project-b" })).toMatchObject({ accepted: false, reason: "active_project" });
|
|
});
|
|
|
|
it("allows takeover after a short project's configured run window plus grace", () => {
|
|
const current = { runId: "old-run", status: "working", startedAt: new Date("2026-09-03T10:00:00.000Z") };
|
|
const takeoverAfterMs = 25 * 60_000;
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:24:00.000Z"), takeoverAfterMs)).toMatchObject({ accepted: false, reason: "active_run" });
|
|
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:26:00.000Z"), takeoverAfterMs)).toMatchObject({ accepted: true });
|
|
});
|
|
|
|
it("isolates concurrent ordinary ChatGPT executors by project", () => {
|
|
expect(autonomyHeartbeatSlotId("mediarr")).toBe("project:mediarr");
|
|
expect(autonomyHeartbeatSlotId("rombuilder")).toBe("project:rombuilder");
|
|
expect(autonomyHeartbeatSlotId("mediarr")).not.toBe(autonomyHeartbeatSlotId("rombuilder"));
|
|
expect(autonomyHeartbeatSlotId()).toBe("primary");
|
|
});
|
|
|
|
it("projects large durable roadmaps into bounded MCP context", () => {
|
|
const roadmap = parseRoadmapMarkdown(`## Current\n${Array.from({ length: 80 }, (_, index) => `- [ ] Item ${index + 1} ${"detail ".repeat(30)}`).join("\n")}\n## Later\n${Array.from({ length: 80 }, (_, index) => `- [ ] Later ${index + 1}`).join("\n")}`);
|
|
const state = normalizeAutonomyState({
|
|
enabled: true,
|
|
roadmap,
|
|
events: Array.from({ length: 30 }, (_, index) => ({ id: `event-${index}`, at: "2026-09-04T10:00:00.000Z", type: "test", actor: "chatgpt", summary: "evidence ".repeat(200) })),
|
|
});
|
|
for (const item of state.roadmap!.phases[0].items.slice(0, 40)) item.status = "completed";
|
|
|
|
const compact = compactAutonomyState(state, { itemLimit: 10, eventLimit: 3 });
|
|
const serialized = JSON.stringify(compact);
|
|
|
|
expect(compact.roadmap?.currentPhase?.items).toHaveLength(10);
|
|
expect(compact.roadmap?.currentPhase).toMatchObject({ totalItems: 80, itemWindowStart: 37, itemsReturned: 10, hasMoreItems: true });
|
|
expect(compact.roadmap?.currentPhase?.items[3]).toMatchObject({ title: expect.stringContaining("Item 41"), status: "pending" });
|
|
expect(compact.events).toHaveLength(3);
|
|
expect(serialized).not.toContain("sourceMarkdown");
|
|
expect(serialized.length).toBeLessThan(12_000);
|
|
expect(serialized.length).toBeLessThan(JSON.stringify(state).length / 5);
|
|
});
|
|
});
|
|
|
|
describe("Auto Mode MCP surface", () => {
|
|
it("orders claimable projects fairly before considering backlog size", () => {
|
|
const projects = [
|
|
{ name: "huge-recent", status: "running", pending_work_items: 201, last_run_at: "2026-09-05T09:00:00.000Z" },
|
|
{ name: "small-old", status: "running", pending_work_items: 4, last_run_at: "2026-09-04T09:00:00.000Z" },
|
|
{ name: "never-run", status: "running", pending_work_items: 2 },
|
|
{ name: "busy", status: "running", pending_work_items: 50, active_work_kind: "implement", last_run_at: "2026-09-01T09:00:00.000Z" },
|
|
{ name: "blocked", status: "blocked", pending_work_items: 500, last_run_at: "2026-08-01T09:00:00.000Z" },
|
|
].sort(compareAutoModeProjectRows);
|
|
|
|
expect(projects.map((project) => project.name)).toEqual(["never-run", "small-old", "huge-recent", "busy", "blocked"]);
|
|
});
|
|
|
|
it("advertises the complete durable roadmap controller to MCP clients", async () => {
|
|
const server = createProjectBrainMcpServer(env, "auto-contract-test");
|
|
const client = new Client({ name: "auto-contract-test", version: "1.0.0" });
|
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const advertisedTools = (await client.listTools()).tools;
|
|
const tools = advertisedTools.map((tool) => tool.name);
|
|
|
|
expect(tools).toEqual(expect.arrayContaining([
|
|
"list_auto_mode_projects",
|
|
"heartbeat_auto_executor",
|
|
"get_auto_mode_state",
|
|
"get_runner_claim_health",
|
|
"configure_auto_mode",
|
|
"import_auto_roadmap",
|
|
"reorder_auto_queue",
|
|
"reset_auto_queue_order",
|
|
"resume_auto_mode",
|
|
"claim_auto_work",
|
|
"report_auto_work",
|
|
]));
|
|
expect(advertisedTools.find((tool) => tool.name === "claim_auto_work")?.inputSchema).toMatchObject({
|
|
required: expect.arrayContaining(["project_id", "run_id"]),
|
|
});
|
|
expect(advertisedTools.find((tool) => tool.name === "reorder_auto_queue")?.inputSchema).toMatchObject({
|
|
required: expect.arrayContaining(["project_id", "item_ids"]),
|
|
});
|
|
expect(advertisedTools.find((tool) => tool.name === "configure_auto_mode")?.inputSchema).toMatchObject({
|
|
properties: expect.objectContaining({ max_work_items_per_run: expect.any(Object), max_run_minutes: expect.any(Object), max_recovery_minutes: expect.any(Object), pause_after_current: expect.any(Object), scheduling_policy: expect.any(Object) }),
|
|
});
|
|
expect(advertisedTools.find((tool) => tool.name === "get_auto_mode_state")?.inputSchema).toMatchObject({
|
|
properties: expect.objectContaining({ view: expect.any(Object), item_limit: expect.any(Object), event_limit: expect.any(Object) }),
|
|
});
|
|
expect(advertisedTools.find((tool) => tool.name === "report_auto_work")?.inputSchema).toMatchObject({
|
|
properties: expect.objectContaining({ blocker_type: expect.any(Object), retry_after_minutes: expect.any(Object) }),
|
|
});
|
|
} finally {
|
|
await client.close();
|
|
await server.close();
|
|
}
|
|
});
|
|
});
|