Skip to content

Commit dabfffc

Browse files
committed
test(deepagent-code): cover all http api routes
1 parent 80212a2 commit dabfffc

10 files changed

Lines changed: 891 additions & 2 deletions

File tree

packages/deepagent-code/test/server/httpapi-exercise/backend.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,17 @@ function authProbePath(path: string) {
118118
}
119119

120120
async function capture(response: Response, mode: CaptureMode): Promise<CallResult> {
121-
const text = mode === "stream" ? await captureStream(response) : await response.text()
121+
const text =
122+
mode === "stream"
123+
? await captureStream(response)
124+
: mode === "headers"
125+
? response.body
126+
? await response.body.cancel().then(
127+
() => "",
128+
() => "",
129+
)
130+
: ""
131+
: await response.text()
122132
return {
123133
status: response.status,
124134
contentType: response.headers.get("content-type") ?? "",

packages/deepagent-code/test/server/httpapi-exercise/dsl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ class ScenarioBuilder<S = undefined> {
6666
return this.clone({ capture: "stream" })
6767
}
6868

69+
headersOnly() {
70+
return this.clone({ capture: "headers" })
71+
}
72+
6973
protected() {
7074
return this.auth("protected")
7175
}

packages/deepagent-code/test/server/httpapi-exercise/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ import { runScenario } from "./runner"
3737
import { disposeApps } from "./backend"
3838
import { runtime } from "./runtime"
3939
import { type Scenario } from "./types"
40+
import { platformScenarios } from "./scenarios/platform"
41+
import { fileScenarios } from "./scenarios/file"
42+
import { imScenarios } from "./scenarios/im"
43+
import { runtimeScenarios } from "./scenarios/runtime"
44+
import { deepagentScenarios } from "./scenarios/deepagent"
45+
import { oversightScenarios } from "./scenarios/oversight"
4046

4147
void (await import("@deepagent-code/core/util/log")).init({ print: false })
4248

@@ -75,6 +81,12 @@ const deepagentCandidate = (id: string) => ({
7581
})
7682

7783
const scenarios: Scenario[] = [
84+
...platformScenarios,
85+
...fileScenarios,
86+
...imScenarios,
87+
...runtimeScenarios,
88+
...deepagentScenarios,
89+
...oversightScenarios,
7890
http.protected
7991
.get("/global/health", "global.health")
8092
.global()
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { AgentGateway } from "@deepagent-code/core/agent-gateway"
2+
import { Effect } from "effect"
3+
import path from "path"
4+
import { array, check, object } from "../assertions"
5+
import { http } from "../dsl"
6+
import { exerciseDataDirectory } from "../environment"
7+
import type { Scenario } from "../types"
8+
9+
AgentGateway.configure({ enabled: true, runsDir: path.join(exerciseDataDirectory, "deepagent", "runs") })
10+
11+
export const deepagentScenarios: Scenario[] = [
12+
http.protected.get("/deepagent/knowledge/pending", "deepagent.knowledge.pending").json(200, (body) => {
13+
object(body)
14+
array(body.items)
15+
}),
16+
http.protected.get("/deepagent/knowledge/review-summary", "deepagent.knowledge.reviewSummary").json(200, (body) => {
17+
object(body)
18+
check(typeof body.pendingCount === "number", "knowledge review summary should return a count")
19+
}),
20+
http.protected
21+
.post("/deepagent/knowledge/approve", "deepagent.knowledge.approve")
22+
.mutating()
23+
.at((ctx) => ({ path: "/deepagent/knowledge/approve", headers: ctx.headers(), body: { ids: [] } }))
24+
.json(200, (body) => {
25+
object(body)
26+
check(Array.isArray(body.updated) && body.updated.length === 0, "empty knowledge approval should be idempotent")
27+
}),
28+
http.protected
29+
.post("/deepagent/knowledge/reject-ids", "deepagent.knowledge.rejectIds")
30+
.mutating()
31+
.at((ctx) => ({ path: "/deepagent/knowledge/reject-ids", headers: ctx.headers(), body: { ids: [] } }))
32+
.json(200, (body) => {
33+
object(body)
34+
check(Array.isArray(body.updated) && body.updated.length === 0, "empty knowledge rejection should be idempotent")
35+
}),
36+
http.protected
37+
.post("/deepagent/knowledge/ship-gate", "deepagent.knowledge.shipGate")
38+
.at((ctx) => ({
39+
path: "/deepagent/knowledge/ship-gate",
40+
headers: ctx.headers(),
41+
body: {
42+
tasks: ["httpapi"],
43+
metrics: [
44+
{ group: "general", task: "httpapi", metric: 1 },
45+
{ group: "high", task: "httpapi", metric: 1 },
46+
{ group: "max", task: "httpapi", metric: 1 },
47+
],
48+
candidateRefs: [],
49+
},
50+
}))
51+
.json(200, (body) => {
52+
object(body)
53+
check(body.ship === true, "equal measured metrics should pass the knowledge ship gate")
54+
array(body.offenders)
55+
object(body.per_group)
56+
}),
57+
http.protected.get("/deepagent/packs/active", "deepagent.packs.active").json(200, (body) => {
58+
object(body)
59+
array(body.packs)
60+
check(typeof body.snapshotId === "string", "active packs should carry a deterministic snapshot id")
61+
}),
62+
http.protected.get("/deepagent/packs/all", "deepagent.packs.all").json(200, (body) => {
63+
object(body)
64+
array(body.packs)
65+
}),
66+
http.protected
67+
.post("/deepagent/packs/pin", "deepagent.packs.pin")
68+
.mutating()
69+
.at((ctx) => ({ path: "/deepagent/packs/pin", headers: ctx.headers(), body: { packId: "httpapi-pack" } }))
70+
.json(200, (body) => {
71+
object(body)
72+
check(body.ok === true && body.packId === "httpapi-pack", "pack pin should persist the requested id")
73+
}),
74+
http.protected
75+
.post("/deepagent/packs/unpin", "deepagent.packs.unpin")
76+
.mutating()
77+
.at((ctx) => ({ path: "/deepagent/packs/unpin", headers: ctx.headers(), body: { packId: "httpapi-pack" } }))
78+
.json(200, (body) => {
79+
object(body)
80+
check(body.ok === true && body.packId === "httpapi-pack", "pack unpin should be idempotent")
81+
}),
82+
http.protected.get("/deepagent/env-facts", "deepagent.envFacts.list").json(200, (body) => {
83+
object(body)
84+
array(body.adopted)
85+
array(body.pending)
86+
}),
87+
http.protected
88+
.post("/deepagent/env-facts/decide", "deepagent.envFacts.decide")
89+
.mutating()
90+
.at((ctx) => ({
91+
path: "/deepagent/env-facts/decide",
92+
headers: ctx.headers(),
93+
body: { factId: "fact_httpapi", decision: "reject" },
94+
}))
95+
.json(200, (body) => {
96+
object(body)
97+
check(body.ok === true && body.factId === "fact_httpapi", "environment fact decision should be recorded")
98+
}),
99+
http.protected
100+
.post("/deepagent/env-facts/modify", "deepagent.envFacts.modify")
101+
.mutating()
102+
.at((ctx) => ({
103+
path: "/deepagent/env-facts/modify",
104+
headers: ctx.headers(),
105+
body: {
106+
factId: "fact_httpapi",
107+
description: "HTTP API environment fact",
108+
body: { host: "localhost", port: 3000, last_confirmed_at: "2026-01-01T00:00:00.000Z" },
109+
mode: "project",
110+
},
111+
}))
112+
.json(200, (body) => {
113+
object(body)
114+
check(
115+
body.ok === true && typeof body.factId === "string",
116+
"environment fact modification should return the stored id",
117+
)
118+
}),
119+
http.protected
120+
.post("/deepagent/panel/consult", "deepagent.panel.consult")
121+
.at((ctx) => ({
122+
path: "/deepagent/panel/consult",
123+
headers: ctx.headers(),
124+
body: { sessionID: "session_httpapi_panel", question: "Review the HTTP API" },
125+
}))
126+
.json(400, object, "status"),
127+
http.protected
128+
.post("/deepagent/panel/arm", "deepagent.panel.arm")
129+
.mutating()
130+
.seeded(() => Effect.sync(() => AgentGateway.DeepAgentSessionState.getOrCreate("session_httpapi_panel", "high")))
131+
.at((ctx) => ({
132+
path: "/deepagent/panel/arm",
133+
headers: ctx.headers(),
134+
body: { sessionID: "session_httpapi_panel", armed: true, rounds: "multi" },
135+
}))
136+
.json(200, (body) => {
137+
object(body)
138+
check(body.armed === true && body.rounds === "multi", "panel arm should persist the requested mode")
139+
}),
140+
http.protected
141+
.get("/deepagent/panel/status", "deepagent.panel.status")
142+
.at((ctx) => ({ path: "/deepagent/panel/status?sessionID=session_httpapi_status", headers: ctx.headers() }))
143+
.json(200, (body) => {
144+
object(body)
145+
check(body.sessionID === "session_httpapi_status", "panel status should echo the session id")
146+
check(
147+
typeof body.armed === "boolean" && typeof body.explicit === "boolean",
148+
"panel status should expose its source",
149+
)
150+
}),
151+
http.protected
152+
.post("/deepagent/goal/start", "deepagent.goal.start")
153+
.at((ctx) => ({
154+
path: "/deepagent/goal/start",
155+
headers: ctx.headers(),
156+
body: { sessionID: "session_httpapi_goal", objective: "Exercise the route" },
157+
}))
158+
.json(400, object, "status"),
159+
...["pause", "resume", "stop"].map((action) =>
160+
http.protected
161+
.post(`/deepagent/goal/${action}`, `deepagent.goal.${action}`)
162+
.at((ctx) => ({
163+
path: `/deepagent/goal/${action}`,
164+
headers: ctx.headers(),
165+
body: { sessionID: "session_httpapi_goal" },
166+
}))
167+
.json(200, (body) => {
168+
object(body)
169+
check(body.ok === false, `disabled goal ${action} should fail closed without mutation`)
170+
}),
171+
),
172+
http.protected
173+
.post("/deepagent/goal/edit-plan", "deepagent.goal.editPlan")
174+
.at((ctx) => ({
175+
path: "/deepagent/goal/edit-plan",
176+
headers: ctx.headers(),
177+
body: {
178+
sessionID: "session_httpapi_goal",
179+
plan: { goal: "Exercise the route", steps: [{ title: "Verify the response" }] },
180+
},
181+
}))
182+
.json(200, (body) => {
183+
object(body)
184+
check(body.ok === false, "disabled goal plan editing should fail closed")
185+
}),
186+
http.protected
187+
.get("/deepagent/goal/status", "deepagent.goal.status")
188+
.at((ctx) => ({ path: "/deepagent/goal/status?sessionID=session_httpapi_goal", headers: ctx.headers() }))
189+
.json(200, (body) => {
190+
object(body)
191+
check(body.goal === null, "an inactive session should have no goal")
192+
}),
193+
http.protected
194+
.get("/deepagent/goal/startable", "deepagent.goal.startable")
195+
.at((ctx) => ({ path: "/deepagent/goal/startable?sessionID=session_httpapi_goal", headers: ctx.headers() }))
196+
.json(200, (body) => {
197+
object(body)
198+
check(typeof body.startable === "boolean", "goal startability should be explicit")
199+
check(["plan", "file", "none"].includes(String(body.source)), "goal startability should identify its source")
200+
}),
201+
http.protected.get("/deepagent/wiki/pages", "deepagent.wiki.pages").json(400, object, "status"),
202+
http.protected
203+
.get("/deepagent/wiki/page", "deepagent.wiki.page")
204+
.at((ctx) => ({ path: "/deepagent/wiki/page?docId=doc_httpapi&scope=durable", headers: ctx.headers() }))
205+
.json(400, object, "status"),
206+
http.protected
207+
.get("/deepagent/wiki/search", "deepagent.wiki.search")
208+
.at((ctx) => ({ path: "/deepagent/wiki/search?text=httpapi", headers: ctx.headers() }))
209+
.json(400, object, "status"),
210+
http.protected
211+
.post("/deepagent/wiki/edit", "deepagent.wiki.edit")
212+
.at((ctx) => ({
213+
path: "/deepagent/wiki/edit",
214+
headers: ctx.headers(),
215+
body: { docId: "doc_httpapi", scope: "durable", body: "updated", editor: { id: "httpapi" } },
216+
}))
217+
.json(400, object, "status"),
218+
http.protected
219+
.get("/deepagent/wiki/execution-archive", "deepagent.wiki.executionArchive")
220+
.at((ctx) => ({
221+
path: "/deepagent/wiki/execution-archive?sessionID=session_httpapi_archive",
222+
headers: ctx.headers(),
223+
}))
224+
.json(400, object, "status"),
225+
]

0 commit comments

Comments
 (0)