404 lines
16 KiB
TypeScript
404 lines
16 KiB
TypeScript
import { Effect } from "effect"
|
|
import { mkdir, rm } from "fs/promises"
|
|
import path from "path"
|
|
import { array, check, object } from "../../server/httpapi-exercise/assertions"
|
|
import { http, route } from "../../server/httpapi-exercise/dsl"
|
|
import type { Scenario, ScenarioContext } from "../../server/httpapi-exercise/types"
|
|
|
|
function directory(ctx: ScenarioContext) {
|
|
if (!ctx.directory) throw new Error("scenario needs a project directory")
|
|
return ctx.directory
|
|
}
|
|
|
|
function file(ctx: ScenarioContext, name: string, content: string) {
|
|
const target = path.join(directory(ctx), name)
|
|
return Effect.promise(async () => {
|
|
await mkdir(path.dirname(target), { recursive: true })
|
|
await Bun.write(target, content)
|
|
return target
|
|
})
|
|
}
|
|
|
|
const edit = {
|
|
provider: "kilo",
|
|
model: "inception/mercury-next-edit",
|
|
currentFilePath: "src/index.ts",
|
|
currentFileContent: "export const value = 1\n",
|
|
cursorLine: 0,
|
|
cursorCharacter: 0,
|
|
editableRegionStartLine: 0,
|
|
editableRegionEndLine: 0,
|
|
recentlyViewedSnippets: [],
|
|
editDiffHistory: [],
|
|
}
|
|
|
|
export const kiloScenarios: Scenario[] = [
|
|
http.protected.get("/background-process", "backgroundProcess.list").json(200, array),
|
|
http.protected
|
|
.get("/background-process/{processID}", "backgroundProcess.get")
|
|
.at((ctx) => ({
|
|
path: route("/background-process/{processID}", { processID: "bgp_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.get("/background-process/{processID}/logs", "backgroundProcess.logs")
|
|
.at((ctx) => ({
|
|
path: route("/background-process/{processID}/logs", { processID: "bgp_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/background-process/{processID}/stop", "backgroundProcess.stop")
|
|
.at((ctx) => ({
|
|
path: route("/background-process/{processID}/stop", { processID: "bgp_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/background-process/{processID}/restart", "backgroundProcess.restart")
|
|
.at((ctx) => ({
|
|
path: route("/background-process/{processID}/restart", { processID: "bgp_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/background-process/session/{sessionID}/stop", "backgroundProcess.stopSession")
|
|
.at((ctx) => ({
|
|
path: route("/background-process/session/{sessionID}/stop", { sessionID: "ses_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.json(200, (body) => check(body === true, "session process stop should return true")),
|
|
http.protected.get("/config/warnings", "config.warnings").json(200, array),
|
|
http.protected.get("/config/effective", "config.effective").json(200, object),
|
|
http.protected.get("/config/model-state", "config.modelState").json(200, object),
|
|
http.protected
|
|
.patch("/config/model-state", "config.modelStateUpdate")
|
|
.at((ctx) => ({ path: "/config/model-state", headers: ctx.headers(), body: { favorite: [] } }))
|
|
.json(200, object),
|
|
http.protected.get("/config/overlay", "config.overlay").json(200, object),
|
|
http.protected
|
|
.patch("/config/overlay", "config.overlayUpdate")
|
|
.mutating()
|
|
.at((ctx) => ({ path: "/config/overlay", headers: ctx.headers(), body: { scope: "project", set: {} } }))
|
|
.json(200, object),
|
|
http.protected.get("/config/rules", "config.rules").json(200, object),
|
|
http.protected
|
|
.put("/config/rules", "config.rulesUpdate")
|
|
.mutating()
|
|
.at((ctx) => ({ path: "/config/rules", headers: ctx.headers(), body: { content: "Use small changes." } }))
|
|
.json(200, object),
|
|
http.protected.get("/config/sources", "config.sources").json(200, object),
|
|
http.protected.get("/tui/config", "tui.config.get").json(200, object),
|
|
http.protected.get("/tui/keybinds", "tui.keybind.list").json(200, object),
|
|
http.protected
|
|
.patch("/tui/config", "tui.config.update")
|
|
.mutating()
|
|
.at((ctx) => ({ path: "/tui/config?scope=project", headers: ctx.headers(), body: { theme: "nord" } }))
|
|
.json(200, object),
|
|
http.protected
|
|
.post("/agent-builder/preview", "agent.builder.preview")
|
|
.at((ctx) => ({
|
|
path: "/agent-builder/preview",
|
|
headers: ctx.headers(),
|
|
body: { id: "httpapi-agent", scope: "project", mode: "subagent", prompt: "Review changes." },
|
|
}))
|
|
.json(200, object),
|
|
http.protected
|
|
.put("/agent-builder/{id}", "agent.builder.save")
|
|
.mutating()
|
|
.at((ctx) => ({
|
|
path: route("/agent-builder/{id}", { id: "httpapi-agent" }),
|
|
headers: ctx.headers(),
|
|
body: { id: "httpapi-agent", scope: "project", mode: "subagent", prompt: "Review changes." },
|
|
}))
|
|
.json(200, object),
|
|
http.protected
|
|
.get("/experimental/worktree/diff", "worktree.diff")
|
|
.at((ctx) => ({ path: "/experimental/worktree/diff?base=HEAD", headers: ctx.headers() }))
|
|
.json(200, array),
|
|
http.protected
|
|
.get("/experimental/worktree/diff/summary", "worktree.diffSummary")
|
|
.at((ctx) => ({ path: "/experimental/worktree/diff/summary?base=HEAD", headers: ctx.headers() }))
|
|
.json(200, array),
|
|
http.protected
|
|
.get("/experimental/worktree/diff/file", "worktree.diffFile")
|
|
.at((ctx) => ({
|
|
path: `/experimental/worktree/diff/file?${new URLSearchParams({ base: "HEAD", file: "missing.txt" })}`,
|
|
headers: ctx.headers(),
|
|
}))
|
|
.json(200, (body) => check(body === null, "missing worktree diff detail should return null")),
|
|
http.protected.get("/indexing/status", "indexing.status").json(200, object),
|
|
http.protected.get("/indexing/models", "indexing.models").json(200, object),
|
|
http.protected.get("/indexing/warnings", "indexing.warnings").json(200, array),
|
|
http.protected.get("/kilo/profile", "kilo.profile").probe({ path: "/path" }).status(401),
|
|
http.protected.get("/kilo/modes", "kilo.modes").json(200, (body) => {
|
|
object(body)
|
|
array(body.modes)
|
|
}),
|
|
http.protected
|
|
.post("/kilo/fim", "kilo.fim")
|
|
.at((ctx) => ({ path: "/kilo/fim", headers: ctx.headers(), body: { prefix: "const value = ", suffix: "\n" } }))
|
|
.status(401),
|
|
http.protected
|
|
.post("/kilo/edit", "kilo.edit")
|
|
.at((ctx) => ({ path: "/kilo/edit", headers: ctx.headers(), body: edit }))
|
|
.status(401),
|
|
http.protected
|
|
.post("/kilo/audio/transcriptions", "kilo.audio.transcriptions")
|
|
.at((ctx) => ({
|
|
path: "/kilo/audio/transcriptions",
|
|
headers: ctx.headers(),
|
|
body: { model: "whisper-1", input_audio: { data: "", format: "wav" } },
|
|
}))
|
|
.status(401),
|
|
http.protected.get("/kilo/notifications", "kilo.notifications").json(200, array),
|
|
http.protected
|
|
.post("/kilo/organization", "kilo.organization.set")
|
|
.at((ctx) => ({ path: "/kilo/organization", headers: ctx.headers(), body: { organizationId: null } }))
|
|
.status(401),
|
|
http.protected.get("/kilo/claw/status", "kilo.claw.status").probe({ path: "/path" }).status(401),
|
|
http.protected.get("/kilo/claw/chat-credentials", "kilo.claw.chatCredentials").probe({ path: "/path" }).status(401),
|
|
http.protected.get("/kilo/cloud-sessions", "kilo.cloudSessions").probe({ path: "/path" }).status(401),
|
|
http.protected
|
|
.get("/kilo/cloud/session/{id}", "kilo.cloud.session.get")
|
|
.probe({ path: "/path" })
|
|
.at((ctx) => ({ path: route("/kilo/cloud/session/{id}", { id: "httpapi-missing" }), headers: ctx.headers() }))
|
|
.status(401),
|
|
http.protected
|
|
.post("/kilo/cloud/session/import", "kilo.cloud.session.import")
|
|
.at((ctx) => ({ path: "/kilo/cloud/session/import", headers: ctx.headers(), body: { sessionId: "missing" } }))
|
|
.status(401),
|
|
http.protected.get("/network", "network.list").json(200, array),
|
|
http.protected
|
|
.post("/network/{requestID}/reply", "network.reply")
|
|
.at((ctx) => ({
|
|
path: route("/network/{requestID}/reply", { requestID: "que_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.json(200, (body) => check(body === true, "missing network reply should remain a no-op success")),
|
|
http.protected
|
|
.post("/network/{requestID}/reject", "network.reject")
|
|
.at((ctx) => ({
|
|
path: route("/network/{requestID}/reject", { requestID: "que_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.json(200, (body) => check(body === true, "missing network reject should remain a no-op success")),
|
|
http.protected.get("/remote/status", "remote.status").json(200, (body) => {
|
|
object(body)
|
|
check(body.enabled === false && body.connected === false, "remote should start disabled")
|
|
}),
|
|
http.protected.post("/remote/disable", "remote.disable").json(200, (body) => {
|
|
object(body)
|
|
check(body.enabled === false && body.connected === false, "remote disable should report disconnected state")
|
|
}),
|
|
http.protected
|
|
.post("/remote/enable", "remote.enable")
|
|
.probe({ path: "/path" })
|
|
.json(200, (body) => {
|
|
object(body)
|
|
check(body.enabled === false && body.connected === false, "disabled ingest should keep remote disconnected")
|
|
}),
|
|
http.protected.get("/suggestion", "suggestion.list").json(200, array),
|
|
http.protected
|
|
.post("/suggestion/{requestID}/accept", "suggestion.accept")
|
|
.at((ctx) => ({
|
|
path: route("/suggestion/{requestID}/accept", { requestID: "sug_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
body: { index: 0 },
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/suggestion/{requestID}/dismiss", "suggestion.dismiss")
|
|
.at((ctx) => ({
|
|
path: route("/suggestion/{requestID}/dismiss", { requestID: "sug_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/commit-message", "commitMessage.generate")
|
|
.at((ctx) => ({ path: "/commit-message", headers: ctx.headers(), body: {} }))
|
|
.status(400),
|
|
http.protected
|
|
.post("/enhance-prompt", "enhancePrompt.enhance")
|
|
.at((ctx) => ({ path: "/enhance-prompt", headers: ctx.headers(), body: { text: "" } }))
|
|
.status(400),
|
|
http.protected
|
|
.post("/kilocode/heap/snapshot", "kilocode.heap.snapshot")
|
|
.mutating()
|
|
.jsonEffect(200, (body) =>
|
|
Effect.gen(function* () {
|
|
check(typeof body === "string", "heap snapshot should return its file path")
|
|
yield* Effect.promise(() => rm(body, { force: true }))
|
|
}),
|
|
),
|
|
http.protected
|
|
.post("/kilocode/skill/remove", "kilocode.removeSkill")
|
|
.mutating()
|
|
.preserveDatabase()
|
|
.seeded((ctx) =>
|
|
Effect.gen(function* () {
|
|
const location = yield* file(
|
|
ctx,
|
|
".opencode/skill/httpapi-remove/SKILL.md",
|
|
"---\nname: httpapi-remove\ndescription: HTTP API removal fixture.\n---\n# HTTP API remove\n",
|
|
)
|
|
const sentinel = yield* file(ctx, ".opencode/skill/httpapi-remove/KEEP.txt", "synthetic sentinel\n")
|
|
return { location, sentinel }
|
|
}),
|
|
)
|
|
.at((ctx) => ({
|
|
path: "/kilocode/skill/remove",
|
|
headers: ctx.headers(),
|
|
body: { location: ctx.state.location },
|
|
}))
|
|
.jsonEffect(200, (body, ctx) =>
|
|
Effect.gen(function* () {
|
|
check(body === true, "skill removal should return true")
|
|
check(
|
|
!(yield* Effect.promise(() => Bun.file(ctx.state.location).exists())),
|
|
"removed skill should not remain on disk",
|
|
)
|
|
check(
|
|
yield* Effect.promise(() => Bun.file(ctx.state.sentinel).exists()),
|
|
"skill removal should preserve sibling files",
|
|
)
|
|
}),
|
|
),
|
|
http.protected
|
|
.post("/kilocode/agent/remove", "kilocode.removeAgent")
|
|
.mutating()
|
|
.seeded((ctx) =>
|
|
file(ctx, ".opencode/agent/httpapi-remove.md", "---\ndescription: HTTP API remove\n---\nRemove me.\n"),
|
|
)
|
|
.at((ctx) => ({ path: "/kilocode/agent/remove", headers: ctx.headers(), body: { name: "httpapi-remove" } }))
|
|
.jsonEffect(200, (body, ctx) =>
|
|
Effect.gen(function* () {
|
|
check(body === true, "agent removal should return true")
|
|
check(!(yield* Effect.promise(() => Bun.file(ctx.state).exists())), "removed agent should not remain on disk")
|
|
}),
|
|
),
|
|
http.protected
|
|
.post("/kilocode/session-import/project", "kilocode.sessionImport.project")
|
|
.mutating()
|
|
.at((ctx) => ({
|
|
path: "/kilocode/session-import/project",
|
|
headers: ctx.headers(),
|
|
body: {
|
|
id: "prj_httpapi_import",
|
|
worktree: directory(ctx),
|
|
timeCreated: 0,
|
|
timeUpdated: 0,
|
|
sandboxes: [],
|
|
},
|
|
}))
|
|
.json(200, (body) => {
|
|
object(body)
|
|
check(body.ok === true && typeof body.id === "string", "project import should return the resolved project")
|
|
}),
|
|
http.protected
|
|
.post("/kilocode/session-import/session", "kilocode.sessionImport.session")
|
|
.mutating()
|
|
.seeded((ctx) => ctx.project())
|
|
.at((ctx) => ({
|
|
path: "/kilocode/session-import/session",
|
|
headers: ctx.headers(),
|
|
body: {
|
|
id: "ses_httpapi_import",
|
|
projectID: ctx.state.id,
|
|
slug: "httpapi-import",
|
|
directory: directory(ctx),
|
|
title: "HTTP API import",
|
|
version: "httpapi",
|
|
timeCreated: 0,
|
|
timeUpdated: 0,
|
|
},
|
|
}))
|
|
.json(200, (body) => {
|
|
object(body)
|
|
check(body.ok === true && body.id === "ses_httpapi_import", "session import should return imported ID")
|
|
}),
|
|
http.protected
|
|
.post("/kilocode/session-import/message", "kilocode.sessionImport.message")
|
|
.mutating()
|
|
.seeded((ctx) => ctx.session({ title: "Import message" }))
|
|
.at((ctx) => ({
|
|
path: "/kilocode/session-import/message",
|
|
headers: ctx.headers(),
|
|
body: {
|
|
id: "msg_httpapi_import",
|
|
sessionID: ctx.state.id,
|
|
timeCreated: 0,
|
|
data: {
|
|
role: "user",
|
|
time: { created: 0 },
|
|
agent: "code",
|
|
model: { providerID: "test", modelID: "test" },
|
|
},
|
|
},
|
|
}))
|
|
.json(200, (body) => {
|
|
object(body)
|
|
check(body.ok === true && body.id === "msg_httpapi_import", "message import should return imported ID")
|
|
}),
|
|
http.protected
|
|
.post("/kilocode/session-import/part", "kilocode.sessionImport.part")
|
|
.mutating()
|
|
.seeded((ctx) =>
|
|
Effect.gen(function* () {
|
|
const session = yield* ctx.session({ title: "Import part" })
|
|
const message = yield* ctx.message(session.id)
|
|
return { session, message }
|
|
}),
|
|
)
|
|
.at((ctx) => ({
|
|
path: "/kilocode/session-import/part",
|
|
headers: ctx.headers(),
|
|
body: {
|
|
id: "prt_httpapi_import",
|
|
messageID: ctx.state.message.info.id,
|
|
sessionID: ctx.state.session.id,
|
|
timeCreated: 0,
|
|
data: { type: "text", text: "imported part" },
|
|
},
|
|
}))
|
|
.json(200, (body) => {
|
|
object(body)
|
|
check(body.ok === true && body.id === "prt_httpapi_import", "part import should return imported ID")
|
|
}),
|
|
http.protected
|
|
.post("/permission/{requestID}/always-rules", "permission.saveAlwaysRules")
|
|
.at((ctx) => ({
|
|
path: route("/permission/{requestID}/always-rules", { requestID: "per_httpapi_missing" }),
|
|
headers: ctx.headers(),
|
|
body: {},
|
|
}))
|
|
.status(404),
|
|
http.protected
|
|
.post("/permission/allow-everything", "permission.allowEverything")
|
|
.mutating()
|
|
.seeded((ctx) => ctx.session({ title: "Allow everything" }))
|
|
.at((ctx) => ({
|
|
path: "/permission/allow-everything",
|
|
headers: ctx.headers(),
|
|
body: { enable: true, sessionID: ctx.state.id },
|
|
}))
|
|
.json(200, (body) => check(body === true, "allow everything should return true")),
|
|
http.protected
|
|
.post("/session/viewed", "session.viewed")
|
|
.at((ctx) => ({ path: "/session/viewed", headers: ctx.headers(), body: { focused: [], open: [] } }))
|
|
.json(200, (body) => check(body === true, "session viewed should return true")),
|
|
http.protected
|
|
.post("/telemetry/capture", "telemetry.capture")
|
|
.at((ctx) => ({
|
|
path: "/telemetry/capture",
|
|
headers: ctx.headers(),
|
|
body: { event: "httpapi_exercise", properties: { source: "httpapi" } },
|
|
}))
|
|
.json(200, (body) => check(body === true, "telemetry capture should return true")),
|
|
http.protected
|
|
.post("/telemetry/setEnabled", "telemetry.setEnabled")
|
|
.at((ctx) => ({ path: "/telemetry/setEnabled", headers: ctx.headers(), body: { enabled: true } }))
|
|
.json(200, (body) => check(body === true, "telemetry enabled update should return true")),
|
|
]
|