Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions server/decision-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export type DecisionKind =

/** Who or what produced the decision. The AutoVerdictSource values carry
* straight through from auto-approve.ts; `question` marks cards a rule may
* never answer, `auto-fallback` a card shown after delivery failed, `user`
* the human's answer, and auto-review sources the isolated model reviewer. */
* never answer, `auto-fallback` a card shown after delivery failed, `routine`
* a durable chat scheduling proposal, `user` the human's answer, and
* auto-review sources the isolated model reviewer. */
export type DecisionSource =
| AutoVerdictSource
| "question"
| "auto-fallback"
| "routine"
| "user"
| "auto-review"
| "auto-review-shadow";
Expand Down
156 changes: 155 additions & 1 deletion server/drivers/agents-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ let lastDelegateBody: any = null;
let delegateResponse: unknown = { queued: true, message: "Delegation queued." };
let lastCreateBody: any = null;
let lastCredentialBody: any = null;
let lastRoutineQuery = "";
let routinesResponse: unknown = {
now: "2026-08-28T10:30:00.000Z",
timeZone: "Asia/Kolkata",
routines: [
{
id: "routine-1",
name: "Morning brief",
enabled: true,
schedule: { type: "daily", time: "09:00", weekdays: [1, 2, 3, 4, 5] },
nextRunAt: "2026-08-31T03:30:00.000Z",
},
],
};
let lastRoutineRequestBody: any = null;

let child: ChildProcess;
const pending = new Map<number, (msg: any) => void>();
Expand Down Expand Up @@ -94,6 +109,21 @@ beforeAll(async () => {
});
return;
}
if (req.method === "GET" && req.url?.startsWith("/api/internal/routines?")) {
lastRoutineQuery = req.url;
res.writeHead(200, { "content-type": "application/json" });
return res.end(JSON.stringify(routinesResponse));
}
if (req.method === "POST" && req.url === "/api/internal/routine-requests") {
let data = "";
req.on("data", (c) => (data += c));
req.on("end", () => {
lastRoutineRequestBody = JSON.parse(data);
res.writeHead(201, { "content-type": "application/json" });
res.end(JSON.stringify({ requestId: "routine-request-1", summary: "Weekdays at 09:00 (Asia/Kolkata)" }));
});
return;
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "unknown" }));
});
Expand Down Expand Up @@ -132,7 +162,7 @@ afterAll(async () => {
});

describe("agents-proxy MCP surface", () => {
it("answers the MCP handshake and lists all five tools", async () => {
it("answers the MCP handshake and lists all eight tools", async () => {
const init = await rpc("initialize", { protocolVersion: "2024-11-05" });
expect(init.result.serverInfo.name).toContain("agents");
const list = await rpc("tools/list");
Expand All @@ -142,7 +172,35 @@ describe("agents-proxy MCP surface", () => {
"delegate_bot",
"create_bot",
"request_credential",
"list_routines",
"propose_routine",
"propose_routine_action",
]);
});

it("publishes explicit, bounded routine schedule schemas", async () => {
const list = await rpc("tools/list");
const create = list.result.tools.find((t: { name: string }) => t.name === "propose_routine");
expect(create.inputSchema.required).toEqual(["name", "instructions", "schedule"]);
expect(create.inputSchema.properties.schedule.oneOf).toEqual(
expect.arrayContaining([
expect.objectContaining({ required: ["type", "at"] }),
expect.objectContaining({ required: ["type", "time", "weekdays"] }),
]),
);
const weekly = create.inputSchema.properties.schedule.oneOf.find(
(option: any) => option.properties.type.const === "weekly",
);
expect(weekly.properties.weekdays.items.enum).toEqual([
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]);
expect(create.description).toContain("does NOT enable");
});

it("list_bots renders the roster and authenticates with the shared token", async () => {
Expand Down Expand Up @@ -245,6 +303,102 @@ describe("agents-proxy MCP surface", () => {
expect(lastCredentialBody).toBeNull();
});

it("lists only the current bot's routines with authoritative time context", async () => {
routinesResponse = {
now: "2026-08-28T10:30:00.000Z",
timeZone: "Asia/Kolkata",
routines: [{ id: "routine-1", name: "Morning brief", enabled: true }],
};
const res = await callTool("list_routines", {});
expect(res.result.content[0].text).toContain("routine-1");
expect(res.result.content[0].text).toContain("Asia/Kolkata");
const query = new URL(lastRoutineQuery, "http://localhost").searchParams;
expect(query.get("fromBotId")).toBe("bot-asker");
expect(query.get("fromThreadId")).toBe("thread-asker-routine");
expect(lastAuth).toBe(`Bearer ${TOKEN}`);
});

it("proposes a weekly routine through a confirmation-only request", async () => {
lastRoutineRequestBody = null;
const res = await callTool("propose_routine", {
name: "Morning brief",
instructions: "Summarize today's priorities.",
schedule: { type: "weekly", time: "09:00", weekdays: ["monday", "friday"] },
run_on: "maus",
duration_minutes: 45,
});
expect(lastRoutineRequestBody).toEqual({
fromBotId: "bot-asker",
fromThreadId: "thread-asker-routine",
action: "create",
routine: {
name: "Morning brief",
instructions: "Summarize today's priorities.",
schedule: { type: "weekly", time: "09:00", weekdays: ["monday", "friday"] },
runOn: "maus",
durationMinutes: 45,
},
});
expect(res.result.content[0].text).toContain("confirmation card");
expect(res.result.content[0].text).toContain("has not been applied");
expect(res.result.content[0].text).toContain("do not claim");
expect(res.result.isError).toBeFalsy();
});

it("proposes a one-time routine with the explicit-offset timestamp intact", async () => {
await callTool("propose_routine", {
name: "Send follow-up",
instructions: "Draft the follow-up for review.",
schedule: { type: "once", at: "2026-09-01T09:00:00+05:30" },
});
expect(lastRoutineRequestBody.routine.schedule).toEqual({
type: "once",
at: "2026-09-01T09:00:00+05:30",
});
});

it("proposes routine updates and destructive actions without applying them", async () => {
const update = await callTool("propose_routine_action", {
routine_id: "routine-1",
action: "update",
changes: { name: "Weekday brief", duration_minutes: 60 },
});
expect(lastRoutineRequestBody).toEqual({
fromBotId: "bot-asker",
fromThreadId: "thread-asker-routine",
action: "update",
routineId: "routine-1",
changes: { name: "Weekday brief", durationMinutes: 60 },
});
expect(update.result.content[0].text).toContain("has not been applied");

await callTool("propose_routine_action", { routine_id: "routine-1", action: "delete" });
expect(lastRoutineRequestBody).toEqual({
fromBotId: "bot-asker",
fromThreadId: "thread-asker-routine",
action: "delete",
routineId: "routine-1",
});
});

it("rejects malformed routine proposals before calling the harness", async () => {
lastRoutineRequestBody = null;
const missing = await callTool("propose_routine", {
name: "No schedule",
instructions: "This cannot be scheduled yet.",
});
expect(missing.result.isError).toBe(true);
expect(lastRoutineRequestBody).toBeNull();

const badUpdate = await callTool("propose_routine_action", {
routine_id: "routine-1",
action: "update",
changes: {},
});
expect(badUpdate.result.isError).toBe(true);
expect(lastRoutineRequestBody).toBeNull();
});

it("rejects unknown tools with -32602", async () => {
const res = await rpc("tools/call", { name: "made_up", arguments: {} });
expect(res.error.code).toBe(-32602);
Expand Down
Loading
Loading