@cursor/july 0.1.24 → 0.1.26
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-channel.ts +19 -2
- package/src/channels/github/github.test.ts +985 -0
- package/src/channels/github/replay.test.ts +179 -0
- package/src/channels/github/types.ts +6 -0
- package/src/channels/slack/api.post-message.test.ts +148 -0
- package/src/channels/slack/api.ts +3 -1
- package/src/channels/slack/approvals.test.ts +328 -0
- package/src/channels/slack/block-actions.test.ts +469 -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 +301 -0
- package/src/channels/slack/cursor-account.ts +3 -1
- package/src/channels/slack/defaults.final-post.test.ts +182 -0
- package/src/channels/slack/dispatch.test.ts +875 -0
- package/src/channels/slack/dispatch.ts +12 -1
- package/src/channels/slack/eval-directive.test.ts +273 -0
- package/src/channels/slack/interactive.ts +3 -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-channel.ts +6 -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/channels/slack/types.ts +6 -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 +432 -0
- package/src/internal/reminder-runner.ts +1 -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/schedule-runner.ts +1 -0
- package/src/internal/sdk-runner.test.ts +290 -0
- package/src/internal/server.artifacts.test.ts +334 -0
- package/src/internal/server.ts +45 -0
- package/src/internal/session-engine.artifacts.test.ts +293 -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 +129 -1
- 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 +152 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +0 -1
- package/dist/playground/assets/index-CidizGZv.css +0 -1
|
@@ -0,0 +1,334 @@
|
|
|
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 { defineArtifacts } from "../artifacts.js";
|
|
7
|
+
import { defineChannel, httpChannel } from "../channels.js";
|
|
8
|
+
import { defineTool } from "../tools.js";
|
|
9
|
+
import type {
|
|
10
|
+
AgentProject,
|
|
11
|
+
ArtifactRecord,
|
|
12
|
+
AuthContext,
|
|
13
|
+
DiscoveredChannel,
|
|
14
|
+
DiscoveredTool,
|
|
15
|
+
ServeOptions,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { toolInputJsonSchema } from "./json-schema.js";
|
|
18
|
+
import type { AgentRunner } from "./sdk-runner.js";
|
|
19
|
+
import { type AgentMount, startServer } from "./server.js";
|
|
20
|
+
import { SessionEngine } from "./session-engine.js";
|
|
21
|
+
|
|
22
|
+
const seedTool = defineTool({
|
|
23
|
+
description: "Tags one artifact (test seed).",
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
kind: z.string(),
|
|
26
|
+
key: z.string().optional(),
|
|
27
|
+
note: z.string().optional(),
|
|
28
|
+
}),
|
|
29
|
+
execute: async ({ kind, key, note }, ctx) => {
|
|
30
|
+
const record = await ctx.artifacts.tag({
|
|
31
|
+
kind,
|
|
32
|
+
key,
|
|
33
|
+
data: { note: note ?? null },
|
|
34
|
+
});
|
|
35
|
+
return { id: record.id };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function testProject(
|
|
40
|
+
rootDir: string,
|
|
41
|
+
options?: { channels?: DiscoveredChannel[] }
|
|
42
|
+
): AgentProject {
|
|
43
|
+
const discovered: DiscoveredTool = {
|
|
44
|
+
name: "seed",
|
|
45
|
+
description: seedTool.description,
|
|
46
|
+
inputSchema: toolInputJsonSchema(seedTool.inputSchema),
|
|
47
|
+
execution: "server",
|
|
48
|
+
needsApproval: false,
|
|
49
|
+
definition: seedTool,
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
rootDir,
|
|
53
|
+
agentDir: join(rootDir, "agent"),
|
|
54
|
+
name: "artifacts-route-test",
|
|
55
|
+
agent: {
|
|
56
|
+
name: "artifacts-route-test",
|
|
57
|
+
runtime: "local",
|
|
58
|
+
instructions: "Test agent.",
|
|
59
|
+
tools: [discovered],
|
|
60
|
+
skills: [],
|
|
61
|
+
connections: [],
|
|
62
|
+
subagents: [],
|
|
63
|
+
seedFiles: [],
|
|
64
|
+
},
|
|
65
|
+
httpChannel: httpChannel(),
|
|
66
|
+
channels: options?.channels ?? [],
|
|
67
|
+
schedules: [],
|
|
68
|
+
hooks: [],
|
|
69
|
+
abs: [],
|
|
70
|
+
artifacts: defineArtifacts({
|
|
71
|
+
kinds: {
|
|
72
|
+
"reviewed-pr": { description: "A reviewed PR." },
|
|
73
|
+
report: { description: "A report." },
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
diagnostics: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const noTurnRunner: AgentRunner = {
|
|
81
|
+
runTurn: () => {
|
|
82
|
+
throw new Error("runTurn must not be called");
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const cleanups: Array<() => Promise<void>> = [];
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
while (cleanups.length > 0) {
|
|
90
|
+
await cleanups.pop()?.();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
async function startTestServer(
|
|
95
|
+
options?: Partial<ServeOptions>,
|
|
96
|
+
projectOptions?: { channels?: DiscoveredChannel[] }
|
|
97
|
+
) {
|
|
98
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-artroute-"));
|
|
99
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
100
|
+
const mounts: AgentMount[] = [
|
|
101
|
+
{ slug: "", project: testProject(stateRoot, projectOptions) },
|
|
102
|
+
];
|
|
103
|
+
const handle = await startServer(mounts, {
|
|
104
|
+
port: 0,
|
|
105
|
+
stateRoot,
|
|
106
|
+
playground: false,
|
|
107
|
+
schedules: false,
|
|
108
|
+
logger: () => {},
|
|
109
|
+
runner: noTurnRunner,
|
|
110
|
+
...options,
|
|
111
|
+
});
|
|
112
|
+
cleanups.push(() => handle.close());
|
|
113
|
+
return handle;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function listArtifacts(
|
|
117
|
+
url: string,
|
|
118
|
+
query = "",
|
|
119
|
+
headers: Record<string, string> = {}
|
|
120
|
+
): Promise<{ status: number; artifacts: ArtifactRecord[] }> {
|
|
121
|
+
const response = await fetch(`${url}/v1/artifacts${query}`, { headers });
|
|
122
|
+
if (response.status !== 200) {
|
|
123
|
+
return { status: response.status, artifacts: [] };
|
|
124
|
+
}
|
|
125
|
+
const body = (await response.json()) as { artifacts: ArtifactRecord[] };
|
|
126
|
+
return { status: response.status, artifacts: body.artifacts };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Force distinct `updatedAt` millis so newest-first ordering is stable. */
|
|
130
|
+
function tick(): Promise<void> {
|
|
131
|
+
return new Promise((resolve) => setTimeout(resolve, 5));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describe("GET /v1/artifacts", () => {
|
|
135
|
+
it("lists newest-updated first with kind filtering (dev includeAll)", async () => {
|
|
136
|
+
const handle = await startTestServer({ dev: true });
|
|
137
|
+
await handle.callTool("seed", { kind: "reviewed-pr", key: "pr-1" });
|
|
138
|
+
await tick();
|
|
139
|
+
await handle.callTool("seed", { kind: "report", key: "r-1" });
|
|
140
|
+
await tick();
|
|
141
|
+
await handle.callTool("seed", { kind: "reviewed-pr", key: "pr-2" });
|
|
142
|
+
|
|
143
|
+
const all = await listArtifacts(handle.url);
|
|
144
|
+
expect(all.status).toBe(200);
|
|
145
|
+
expect(all.artifacts.map((a) => a.key)).toEqual(["pr-2", "r-1", "pr-1"]);
|
|
146
|
+
|
|
147
|
+
const reports = await listArtifacts(handle.url, "?kind=report");
|
|
148
|
+
expect(reports.artifacts.map((a) => a.key)).toEqual(["r-1"]);
|
|
149
|
+
|
|
150
|
+
const none = await listArtifacts(handle.url, "?kind=missing");
|
|
151
|
+
expect(none.artifacts).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("filters by sessionId", async () => {
|
|
155
|
+
// Scratch (session-less) seeds never match a sessionId filter.
|
|
156
|
+
const handle = await startTestServer({ dev: true });
|
|
157
|
+
await handle.callTool("seed", { kind: "report", key: "loose" });
|
|
158
|
+
const filtered = await listArtifacts(handle.url, "?sessionId=ses_other");
|
|
159
|
+
expect(filtered.status).toBe(200);
|
|
160
|
+
expect(filtered.artifacts).toEqual([]);
|
|
161
|
+
expect((await listArtifacts(handle.url)).artifacts).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("bounds the limit and rejects non-positive limits", async () => {
|
|
165
|
+
const handle = await startTestServer({ dev: true });
|
|
166
|
+
for (const key of ["a", "b", "c"]) {
|
|
167
|
+
await handle.callTool("seed", { kind: "report", key });
|
|
168
|
+
await tick();
|
|
169
|
+
}
|
|
170
|
+
const limited = await listArtifacts(handle.url, "?limit=2");
|
|
171
|
+
expect(limited.artifacts).toHaveLength(2);
|
|
172
|
+
expect(limited.artifacts.map((a) => a.key)).toEqual(["c", "b"]);
|
|
173
|
+
|
|
174
|
+
// Above the ceiling clamps rather than erroring.
|
|
175
|
+
const clamped = await listArtifacts(handle.url, "?limit=99999");
|
|
176
|
+
expect(clamped.status).toBe(200);
|
|
177
|
+
expect(clamped.artifacts).toHaveLength(3);
|
|
178
|
+
|
|
179
|
+
expect((await listArtifacts(handle.url, "?limit=0")).status).toBe(400);
|
|
180
|
+
expect((await listArtifacts(handle.url, "?limit=nope")).status).toBe(400);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("hides session-less artifacts from non-includeAll callers", async () => {
|
|
184
|
+
// No --dev: loopback callers get the strict per-principal view, and
|
|
185
|
+
// session-less (agent-scoped) artifacts have no owning principal.
|
|
186
|
+
const handle = await startTestServer();
|
|
187
|
+
await handle.callTool("seed", { kind: "report", key: "agent-scoped" });
|
|
188
|
+
const listed = await listArtifacts(handle.url);
|
|
189
|
+
expect(listed.status).toBe(200);
|
|
190
|
+
expect(listed.artifacts).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("channel onStart gets the unbound artifacts facade", async () => {
|
|
194
|
+
const channel: DiscoveredChannel = {
|
|
195
|
+
id: "starter",
|
|
196
|
+
definition: defineChannel({
|
|
197
|
+
async onStart({ artifacts }) {
|
|
198
|
+
await artifacts.tag({
|
|
199
|
+
kind: "report",
|
|
200
|
+
key: "from-onstart",
|
|
201
|
+
data: 1,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
const handle = await startTestServer(
|
|
207
|
+
{ dev: true },
|
|
208
|
+
{ channels: [channel] }
|
|
209
|
+
);
|
|
210
|
+
const listed = await listArtifacts(handle.url);
|
|
211
|
+
expect(listed.artifacts.map((a) => a.key)).toEqual(["from-onstart"]);
|
|
212
|
+
expect(listed.artifacts[0]?.sessionId).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("requires the bearer token when one is configured", async () => {
|
|
216
|
+
const handle = await startTestServer({ authToken: "sekrit" });
|
|
217
|
+
const denied = await fetch(`${handle.url}/v1/artifacts`);
|
|
218
|
+
expect(denied.status).toBe(401);
|
|
219
|
+
const allowed = await fetch(`${handle.url}/v1/artifacts`, {
|
|
220
|
+
headers: { authorization: "Bearer sekrit" },
|
|
221
|
+
});
|
|
222
|
+
expect(allowed.status).toBe(200);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("never widens to includeAll when a bearer token is configured", async () => {
|
|
226
|
+
// Bearer auth vetoes relaxSessionReadAuth even under --dev on loopback,
|
|
227
|
+
// matching GET /v1/sessions.
|
|
228
|
+
const handle = await startTestServer({ dev: true, authToken: "sekrit" });
|
|
229
|
+
await handle.callTool("seed", { kind: "report", key: "agent-scoped" });
|
|
230
|
+
const listed = await listArtifacts(handle.url, "", {
|
|
231
|
+
authorization: "Bearer sekrit",
|
|
232
|
+
});
|
|
233
|
+
expect(listed.status).toBe(200);
|
|
234
|
+
expect(listed.artifacts).toEqual([]);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("SessionEngine.listArtifacts ownership", () => {
|
|
239
|
+
const alice: AuthContext = {
|
|
240
|
+
authenticator: "test",
|
|
241
|
+
principalId: "alice",
|
|
242
|
+
principalType: "user",
|
|
243
|
+
};
|
|
244
|
+
const bob: AuthContext = {
|
|
245
|
+
authenticator: "test",
|
|
246
|
+
principalId: "bob",
|
|
247
|
+
principalType: "user",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
async function makeEngine(): Promise<SessionEngine> {
|
|
251
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-artown-"));
|
|
252
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
253
|
+
const engine = new SessionEngine({
|
|
254
|
+
project: testProject(stateRoot),
|
|
255
|
+
stateRoot,
|
|
256
|
+
runner: noTurnRunner,
|
|
257
|
+
});
|
|
258
|
+
cleanups.push(() => engine.close());
|
|
259
|
+
return engine;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function seedSession(
|
|
263
|
+
engine: SessionEngine,
|
|
264
|
+
sessionId: string,
|
|
265
|
+
auth: AuthContext | null
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
await engine.sessions.save({
|
|
268
|
+
sessionId,
|
|
269
|
+
channelId: "http",
|
|
270
|
+
mode: "chat",
|
|
271
|
+
continuationKey: null,
|
|
272
|
+
auth,
|
|
273
|
+
turnCount: 0,
|
|
274
|
+
createdAt: new Date().toISOString(),
|
|
275
|
+
updatedAt: new Date().toISOString(),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
it("scopes non-includeAll listings to the caller's sessions", async () => {
|
|
280
|
+
const engine = await makeEngine();
|
|
281
|
+
await seedSession(engine, "ses_alice", alice);
|
|
282
|
+
await seedSession(engine, "ses_bob", bob);
|
|
283
|
+
await engine.artifacts.tag({
|
|
284
|
+
kind: "report",
|
|
285
|
+
key: "alice-report",
|
|
286
|
+
data: 1,
|
|
287
|
+
sessionId: "ses_alice",
|
|
288
|
+
});
|
|
289
|
+
await engine.artifacts.tag({
|
|
290
|
+
kind: "report",
|
|
291
|
+
key: "bob-report",
|
|
292
|
+
data: 1,
|
|
293
|
+
sessionId: "ses_bob",
|
|
294
|
+
});
|
|
295
|
+
await engine.artifacts.tag({ kind: "report", key: "loose", data: 1 });
|
|
296
|
+
|
|
297
|
+
const forAlice = await engine.listArtifacts(alice);
|
|
298
|
+
expect(forAlice.map((a) => a.key)).toEqual(["alice-report"]);
|
|
299
|
+
|
|
300
|
+
const forBob = await engine.listArtifacts(bob);
|
|
301
|
+
expect(forBob.map((a) => a.key)).toEqual(["bob-report"]);
|
|
302
|
+
|
|
303
|
+
const anonymous = await engine.listArtifacts(null);
|
|
304
|
+
expect(anonymous).toEqual([]);
|
|
305
|
+
|
|
306
|
+
const everything = await engine.listArtifacts(null, { includeAll: true });
|
|
307
|
+
expect(everything.map((a) => a.key).sort()).toEqual([
|
|
308
|
+
"alice-report",
|
|
309
|
+
"bob-report",
|
|
310
|
+
"loose",
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("applies the filter before ownership scoping", async () => {
|
|
315
|
+
const engine = await makeEngine();
|
|
316
|
+
await seedSession(engine, "ses_alice", alice);
|
|
317
|
+
await engine.artifacts.tag({
|
|
318
|
+
kind: "reviewed-pr",
|
|
319
|
+
key: "pr",
|
|
320
|
+
data: 1,
|
|
321
|
+
sessionId: "ses_alice",
|
|
322
|
+
});
|
|
323
|
+
await engine.artifacts.tag({
|
|
324
|
+
kind: "report",
|
|
325
|
+
key: "rep",
|
|
326
|
+
data: 1,
|
|
327
|
+
sessionId: "ses_alice",
|
|
328
|
+
});
|
|
329
|
+
const filtered = await engine.listArtifacts(alice, {
|
|
330
|
+
filter: { kind: "reviewed-pr" },
|
|
331
|
+
});
|
|
332
|
+
expect(filtered.map((a) => a.key)).toEqual(["pr"]);
|
|
333
|
+
});
|
|
334
|
+
});
|
package/src/internal/server.ts
CHANGED
|
@@ -139,6 +139,10 @@ import { sessionUrlsForRequest } from "./session-urls.js";
|
|
|
139
139
|
|
|
140
140
|
const MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
141
141
|
|
|
142
|
+
/** `GET /v1/artifacts` limit defaults / ceiling. */
|
|
143
|
+
const ARTIFACTS_LIST_DEFAULT_LIMIT = 200;
|
|
144
|
+
const ARTIFACTS_LIST_MAX_LIMIT = 1000;
|
|
145
|
+
|
|
142
146
|
/** One agent to serve: its slug (empty for single-agent mode) and project. */
|
|
143
147
|
export interface AgentMount {
|
|
144
148
|
slug: string;
|
|
@@ -490,6 +494,7 @@ export async function startServer(
|
|
|
490
494
|
interruptContinuation: (continuationToken) =>
|
|
491
495
|
engine.interruptContinuation(channel.id, continuationToken),
|
|
492
496
|
logger,
|
|
497
|
+
artifacts: engine.artifacts,
|
|
493
498
|
};
|
|
494
499
|
try {
|
|
495
500
|
await definition.onStart(startArgs);
|
|
@@ -999,6 +1004,44 @@ function mountAgentRoutes(
|
|
|
999
1004
|
},
|
|
1000
1005
|
});
|
|
1001
1006
|
|
|
1007
|
+
// Durable artifacts (ctx.artifacts / tag_artifact), newest-updated first.
|
|
1008
|
+
router.add({
|
|
1009
|
+
method: "GET",
|
|
1010
|
+
pattern: `${base}/v1/artifacts`,
|
|
1011
|
+
auth: httpAuth,
|
|
1012
|
+
handler: async (request, ctx) => {
|
|
1013
|
+
const url = new URL(request.url);
|
|
1014
|
+
const limitRaw = url.searchParams.get("limit");
|
|
1015
|
+
let limit = ARTIFACTS_LIST_DEFAULT_LIMIT;
|
|
1016
|
+
if (limitRaw !== null && limitRaw !== "") {
|
|
1017
|
+
const parsed = Number.parseInt(limitRaw, 10);
|
|
1018
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1019
|
+
return errorResponse(
|
|
1020
|
+
400,
|
|
1021
|
+
"invalid_request",
|
|
1022
|
+
"`limit` must be a positive integer."
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
limit = Math.min(parsed, ARTIFACTS_LIST_MAX_LIMIT);
|
|
1026
|
+
}
|
|
1027
|
+
const kind = url.searchParams.get("kind") ?? undefined;
|
|
1028
|
+
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
1029
|
+
// Same includeAll gate as /v1/sessions: never with bearer or custom
|
|
1030
|
+
// channel auth (strict per-principal isolation); otherwise session
|
|
1031
|
+
// ownership applies (see SessionEngine.listArtifacts).
|
|
1032
|
+
const includeAll =
|
|
1033
|
+
relaxSessionReadAuth &&
|
|
1034
|
+
(relaxSessionReadAuthBeyondLoopback ||
|
|
1035
|
+
(typeof ctx.requestIp === "string" &&
|
|
1036
|
+
isLoopbackAddress(ctx.requestIp)));
|
|
1037
|
+
const artifacts = await engine.listArtifacts(ctx.auth, {
|
|
1038
|
+
includeAll,
|
|
1039
|
+
filter: { kind, sessionId },
|
|
1040
|
+
});
|
|
1041
|
+
return jsonResponse({ artifacts: artifacts.slice(0, limit) });
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1002
1045
|
// The agent's MCP surface: peers and external MCP clients delegate to
|
|
1003
1046
|
// this agent (ask / check / call_tool) over streamable HTTP. Stateless —
|
|
1004
1047
|
// POST only; GET/DELETE get a spec-compliant 405 from the endpoint.
|
|
@@ -1736,6 +1779,7 @@ async function startCursorSlackRelay(input: {
|
|
|
1736
1779
|
envelope,
|
|
1737
1780
|
send: args.send,
|
|
1738
1781
|
host: args.host,
|
|
1782
|
+
artifacts: args.artifacts,
|
|
1739
1783
|
hasContinuationSession: (continuationToken) =>
|
|
1740
1784
|
target.runtime.engine.hasContinuationSession(
|
|
1741
1785
|
target.channelId,
|
|
@@ -1803,6 +1847,7 @@ function buildChannelHandlerArgs(input: {
|
|
|
1803
1847
|
requestIp: input.requestIp ?? null,
|
|
1804
1848
|
auth,
|
|
1805
1849
|
host: runtime.engine.host,
|
|
1850
|
+
artifacts: runtime.engine.artifacts,
|
|
1806
1851
|
send: (message, sendOptions = {}) =>
|
|
1807
1852
|
runtime.engine.send(channelId, message, {
|
|
1808
1853
|
...sendOptions,
|
|
@@ -0,0 +1,293 @@
|
|
|
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, vi } from "vitest";
|
|
5
|
+
import { defineArtifacts } from "../artifacts.js";
|
|
6
|
+
import { defineChannel, httpChannel } from "../channels.js";
|
|
7
|
+
import { defineHook } from "../hooks.js";
|
|
8
|
+
import { defineSchedule } from "../schedules.js";
|
|
9
|
+
import { defineTool } from "../tools.js";
|
|
10
|
+
import type {
|
|
11
|
+
AgentProject,
|
|
12
|
+
ArtifactsDefinition,
|
|
13
|
+
DiscoveredChannel,
|
|
14
|
+
DiscoveredHook,
|
|
15
|
+
DiscoveredSchedule,
|
|
16
|
+
DiscoveredTool,
|
|
17
|
+
JsonObject,
|
|
18
|
+
ToolContext,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
import { ScheduleRunner } from "./schedule-runner.js";
|
|
21
|
+
import type { AgentRunner } from "./sdk-runner.js";
|
|
22
|
+
import { SessionEngine } from "./session-engine.js";
|
|
23
|
+
|
|
24
|
+
const AGENT = "artifacts-engine-test";
|
|
25
|
+
|
|
26
|
+
function tagTool(): DiscoveredTool {
|
|
27
|
+
const definition = defineTool({
|
|
28
|
+
description: "Tags the reviewed PR as an artifact.",
|
|
29
|
+
async execute(_input: JsonObject, ctx: ToolContext) {
|
|
30
|
+
const record = await ctx.artifacts.tag({
|
|
31
|
+
kind: "artifact",
|
|
32
|
+
key: "pr-1",
|
|
33
|
+
title: "Reviewed PR",
|
|
34
|
+
data: { url: "https://github.com/org/repo/pull/1" },
|
|
35
|
+
});
|
|
36
|
+
return { id: record.id };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
name: "tag_result",
|
|
41
|
+
description: definition.description,
|
|
42
|
+
execution: "server",
|
|
43
|
+
needsApproval: false,
|
|
44
|
+
inputSchema: { type: "object", additionalProperties: false },
|
|
45
|
+
definition,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function testProject(options?: {
|
|
50
|
+
artifacts?: ArtifactsDefinition;
|
|
51
|
+
tools?: DiscoveredTool[];
|
|
52
|
+
hooks?: DiscoveredHook[];
|
|
53
|
+
schedules?: DiscoveredSchedule[];
|
|
54
|
+
channels?: DiscoveredChannel[];
|
|
55
|
+
}): AgentProject {
|
|
56
|
+
return {
|
|
57
|
+
rootDir: "/tmp/artifacts-fixture",
|
|
58
|
+
agentDir: "/tmp/artifacts-fixture/agent",
|
|
59
|
+
name: AGENT,
|
|
60
|
+
agent: {
|
|
61
|
+
name: AGENT,
|
|
62
|
+
runtime: "local",
|
|
63
|
+
instructions: "Test agent.",
|
|
64
|
+
tools: options?.tools ?? [],
|
|
65
|
+
skills: [],
|
|
66
|
+
connections: [],
|
|
67
|
+
subagents: [],
|
|
68
|
+
seedFiles: [],
|
|
69
|
+
},
|
|
70
|
+
httpChannel: httpChannel(),
|
|
71
|
+
channels: options?.channels ?? [],
|
|
72
|
+
schedules: options?.schedules ?? [],
|
|
73
|
+
hooks: options?.hooks ?? [],
|
|
74
|
+
abs: [],
|
|
75
|
+
artifacts: options?.artifacts,
|
|
76
|
+
diagnostics: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function echoRunner(): AgentRunner {
|
|
81
|
+
return {
|
|
82
|
+
runTurn: async (request) => ({
|
|
83
|
+
status: "finished",
|
|
84
|
+
result: `echo: ${request.prompt}`,
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Runner that invokes the session-bound `tag_result` custom tool. */
|
|
90
|
+
function toolCallingRunner(): AgentRunner {
|
|
91
|
+
return {
|
|
92
|
+
runTurn: async (request) => {
|
|
93
|
+
const tool = request.customTools.tag_result;
|
|
94
|
+
if (tool === undefined) {
|
|
95
|
+
throw new Error("tag_result not registered");
|
|
96
|
+
}
|
|
97
|
+
await tool.execute({}, { toolCallId: "call_tag_1" });
|
|
98
|
+
return { status: "finished", result: "done" };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cleanups: Array<() => Promise<void>> = [];
|
|
104
|
+
|
|
105
|
+
afterEach(async () => {
|
|
106
|
+
while (cleanups.length > 0) {
|
|
107
|
+
await cleanups.pop()?.();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
async function makeEngine(
|
|
112
|
+
project: AgentProject,
|
|
113
|
+
runner: AgentRunner
|
|
114
|
+
): Promise<SessionEngine> {
|
|
115
|
+
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-artifacts-eng-"));
|
|
116
|
+
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
117
|
+
const engine = new SessionEngine({ project, stateRoot, runner });
|
|
118
|
+
cleanups.push(() => engine.close());
|
|
119
|
+
return engine;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("session engine artifacts wiring", () => {
|
|
123
|
+
it("tool ctx.artifacts auto-fills the session and appends artifact.tagged", async () => {
|
|
124
|
+
const engine = await makeEngine(
|
|
125
|
+
testProject({ tools: [tagTool()] }),
|
|
126
|
+
toolCallingRunner()
|
|
127
|
+
);
|
|
128
|
+
const session = await engine.send("http", "review the PR", { auth: null });
|
|
129
|
+
await session.waitForCompletion();
|
|
130
|
+
|
|
131
|
+
const rows = await engine.artifacts.list({ sessionId: session.id });
|
|
132
|
+
expect(rows).toHaveLength(1);
|
|
133
|
+
expect(rows[0]).toMatchObject({
|
|
134
|
+
kind: "artifact",
|
|
135
|
+
key: "pr-1",
|
|
136
|
+
sessionId: session.id,
|
|
137
|
+
turnId: "turn_1",
|
|
138
|
+
source: "host",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const events = await engine.listEvents(session.id);
|
|
142
|
+
const tagged = events.filter((event) => event.type === "artifact.tagged");
|
|
143
|
+
expect(tagged).toHaveLength(1);
|
|
144
|
+
expect(tagged[0]).toMatchObject({
|
|
145
|
+
sessionId: session.id,
|
|
146
|
+
turnId: "turn_1",
|
|
147
|
+
data: {
|
|
148
|
+
id: rows[0]?.id,
|
|
149
|
+
kind: "artifact",
|
|
150
|
+
key: "pr-1",
|
|
151
|
+
title: "Reviewed PR",
|
|
152
|
+
source: "host",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("bound facades reject a conflicting explicit sessionId", async () => {
|
|
158
|
+
const engine = await makeEngine(testProject(), echoRunner());
|
|
159
|
+
const session = await engine.send("http", "hello", { auth: null });
|
|
160
|
+
await session.waitForCompletion();
|
|
161
|
+
|
|
162
|
+
const bound = engine.artifactsApi({ sessionId: session.id });
|
|
163
|
+
await expect(
|
|
164
|
+
bound.tag({ data: 1, sessionId: "ses_other" })
|
|
165
|
+
).rejects.toThrow(/bound to session/);
|
|
166
|
+
// The matching sessionId (a no-op override) is fine.
|
|
167
|
+
await expect(
|
|
168
|
+
bound.tag({ data: 1, sessionId: session.id })
|
|
169
|
+
).resolves.toMatchObject({ sessionId: session.id });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("session-less tags via the unbound channel facade write the row only", async () => {
|
|
173
|
+
const engine = await makeEngine(testProject(), echoRunner());
|
|
174
|
+
const session = await engine.send("http", "hello", { auth: null });
|
|
175
|
+
await session.waitForCompletion();
|
|
176
|
+
const eventCount = (await engine.listEvents(session.id)).length;
|
|
177
|
+
|
|
178
|
+
// ChannelHandlerArgs.artifacts is this same unbound facade.
|
|
179
|
+
const record = await engine.artifacts.tag({ data: { note: "no session" } });
|
|
180
|
+
expect(record.sessionId).toBeUndefined();
|
|
181
|
+
expect(await engine.artifacts.list()).toHaveLength(1);
|
|
182
|
+
expect(await engine.listEvents(session.id)).toHaveLength(eventCount);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("unbound tags with an explicit sessionId stream to that session", async () => {
|
|
186
|
+
const engine = await makeEngine(testProject(), echoRunner());
|
|
187
|
+
const session = await engine.send("http", "hello", { auth: null });
|
|
188
|
+
await session.waitForCompletion();
|
|
189
|
+
|
|
190
|
+
const record = await engine.artifacts.tag({
|
|
191
|
+
data: "attributed",
|
|
192
|
+
sessionId: session.id,
|
|
193
|
+
});
|
|
194
|
+
expect(record.sessionId).toBe(session.id);
|
|
195
|
+
const events = await engine.listEvents(session.id);
|
|
196
|
+
expect(events.some((event) => event.type === "artifact.tagged")).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("tags for an unknown sessionId reject before writing the row", async () => {
|
|
200
|
+
const engine = await makeEngine(testProject(), echoRunner());
|
|
201
|
+
await expect(
|
|
202
|
+
engine.artifacts.tag({ data: 1, sessionId: "ses_nope" })
|
|
203
|
+
).rejects.toThrow(/Unknown session/);
|
|
204
|
+
expect(await engine.artifacts.list()).toHaveLength(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("schedule handlers get the unbound artifacts facade", async () => {
|
|
208
|
+
const cron = "* * * * *";
|
|
209
|
+
const schedule: DiscoveredSchedule = {
|
|
210
|
+
name: "tagger",
|
|
211
|
+
cron,
|
|
212
|
+
kind: "run",
|
|
213
|
+
definition: defineSchedule({
|
|
214
|
+
cron,
|
|
215
|
+
async run({ artifacts }) {
|
|
216
|
+
await artifacts.tag({ key: "from-schedule", data: "s" });
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
const project = testProject({ schedules: [schedule] });
|
|
221
|
+
const engine = await makeEngine(project, echoRunner());
|
|
222
|
+
const runner = new ScheduleRunner(engine, project);
|
|
223
|
+
await runner.dispatch("tagger");
|
|
224
|
+
|
|
225
|
+
const rows = await engine.artifacts.list();
|
|
226
|
+
expect(rows.map((row) => row.key)).toEqual(["from-schedule"]);
|
|
227
|
+
expect(rows[0]?.sessionId).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("channel receive hooks get the unbound artifacts facade", async () => {
|
|
231
|
+
const channel: DiscoveredChannel = {
|
|
232
|
+
id: "handoff",
|
|
233
|
+
definition: defineChannel({
|
|
234
|
+
async receive(input, { send, artifacts }) {
|
|
235
|
+
await artifacts.tag({ key: "from-receive", data: "r" });
|
|
236
|
+
return send(input.message, { auth: input.auth });
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
const engine = await makeEngine(
|
|
241
|
+
testProject({ channels: [channel] }),
|
|
242
|
+
echoRunner()
|
|
243
|
+
);
|
|
244
|
+
await engine.receive("handoff", { message: "hand-off", auth: null });
|
|
245
|
+
|
|
246
|
+
const rows = await engine.artifacts.list();
|
|
247
|
+
expect(rows.map((row) => row.key)).toEqual(["from-receive"]);
|
|
248
|
+
expect(rows[0]?.sessionId).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("hook ctx.artifacts is bound to the hook's session", async () => {
|
|
252
|
+
const hook: DiscoveredHook = {
|
|
253
|
+
name: "tagger",
|
|
254
|
+
definition: defineHook({
|
|
255
|
+
events: {
|
|
256
|
+
async "turn.completed"(_event, ctx) {
|
|
257
|
+
await ctx.artifacts.tag({ key: "from-hook", data: "h" });
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
const engine = await makeEngine(
|
|
263
|
+
testProject({ hooks: [hook] }),
|
|
264
|
+
echoRunner()
|
|
265
|
+
);
|
|
266
|
+
const session = await engine.send("http", "hello", { auth: null });
|
|
267
|
+
await session.waitForCompletion();
|
|
268
|
+
|
|
269
|
+
await vi.waitFor(async () => {
|
|
270
|
+
const rows = await engine.artifacts.list({ sessionId: session.id });
|
|
271
|
+
expect(rows.map((row) => row.key)).toEqual(["from-hook"]);
|
|
272
|
+
});
|
|
273
|
+
const events = await engine.listEvents(session.id);
|
|
274
|
+
expect(events.some((event) => event.type === "artifact.tagged")).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("enforces declared kinds end to end", async () => {
|
|
278
|
+
const engine = await makeEngine(
|
|
279
|
+
testProject({
|
|
280
|
+
artifacts: defineArtifacts({
|
|
281
|
+
kinds: { note: { description: "Note." } },
|
|
282
|
+
}),
|
|
283
|
+
}),
|
|
284
|
+
echoRunner()
|
|
285
|
+
);
|
|
286
|
+
await expect(
|
|
287
|
+
engine.artifacts.tag({ kind: "bogus", data: 1 })
|
|
288
|
+
).rejects.toThrow(/unknown kind/);
|
|
289
|
+
await expect(
|
|
290
|
+
engine.artifacts.tag({ kind: "note", data: 1 })
|
|
291
|
+
).resolves.toMatchObject({ kind: "note" });
|
|
292
|
+
});
|
|
293
|
+
});
|