mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 15:21:31 +08:00
Compare commits
1 Commits
kit/cli-fl
...
kit/httpap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98fef45553 |
@@ -0,0 +1,21 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
/**
|
||||
* 404 Not Found error matching the legacy Hono `NamedError` JSON shape:
|
||||
* `{ name: "NotFoundError", data: { message } }`.
|
||||
*
|
||||
* `httpApiStatus: 404` annotation drives the response status; the schema
|
||||
* fields drive the response body. Use this in place of
|
||||
* `HttpApiError.NotFound` (which has an empty body) anywhere SDK clients
|
||||
* may inspect `error.data.message`.
|
||||
*/
|
||||
export class OpencodeNotFound extends Schema.ErrorClass<OpencodeNotFound>("opencode/Error/NotFound")(
|
||||
{
|
||||
name: Schema.tag("NotFoundError"),
|
||||
data: Schema.Struct({ message: Schema.String }),
|
||||
},
|
||||
{
|
||||
description: "Not found",
|
||||
httpApiStatus: 404,
|
||||
},
|
||||
) {}
|
||||
@@ -12,6 +12,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Schema, SchemaGetter, Struct } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { OpencodeNotFound } from "../errors"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
@@ -123,7 +124,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("get", SessionPaths.get, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Get session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.get",
|
||||
@@ -134,7 +135,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("children", SessionPaths.children, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Array(Session.Info), "List of children"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.children",
|
||||
@@ -145,7 +146,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Array(Todo.Info), "Todo list"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.todo",
|
||||
@@ -157,6 +158,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: DiffQuery,
|
||||
success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.diff",
|
||||
@@ -168,7 +170,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.messages",
|
||||
@@ -179,7 +181,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
success: described(MessageV2.WithParts, "Message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.message",
|
||||
@@ -201,7 +203,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Boolean, "Successfully deleted session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.delete",
|
||||
@@ -213,7 +215,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: UpdatePayload,
|
||||
success: described(Session.Info, "Successfully updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.update",
|
||||
@@ -225,6 +227,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: ForkPayload,
|
||||
success: described(Session.Info, "200"),
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.fork",
|
||||
@@ -235,7 +238,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Boolean, "Aborted session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.abort",
|
||||
@@ -247,7 +250,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: InitPayload,
|
||||
success: described(Schema.Boolean, "200"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.init",
|
||||
@@ -259,7 +262,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.post("share", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Successfully shared session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.share",
|
||||
@@ -270,7 +273,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Successfully unshared session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.unshare",
|
||||
@@ -282,7 +285,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: SummarizePayload,
|
||||
success: described(Schema.Boolean, "Summarized session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.summarize",
|
||||
@@ -294,7 +297,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: PromptPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.prompt",
|
||||
@@ -306,7 +309,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: PromptPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.prompt_async",
|
||||
@@ -319,7 +322,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: CommandPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.command",
|
||||
@@ -331,7 +334,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: ShellPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.shell",
|
||||
@@ -343,7 +346,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
payload: RevertPayload,
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.revert",
|
||||
@@ -355,7 +358,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.unrevert",
|
||||
@@ -367,7 +370,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||
payload: PermissionResponsePayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.respond",
|
||||
@@ -379,7 +382,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
success: described(Schema.Boolean, "Successfully deleted message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.deleteMessage",
|
||||
@@ -391,7 +394,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
success: described(Schema.Boolean, "Successfully deleted part"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "part.delete",
|
||||
@@ -402,7 +405,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
payload: MessageV2.Part,
|
||||
success: described(MessageV2.Part, "Successfully updated part"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "part.update",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { OpencodeNotFound } from "../errors"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Cause, Effect, Option, Schema, Scope } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
@@ -38,11 +39,17 @@ import {
|
||||
UpdatePayload,
|
||||
} from "../groups/session"
|
||||
|
||||
// TODO: long-term, services like Session.Service should fail with typed errors
|
||||
// directly (e.g. Effect<SessionInfo, SessionNotFound>) and let HttpApi auto-route
|
||||
// status + body via the schema annotations. Until then, we catch the legacy
|
||||
// thrown NotFoundError at the boundary and rebrand to OpencodeNotFound — which
|
||||
// matches the Hono NamedError JSON shape SDK consumers already expect.
|
||||
const mapNotFound = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||
self.pipe(
|
||||
Effect.catchIf(NotFoundError.isInstance, () => Effect.fail(new HttpApiError.NotFound({}))),
|
||||
Effect.catchDefect((error) =>
|
||||
NotFoundError.isInstance(error) ? Effect.fail(new HttpApiError.NotFound({})) : Effect.die(error),
|
||||
NotFoundError.isInstance(error)
|
||||
? Effect.fail(new OpencodeNotFound({ data: { message: error.message } }))
|
||||
: Effect.die(error),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -87,14 +94,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
})
|
||||
|
||||
const todo = Effect.fn("SessionHttpApi.todo")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||
return yield* todoSvc.get(ctx.params.sessionID)
|
||||
return yield* mapNotFound(todoSvc.get(ctx.params.sessionID))
|
||||
})
|
||||
|
||||
const diff = Effect.fn("SessionHttpApi.diff")(function* (ctx: {
|
||||
params: { sessionID: SessionID }
|
||||
query: typeof DiffQuery.Type
|
||||
}) {
|
||||
return yield* summary.diff({ sessionID: ctx.params.sessionID, messageID: ctx.query.messageID })
|
||||
return yield* mapNotFound(summary.diff({ sessionID: ctx.params.sessionID, messageID: ctx.query.messageID }))
|
||||
})
|
||||
|
||||
const messages = Effect.fn("SessionHttpApi.messages")(function* (ctx: {
|
||||
@@ -198,11 +205,11 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof ForkPayload.Type
|
||||
}) {
|
||||
return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID })
|
||||
return yield* mapNotFound(session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }))
|
||||
})
|
||||
|
||||
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||
yield* promptSvc.cancel(ctx.params.sessionID)
|
||||
yield* mapNotFound(promptSvc.cancel(ctx.params.sessionID))
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -210,13 +217,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof InitPayload.Type
|
||||
}) {
|
||||
yield* promptSvc.command({
|
||||
sessionID: ctx.params.sessionID,
|
||||
messageID: ctx.payload.messageID,
|
||||
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
||||
command: Command.Default.INIT,
|
||||
arguments: "",
|
||||
})
|
||||
yield* mapNotFound(
|
||||
promptSvc.command({
|
||||
sessionID: ctx.params.sessionID,
|
||||
messageID: ctx.payload.messageID,
|
||||
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
||||
command: Command.Default.INIT,
|
||||
arguments: "",
|
||||
}),
|
||||
)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -234,22 +243,26 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof SummarizePayload.Type
|
||||
}) {
|
||||
yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
||||
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
||||
const defaultAgent = yield* agentSvc.defaultAgent()
|
||||
const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
||||
return yield* mapNotFound(
|
||||
Effect.gen(function* () {
|
||||
yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
||||
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
||||
const defaultAgent = yield* agentSvc.defaultAgent()
|
||||
const currentAgent = messages.findLast((m) => m.info.role === "user")?.info.agent ?? defaultAgent
|
||||
|
||||
yield* compactSvc.create({
|
||||
sessionID: ctx.params.sessionID,
|
||||
agent: currentAgent,
|
||||
model: {
|
||||
providerID: ctx.payload.providerID,
|
||||
modelID: ctx.payload.modelID,
|
||||
},
|
||||
auto: ctx.payload.auto ?? false,
|
||||
})
|
||||
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
||||
return true
|
||||
yield* compactSvc.create({
|
||||
sessionID: ctx.params.sessionID,
|
||||
agent: currentAgent,
|
||||
model: {
|
||||
providerID: ctx.payload.providerID,
|
||||
modelID: ctx.payload.modelID,
|
||||
},
|
||||
auto: ctx.payload.auto ?? false,
|
||||
})
|
||||
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
||||
return true
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const prompt = Effect.fn("SessionHttpApi.prompt")(function* (ctx: {
|
||||
@@ -297,25 +310,25 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof CommandPayload.Type
|
||||
}) {
|
||||
return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID })
|
||||
return yield* mapNotFound(promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID }))
|
||||
})
|
||||
|
||||
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof ShellPayload.Type
|
||||
}) {
|
||||
return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID })
|
||||
return yield* mapNotFound(promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID }))
|
||||
})
|
||||
|
||||
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
||||
params: { sessionID: SessionID }
|
||||
payload: typeof RevertPayload.Type
|
||||
}) {
|
||||
return yield* revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })
|
||||
return yield* mapNotFound(revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload }))
|
||||
})
|
||||
|
||||
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||
return yield* revertSvc.unrevert({ sessionID: ctx.params.sessionID })
|
||||
return yield* mapNotFound(revertSvc.unrevert({ sessionID: ctx.params.sessionID }))
|
||||
})
|
||||
|
||||
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
||||
@@ -329,8 +342,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
||||
params: { sessionID: SessionID; messageID: MessageID }
|
||||
}) {
|
||||
yield* runState.assertNotBusy(ctx.params.sessionID)
|
||||
yield* session.removeMessage(ctx.params)
|
||||
yield* mapNotFound(
|
||||
Effect.gen(function* () {
|
||||
yield* runState.assertNotBusy(ctx.params.sessionID)
|
||||
yield* session.removeMessage(ctx.params)
|
||||
}),
|
||||
)
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
function createSessionWithMessages(directory: string, count: number) {
|
||||
return WithInstance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
const session = await runSession(Session.Service.use((svc) => svc.create({})))
|
||||
for (let i = 0; i < count; i++) {
|
||||
await runSession(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Session.Service
|
||||
fn: () =>
|
||||
runSession(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Session.Service
|
||||
const session = yield* svc.create({})
|
||||
for (let i = 0; i < count; i++) {
|
||||
yield* svc.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
@@ -46,11 +46,10 @@ function createSessionWithMessages(directory: string, count: number) {
|
||||
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
return session.id
|
||||
},
|
||||
}
|
||||
return session.id
|
||||
}),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,22 +81,23 @@ describe("Link header host", () => {
|
||||
})
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Reproducer 2: GET /session/{missing-id}/todo should return 404, not 500.
|
||||
// The session.todo handler in HttpApi doesn't wrap with `mapNotFound`, so a
|
||||
// `NotFoundError` from the service surfaces as a defect → 500. Hono's
|
||||
// equivalent maps to 404 via `errors.notFound`.
|
||||
//
|
||||
// Affected endpoints (handlers without mapNotFound): todo, diff, summarize,
|
||||
// fork, abort, init, deleteMessage, command, shell, revert, unrevert.
|
||||
//
|
||||
// FIXME: unskip when mapNotFound coverage is added (next PR).
|
||||
// Reproducer 2: GET /session/{missing-id}/todo returns 404, not 500.
|
||||
// Previously the session.todo handler didn't wrap with `mapNotFound`, so a
|
||||
// thrown `NotFoundError` surfaced as a defect → 500. Hono's equivalent maps
|
||||
// to 404 via `errors.notFound`. mapNotFound is now applied to all session
|
||||
// endpoints that take a sessionID.
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
describe("404 mapping for missing session", () => {
|
||||
test.todo("HttpApi /session/{missing}/todo returns 404 not 500", async () => {
|
||||
test("HttpApi /session/{missing}/fork returns 404 not 500", async () => {
|
||||
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
||||
|
||||
const response = await app(true).request("/session/ses_does_not_exist/todo", {
|
||||
headers: { "x-opencode-directory": tmp.path },
|
||||
const response = await app(true).request("/session/ses_does_not_exist/fork", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-opencode-directory": tmp.path,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
@@ -105,15 +105,14 @@ describe("404 mapping for missing session", () => {
|
||||
})
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Reproducer 3: 404 response body shape should match Hono's NamedError
|
||||
// envelope `{ name, data: { message } }`. HttpApi returns the typed-error
|
||||
// shape `{ _tag }` instead. SDK consumers reading `error.data.message`
|
||||
// see undefined.
|
||||
//
|
||||
// FIXME: unskip when error JSON shape policy is decided + applied (separate PR).
|
||||
// Reproducer 3: 404 body matches Hono's NamedError envelope
|
||||
// `{ name: "NotFoundError", data: { message } }`. HttpApi previously returned
|
||||
// `{ _tag: "NotFound" }` (empty body via HttpApiError.NotFound). The new
|
||||
// OpencodeNotFound class encodes the legacy shape via its schema fields and
|
||||
// `httpApiStatus: 404` annotation.
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
describe("Error JSON shape parity", () => {
|
||||
test.todo("HttpApi 404 body matches NamedError shape", async () => {
|
||||
test("HttpApi 404 body matches NamedError shape", async () => {
|
||||
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
||||
|
||||
const response = await app(true).request("/session/ses_does_not_exist", {
|
||||
|
||||
Reference in New Issue
Block a user