@cursor/july 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +0 -0
- package/dist/channels/github/instrument.d.ts +20 -0
- package/dist/channels/github/instrument.d.ts.map +1 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.CvSGaAxk.js → app.DYcC9FY-.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.J1jJbCvs.js → VPLocalSearchBox.o4N_knTV.js} +1 -1
- package/dist/docs/assets/chunks/{theme.C7vfzr7h.js → theme.DQ-njyo0.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
- package/dist/playground/assets/{index-DTG9OsPV.js → index-DqXdAFGa.js} +41 -41
- package/dist/playground/index.html +2 -2
- package/package.json +31 -24
- package/src/artifacts.ts +78 -0
- package/src/bin/agent-serve.version.test.ts +64 -0
- package/src/channels/github/api.test.ts +64 -0
- package/src/channels/github/auth.test.ts +105 -0
- package/src/channels/github/cursor-account.test.ts +204 -0
- package/src/channels/github/forward.test.ts +457 -0
- package/src/channels/github/github.test.ts +943 -0
- package/src/channels/github/replay.test.ts +179 -0
- package/src/channels/slack/api.post-message.test.ts +148 -0
- package/src/channels/slack/approvals.test.ts +328 -0
- package/src/channels/slack/block-actions.test.ts +452 -0
- package/src/channels/slack/bot-mentions.test.ts +267 -0
- package/src/channels/slack/channel-watch.test.ts +363 -0
- package/src/channels/slack/cursor-account.test.ts +253 -0
- package/src/channels/slack/defaults.final-post.test.ts +182 -0
- package/src/channels/slack/dispatch.test.ts +795 -0
- package/src/channels/slack/eval-directive.test.ts +273 -0
- package/src/channels/slack/message-body.test.ts +54 -0
- package/src/channels/slack/nudge-store.test.ts +143 -0
- package/src/channels/slack/slack.test.ts +391 -0
- package/src/channels/slack/stop.test.ts +23 -0
- package/src/channels/slack/thread-context.test.ts +202 -0
- package/src/evals/assertions.test.ts +617 -0
- package/src/evals/assertions.ts +37 -1
- package/src/evals/expect.test.ts +144 -0
- package/src/evals/judge.test.ts +181 -0
- package/src/evals/loaders.test.ts +132 -0
- package/src/evals/matchers.test.ts +95 -0
- package/src/evals/reporters.test.ts +303 -0
- package/src/evals/run-facts.test.ts +259 -0
- package/src/evals.ts +1 -0
- package/src/index.ts +1 -0
- package/src/internal/ab-snapshot.test.ts +325 -0
- package/src/internal/approval-gate.test.ts +49 -0
- package/src/internal/approvals.integration.test.ts +383 -0
- package/src/internal/artifacts-store.test.ts +283 -0
- package/src/internal/artifacts-store.ts +290 -0
- package/src/internal/authored-loaders.test.ts +31 -0
- package/src/internal/builtin-tools/artifacts.test.ts +247 -0
- package/src/internal/builtin-tools/artifacts.ts +74 -0
- package/src/internal/builtin-tools/index.ts +21 -0
- package/src/internal/builtin-tools/reminders.test.ts +201 -0
- package/src/internal/channel-route-schema.test.ts +294 -0
- package/src/internal/chat-attach.test.ts +262 -0
- package/src/internal/cli-deploy.test.ts +1991 -0
- package/src/internal/cli-docs.test.ts +161 -0
- package/src/internal/cli-mcp.test.ts +789 -0
- package/src/internal/cli-skills.test.ts +133 -0
- package/src/internal/cli-slack.test.ts +1654 -0
- package/src/internal/cli-slack.ts +13 -4
- package/src/internal/cloud-merge.test.ts +74 -0
- package/src/internal/cron.test.ts +22 -0
- package/src/internal/cursor/account-mcp.test.ts +807 -0
- package/src/internal/cursor/backend-client.test.ts +591 -0
- package/src/internal/cursor/credentials.test.ts +351 -0
- package/src/internal/cursor/github-credentials.test.ts +136 -0
- package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
- package/src/internal/cursor-account.integration.test.ts +441 -0
- package/src/internal/cursor-event-relay.test.ts +746 -0
- package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
- package/src/internal/cursor-slack-relay.test.ts +525 -0
- package/src/internal/deploy-source.test.ts +111 -0
- package/src/internal/discovery.artifact-tool.test.ts +136 -0
- package/src/internal/discovery.artifacts.test.ts +119 -0
- package/src/internal/discovery.builtin-tools.test.ts +94 -0
- package/src/internal/discovery.concurrency.test.ts +60 -0
- package/src/internal/discovery.cursor-account.test.ts +133 -0
- package/src/internal/discovery.cwd.test.ts +83 -0
- package/src/internal/discovery.hosting.test.ts +80 -0
- package/src/internal/discovery.identity.test.ts +44 -0
- package/src/internal/discovery.ts +102 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/docs-site.test.ts +66 -0
- package/src/internal/duration.test.ts +29 -0
- package/src/internal/eval-judge-model.test.ts +187 -0
- package/src/internal/eval-run-store.cancel.test.ts +142 -0
- package/src/internal/eval-run-store.storage.test.ts +211 -0
- package/src/internal/eval-runner.http.test.ts +403 -0
- package/src/internal/eval-runner.run.test.ts +928 -0
- package/src/internal/evals-client.test.ts +307 -0
- package/src/internal/event-mapper.test.ts +243 -0
- package/src/internal/github-fanout.test.ts +213 -0
- package/src/internal/handleAgentServeTrigger.test.ts +185 -0
- package/src/internal/host-kv.test.ts +82 -0
- package/src/internal/host-platforms.test.ts +126 -0
- package/src/internal/http-channel.test.ts +402 -0
- package/src/internal/init-project.test.ts +270 -0
- package/src/internal/install-cursor-skills.test.ts +262 -0
- package/src/internal/local-env.test.ts +120 -0
- package/src/internal/log-ring.test.ts +31 -0
- package/src/internal/logs-client.test.ts +350 -0
- package/src/internal/mcp-endpoint.test.ts +436 -0
- package/src/internal/mcp-host.test.ts +298 -0
- package/src/internal/mcp-oauth.test.ts +148 -0
- package/src/internal/net.test.ts +17 -0
- package/src/internal/peer-connections.test.ts +128 -0
- package/src/internal/peer-mcp.integration.test.ts +289 -0
- package/src/internal/playground/toolchain.test.ts +53 -0
- package/src/internal/playground-cli.test.ts +187 -0
- package/src/internal/playground-proxy.test.ts +376 -0
- package/src/internal/prompt-context.integration.test.ts +232 -0
- package/src/internal/prompt-context.test.ts +127 -0
- package/src/internal/reminder-runner.test.ts +390 -0
- package/src/internal/reminder-store.test.ts +53 -0
- package/src/internal/request-headers.test.ts +27 -0
- package/src/internal/resolve-prod-target.test.ts +787 -0
- package/src/internal/resolved-connections.test.ts +295 -0
- package/src/internal/router.test.ts +57 -0
- package/src/internal/sdk-runner.test.ts +290 -0
- package/src/internal/server.artifacts.test.ts +303 -0
- package/src/internal/server.ts +43 -0
- package/src/internal/session-engine.artifacts.test.ts +243 -0
- package/src/internal/session-engine.coalesce.test.ts +169 -0
- package/src/internal/session-engine.concurrency.test.ts +250 -0
- package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
- package/src/internal/session-engine.interrupt.test.ts +577 -0
- package/src/internal/session-engine.storage.test.ts +547 -0
- package/src/internal/session-engine.ts +125 -0
- package/src/internal/session-urls.test.ts +28 -0
- package/src/internal/sessions-client.test.ts +518 -0
- package/src/internal/storage-coordinator.test.ts +517 -0
- package/src/internal/storage-coordinator.ts +62 -0
- package/src/internal/tool-call.test.ts +458 -0
- package/src/internal/tool-result.test.ts +52 -0
- package/src/internal/trajectory.approvals.test.ts +83 -0
- package/src/internal/trajectory.subagents.test.ts +198 -0
- package/src/internal/turn-governor.test.ts +137 -0
- package/src/internal/update-check.test.ts +485 -0
- package/src/internal/workspace.test.ts +207 -0
- package/src/storage-backends/cursor-hosted.test.ts +121 -0
- package/src/storage.ts +6 -0
- package/src/types.ts +134 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +0 -1
- package/dist/playground/assets/index-CidizGZv.css +0 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { httpChannel } from "../../channels.js";
|
|
6
|
+
import type { AgentProject, ReminderInfo, SessionRecord } from "../../types.js";
|
|
7
|
+
import { ReminderRunner } from "../reminder-runner.js";
|
|
8
|
+
import { SessionEngine } from "../session-engine.js";
|
|
9
|
+
import { builtinTools } from "./index.js";
|
|
10
|
+
|
|
11
|
+
function testProject(rootDir: string): AgentProject {
|
|
12
|
+
return {
|
|
13
|
+
rootDir,
|
|
14
|
+
agentDir: join(rootDir, "agent"),
|
|
15
|
+
name: "builtin-reminders-test",
|
|
16
|
+
agent: {
|
|
17
|
+
name: "builtin-reminders-test",
|
|
18
|
+
runtime: "local",
|
|
19
|
+
instructions: "Test agent instructions.",
|
|
20
|
+
tools: builtinTools({ reminders: true }),
|
|
21
|
+
skills: [],
|
|
22
|
+
connections: [],
|
|
23
|
+
subagents: [],
|
|
24
|
+
seedFiles: [],
|
|
25
|
+
},
|
|
26
|
+
httpChannel: httpChannel(),
|
|
27
|
+
channels: [],
|
|
28
|
+
schedules: [],
|
|
29
|
+
hooks: [],
|
|
30
|
+
abs: [],
|
|
31
|
+
diagnostics: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cleanups: Array<() => Promise<void>> = [];
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
while (cleanups.length > 0) {
|
|
39
|
+
await cleanups.pop()?.();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function makeEngine(): Promise<SessionEngine> {
|
|
44
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-builtin-rem-"));
|
|
45
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
46
|
+
const engine = new SessionEngine({
|
|
47
|
+
project: testProject(stateRoot),
|
|
48
|
+
stateRoot,
|
|
49
|
+
runner: {
|
|
50
|
+
runTurn: () => {
|
|
51
|
+
throw new Error("no model turns in this test");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
engine.setReminders(new ReminderRunner(engine));
|
|
56
|
+
return engine;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function saveSession(
|
|
60
|
+
engine: SessionEngine,
|
|
61
|
+
sessionId: string,
|
|
62
|
+
continuationKey: string | null
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const record: SessionRecord = {
|
|
65
|
+
sessionId,
|
|
66
|
+
channelId: "http",
|
|
67
|
+
mode: "chat",
|
|
68
|
+
continuationKey,
|
|
69
|
+
auth: null,
|
|
70
|
+
turnCount: 0,
|
|
71
|
+
createdAt: new Date().toISOString(),
|
|
72
|
+
updatedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
await engine.sessions.save(record);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("built-in reminder tools", () => {
|
|
78
|
+
it("creates, lists, and cancels a reminder bound to the conversation", async () => {
|
|
79
|
+
const engine = await makeEngine();
|
|
80
|
+
await saveSession(engine, "ses_a", "conv:a");
|
|
81
|
+
|
|
82
|
+
const created = await engine.callTool(
|
|
83
|
+
"reminders_create",
|
|
84
|
+
{
|
|
85
|
+
prompt: "Re-check CI.",
|
|
86
|
+
every: "30m",
|
|
87
|
+
until: "Cancel once CI is green.",
|
|
88
|
+
},
|
|
89
|
+
{ sessionId: "ses_a" }
|
|
90
|
+
);
|
|
91
|
+
expect(created.isError).toBe(false);
|
|
92
|
+
const reminder = created.result as { reminderId: string; until: string };
|
|
93
|
+
expect(reminder.reminderId).toMatch(/^rem_/);
|
|
94
|
+
expect(reminder.until).toBe("Cancel once CI is green.");
|
|
95
|
+
|
|
96
|
+
const listed = await engine.callTool(
|
|
97
|
+
"reminders_list",
|
|
98
|
+
{},
|
|
99
|
+
{ sessionId: "ses_a" }
|
|
100
|
+
);
|
|
101
|
+
const list = listed.result as { reminders: Array<{ reminderId: string }> };
|
|
102
|
+
expect(list.reminders.map((r) => r.reminderId)).toEqual([
|
|
103
|
+
reminder.reminderId,
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const cancelled = await engine.callTool(
|
|
107
|
+
"reminders_cancel",
|
|
108
|
+
{ id: reminder.reminderId },
|
|
109
|
+
{ sessionId: "ses_a" }
|
|
110
|
+
);
|
|
111
|
+
expect(cancelled.isError).toBe(false);
|
|
112
|
+
expect(cancelled.result).toEqual({
|
|
113
|
+
reminderId: reminder.reminderId,
|
|
114
|
+
status: "cancelled",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const after = await engine.callTool(
|
|
118
|
+
"reminders_list",
|
|
119
|
+
{},
|
|
120
|
+
{ sessionId: "ses_a" }
|
|
121
|
+
);
|
|
122
|
+
expect((after.result as { reminders: unknown[] }).reminders).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("refuses to cancel another conversation's reminder", async () => {
|
|
126
|
+
const engine = await makeEngine();
|
|
127
|
+
await saveSession(engine, "ses_a", "conv:a");
|
|
128
|
+
await saveSession(engine, "ses_b", "conv:b");
|
|
129
|
+
|
|
130
|
+
const created = await engine.callTool(
|
|
131
|
+
"reminders_create",
|
|
132
|
+
{ prompt: "wake", delay: "5m" },
|
|
133
|
+
{ sessionId: "ses_a" }
|
|
134
|
+
);
|
|
135
|
+
const { reminderId } = created.result as { reminderId: string };
|
|
136
|
+
|
|
137
|
+
const foreign = await engine.callTool(
|
|
138
|
+
"reminders_cancel",
|
|
139
|
+
{ id: reminderId },
|
|
140
|
+
{ sessionId: "ses_b" }
|
|
141
|
+
);
|
|
142
|
+
expect(foreign.isError).toBe(true);
|
|
143
|
+
expect(foreign.errorMessage).toContain("in this conversation");
|
|
144
|
+
|
|
145
|
+
const mine = await engine.host.reminders?.list({ status: "armed" });
|
|
146
|
+
expect((mine as ReminderInfo[]).map((r) => r.id)).toEqual([reminderId]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps reminders addressed across HTTP continuation rotation", async () => {
|
|
150
|
+
const engine = await makeEngine();
|
|
151
|
+
await saveSession(engine, "ses_a", "conv:a");
|
|
152
|
+
|
|
153
|
+
const created = await engine.callTool(
|
|
154
|
+
"reminders_create",
|
|
155
|
+
{ prompt: "wake", delay: "5m" },
|
|
156
|
+
{ sessionId: "ses_a" }
|
|
157
|
+
);
|
|
158
|
+
const { reminderId } = created.result as { reminderId: string };
|
|
159
|
+
|
|
160
|
+
// The built-in HTTP channel rotates the token after every follow-up.
|
|
161
|
+
await engine.setContinuationToken("ses_a", "conv:rotated");
|
|
162
|
+
|
|
163
|
+
const listed = await engine.callTool(
|
|
164
|
+
"reminders_list",
|
|
165
|
+
{},
|
|
166
|
+
{ sessionId: "ses_a" }
|
|
167
|
+
);
|
|
168
|
+
const list = listed.result as { reminders: Array<{ reminderId: string }> };
|
|
169
|
+
expect(list.reminders.map((r) => r.reminderId)).toEqual([reminderId]);
|
|
170
|
+
|
|
171
|
+
// Fire path must still resolve the session (no session_gone disarm).
|
|
172
|
+
const dispatched = await engine.host.reminders?.dispatch(reminderId);
|
|
173
|
+
expect(dispatched).toMatchObject({ outcome: "fired", reason: "delivered" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects sub-minute recurring reminders", async () => {
|
|
177
|
+
const engine = await makeEngine();
|
|
178
|
+
await saveSession(engine, "ses_a", "conv:a");
|
|
179
|
+
|
|
180
|
+
const outcome = await engine.callTool(
|
|
181
|
+
"reminders_create",
|
|
182
|
+
{ prompt: "wake", every: "5s" },
|
|
183
|
+
{ sessionId: "ses_a" }
|
|
184
|
+
);
|
|
185
|
+
expect(outcome.isError).toBe(true);
|
|
186
|
+
expect(outcome.errorMessage).toContain('every >= "1m"');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("rejects creates from sessions without a continuation key", async () => {
|
|
190
|
+
const engine = await makeEngine();
|
|
191
|
+
await saveSession(engine, "ses_nokey", null);
|
|
192
|
+
|
|
193
|
+
const outcome = await engine.callTool(
|
|
194
|
+
"reminders_create",
|
|
195
|
+
{ prompt: "wake", delay: "5m" },
|
|
196
|
+
{ sessionId: "ses_nokey" }
|
|
197
|
+
);
|
|
198
|
+
expect(outcome.isError).toBe(true);
|
|
199
|
+
expect(outcome.errorMessage).toContain("followable");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { defineChannel, GET, httpChannel, POST } from "../channels.js";
|
|
7
|
+
import type { AgentProject } from "../types.js";
|
|
8
|
+
import { projectInfo } from "./discovery.js";
|
|
9
|
+
import { toolInputJsonSchema, validateToolInput } from "./json-schema.js";
|
|
10
|
+
import { type AgentMount, startServer } from "./server.js";
|
|
11
|
+
|
|
12
|
+
const cleanups: Array<() => Promise<void>> = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
while (cleanups.length > 0) {
|
|
16
|
+
const cleanup = cleanups.pop();
|
|
17
|
+
if (cleanup !== undefined) await cleanup();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("channel route schemas", () => {
|
|
22
|
+
it("requires bodySchema on POST and querySchema on GET", () => {
|
|
23
|
+
const bodySchema = z.object({ pr: z.string() });
|
|
24
|
+
const querySchema = z.object({ status: z.string().optional() });
|
|
25
|
+
const channel = defineChannel({
|
|
26
|
+
routes: [
|
|
27
|
+
POST("/", {
|
|
28
|
+
description: "start",
|
|
29
|
+
bodySchema,
|
|
30
|
+
handler: async () => Response.json({ ok: true }),
|
|
31
|
+
}),
|
|
32
|
+
GET("/findings", {
|
|
33
|
+
querySchema,
|
|
34
|
+
handler: async () => Response.json({ ok: true }),
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const post = channel.routes?.[0];
|
|
40
|
+
const get = channel.routes?.[1];
|
|
41
|
+
expect(post?.method).toBe("POST");
|
|
42
|
+
expect(post?.description).toBe("start");
|
|
43
|
+
expect(post?.bodySchema).toBe(bodySchema);
|
|
44
|
+
expect(get?.querySchema).toBe(querySchema);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("converts Zod bodySchema to JSON Schema like tools", () => {
|
|
48
|
+
const schema = z.object({
|
|
49
|
+
pr: z.string(),
|
|
50
|
+
mode: z.enum(["suggest", "apply"]).optional(),
|
|
51
|
+
});
|
|
52
|
+
const json = toolInputJsonSchema(schema);
|
|
53
|
+
expect(json.type).toBe("object");
|
|
54
|
+
expect(json.properties).toMatchObject({
|
|
55
|
+
pr: { type: "string" },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("validateToolInput rejects invalid channel bodies", () => {
|
|
60
|
+
const schema = z.object({ pr: z.string() });
|
|
61
|
+
const bad = validateToolInput(schema, { mode: "suggest" });
|
|
62
|
+
expect(bad.ok).toBe(false);
|
|
63
|
+
if (!bad.ok) {
|
|
64
|
+
expect(bad.message).toContain("pr");
|
|
65
|
+
}
|
|
66
|
+
const good = validateToolInput(schema, { pr: "acme/repo#1" });
|
|
67
|
+
expect(good.ok).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("projectInfo exposes bodySchema and querySchema on channel routes", () => {
|
|
71
|
+
const bodySchema = z.object({ pr: z.string() });
|
|
72
|
+
const querySchema = z.object({ status: z.string().optional() });
|
|
73
|
+
const channel = defineChannel({
|
|
74
|
+
routes: [
|
|
75
|
+
POST("/", {
|
|
76
|
+
bodySchema,
|
|
77
|
+
handler: async () => Response.json({ ok: true }),
|
|
78
|
+
}),
|
|
79
|
+
GET("/items", {
|
|
80
|
+
querySchema,
|
|
81
|
+
handler: async () => Response.json({ ok: true }),
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
const project: AgentProject = {
|
|
86
|
+
rootDir: "/tmp/demo",
|
|
87
|
+
agentDir: "/tmp/demo/agent",
|
|
88
|
+
name: "demo",
|
|
89
|
+
agent: {
|
|
90
|
+
name: "demo",
|
|
91
|
+
runtime: "local",
|
|
92
|
+
instructions: "test",
|
|
93
|
+
tools: [],
|
|
94
|
+
skills: [],
|
|
95
|
+
connections: [],
|
|
96
|
+
subagents: [],
|
|
97
|
+
seedFiles: [],
|
|
98
|
+
},
|
|
99
|
+
httpChannel: httpChannel(),
|
|
100
|
+
channels: [{ id: "demo", definition: channel }],
|
|
101
|
+
schedules: [],
|
|
102
|
+
hooks: [],
|
|
103
|
+
abs: [],
|
|
104
|
+
diagnostics: [],
|
|
105
|
+
};
|
|
106
|
+
const info = projectInfo(project);
|
|
107
|
+
const demo = info.channels.find((c) => c.id === "demo");
|
|
108
|
+
expect(demo?.routes).toEqual([
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
method: "POST",
|
|
111
|
+
url: "/v1/channels/demo/",
|
|
112
|
+
bodySchema: toolInputJsonSchema(bodySchema),
|
|
113
|
+
}),
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
method: "GET",
|
|
116
|
+
url: "/v1/channels/demo/items",
|
|
117
|
+
querySchema: toolInputJsonSchema(querySchema),
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("projectInfo surfaces channel meta (kind, slackChannelPosts)", () => {
|
|
123
|
+
const channel = defineChannel({
|
|
124
|
+
meta: { kind: "slack", slackChannelPosts: ["#issues-glass"] },
|
|
125
|
+
});
|
|
126
|
+
const project: AgentProject = {
|
|
127
|
+
rootDir: "/tmp/demo",
|
|
128
|
+
agentDir: "/tmp/demo/agent",
|
|
129
|
+
name: "demo",
|
|
130
|
+
agent: {
|
|
131
|
+
name: "demo",
|
|
132
|
+
runtime: "local",
|
|
133
|
+
instructions: "test",
|
|
134
|
+
tools: [],
|
|
135
|
+
skills: [],
|
|
136
|
+
connections: [],
|
|
137
|
+
subagents: [],
|
|
138
|
+
seedFiles: [],
|
|
139
|
+
},
|
|
140
|
+
httpChannel: httpChannel(),
|
|
141
|
+
channels: [{ id: "slack", definition: channel }],
|
|
142
|
+
schedules: [],
|
|
143
|
+
hooks: [],
|
|
144
|
+
abs: [],
|
|
145
|
+
diagnostics: [],
|
|
146
|
+
};
|
|
147
|
+
const info = projectInfo(project);
|
|
148
|
+
const slack = info.channels.find((c) => c.id === "slack");
|
|
149
|
+
expect(slack?.kind).toBe("slack");
|
|
150
|
+
expect(slack?.slackChannelPosts).toEqual(["#issues-glass"]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("channel route HTTP validation", () => {
|
|
155
|
+
async function startSchemaChannelServer(): Promise<{ url: string }> {
|
|
156
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-chschema-"));
|
|
157
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
158
|
+
const bodySchema = z.object({ pr: z.string().min(1) });
|
|
159
|
+
const querySchema = z.object({ status: z.string().optional() });
|
|
160
|
+
const channel = defineChannel({
|
|
161
|
+
routes: [
|
|
162
|
+
POST("/", {
|
|
163
|
+
bodySchema,
|
|
164
|
+
handler: async (_req, args) =>
|
|
165
|
+
Response.json({ body: args.body ?? null }),
|
|
166
|
+
}),
|
|
167
|
+
GET("/items", {
|
|
168
|
+
querySchema,
|
|
169
|
+
handler: async (_req, args) =>
|
|
170
|
+
Response.json({ query: args.query ?? null }),
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
const project: AgentProject = {
|
|
175
|
+
rootDir: stateRoot,
|
|
176
|
+
agentDir: join(stateRoot, "agent"),
|
|
177
|
+
name: "schema-test",
|
|
178
|
+
agent: {
|
|
179
|
+
name: "schema-test",
|
|
180
|
+
runtime: "local",
|
|
181
|
+
instructions: "test",
|
|
182
|
+
tools: [],
|
|
183
|
+
skills: [],
|
|
184
|
+
connections: [],
|
|
185
|
+
subagents: [],
|
|
186
|
+
seedFiles: [],
|
|
187
|
+
},
|
|
188
|
+
httpChannel: httpChannel(),
|
|
189
|
+
channels: [{ id: "schema", definition: channel }],
|
|
190
|
+
schedules: [],
|
|
191
|
+
hooks: [],
|
|
192
|
+
abs: [],
|
|
193
|
+
diagnostics: [],
|
|
194
|
+
};
|
|
195
|
+
const mounts: AgentMount[] = [{ slug: "", project }];
|
|
196
|
+
const handle = await startServer(mounts, {
|
|
197
|
+
port: 0,
|
|
198
|
+
stateRoot,
|
|
199
|
+
playground: false,
|
|
200
|
+
schedules: false,
|
|
201
|
+
logger: () => {},
|
|
202
|
+
});
|
|
203
|
+
cleanups.push(() => handle.close());
|
|
204
|
+
return { url: handle.url };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
it("returns 400 for invalid body before the handler runs", async () => {
|
|
208
|
+
const { url } = await startSchemaChannelServer();
|
|
209
|
+
const response = await fetch(`${url}/v1/channels/schema/`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: { "content-type": "application/json" },
|
|
212
|
+
body: JSON.stringify({}),
|
|
213
|
+
});
|
|
214
|
+
expect(response.status).toBe(400);
|
|
215
|
+
const body = (await response.json()) as { error: string };
|
|
216
|
+
expect(body.error).toContain("pr");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("accepts an empty request body for z.object({}) routes", async () => {
|
|
220
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-empty-body-"));
|
|
221
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
222
|
+
const emptyBodySchema = z.object({});
|
|
223
|
+
const channel = defineChannel({
|
|
224
|
+
routes: [
|
|
225
|
+
POST("/noop", {
|
|
226
|
+
bodySchema: emptyBodySchema,
|
|
227
|
+
handler: async (_req, args) =>
|
|
228
|
+
Response.json({ body: args.body ?? null }),
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
const project: AgentProject = {
|
|
233
|
+
rootDir: stateRoot,
|
|
234
|
+
agentDir: join(stateRoot, "agent"),
|
|
235
|
+
name: "empty-body-test",
|
|
236
|
+
agent: {
|
|
237
|
+
name: "empty-body-test",
|
|
238
|
+
runtime: "local",
|
|
239
|
+
instructions: "test",
|
|
240
|
+
tools: [],
|
|
241
|
+
skills: [],
|
|
242
|
+
connections: [],
|
|
243
|
+
subagents: [],
|
|
244
|
+
seedFiles: [],
|
|
245
|
+
},
|
|
246
|
+
httpChannel: httpChannel(),
|
|
247
|
+
channels: [{ id: "noop", definition: channel }],
|
|
248
|
+
schedules: [],
|
|
249
|
+
hooks: [],
|
|
250
|
+
abs: [],
|
|
251
|
+
diagnostics: [],
|
|
252
|
+
};
|
|
253
|
+
const handle = await startServer([{ slug: "", project }], {
|
|
254
|
+
port: 0,
|
|
255
|
+
stateRoot,
|
|
256
|
+
playground: false,
|
|
257
|
+
schedules: false,
|
|
258
|
+
logger: () => {},
|
|
259
|
+
});
|
|
260
|
+
cleanups.push(() => handle.close());
|
|
261
|
+
|
|
262
|
+
const response = await fetch(`${handle.url}/v1/channels/noop/noop`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "content-type": "application/json" },
|
|
265
|
+
});
|
|
266
|
+
expect(response.status).toBe(200);
|
|
267
|
+
expect(
|
|
268
|
+
((await response.json()) as { body: Record<string, never> }).body
|
|
269
|
+
).toEqual({});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("passes validated body and query to the handler", async () => {
|
|
273
|
+
const { url } = await startSchemaChannelServer();
|
|
274
|
+
const post = await fetch(`${url}/v1/channels/schema/`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: { "content-type": "application/json" },
|
|
277
|
+
body: JSON.stringify({ pr: "acme/repo#1" }),
|
|
278
|
+
});
|
|
279
|
+
expect(post.status).toBe(200);
|
|
280
|
+
expect(((await post.json()) as { body: { pr: string } }).body).toEqual({
|
|
281
|
+
pr: "acme/repo#1",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const get = await fetch(
|
|
285
|
+
`${url}/v1/channels/schema/items?status=OPEN&ignored=1`
|
|
286
|
+
);
|
|
287
|
+
expect(get.status).toBe(200);
|
|
288
|
+
expect(((await get.json()) as { query: { status: string } }).query).toEqual(
|
|
289
|
+
{
|
|
290
|
+
status: "OPEN",
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
});
|