Merge remote-tracking branch 'origin/dev' into refactor/core-database-schema-ownership

# Conflicts:
#	packages/core/src/project.ts
#	packages/opencode/src/project/project.ts
This commit is contained in:
Dax Raad
2026-05-24 01:57:05 -04:00
176 changed files with 4485 additions and 1728 deletions
@@ -177,6 +177,15 @@ const scenarios: Scenario[] = [
},
"status",
),
http.protected
.patch("/project/{projectID}", "project.update.missing")
.mutating()
.at((ctx) => ({
path: route("/project/{projectID}", { projectID: "project_httpapi_missing" }),
headers: ctx.headers(),
body: { name: "Missing Project" },
}))
.json(404, object, "status"),
http.protected
.post("/project/git/init", "project.initGit")
.mutating()
@@ -404,9 +413,7 @@ const scenarios: Scenario[] = [
.delete("/pty/{ptyID}", "pty.remove")
.mutating()
.at((ctx) => ({ path: route("/pty/{ptyID}", { ptyID: "pty_httpapi_missing" }), headers: ctx.headers() }))
.json(200, (body) => {
check(body === true, "PTY remove should return true")
}),
.json(404, object, "status"),
http.protected
.get("/pty/{ptyID}/connect", "pty.connect")
.at((ctx) => ({ path: route("/pty/{ptyID}/connect", { ptyID: "pty_httpapi_missing" }), headers: ctx.headers() }))
@@ -9,6 +9,7 @@ import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/co
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { PermissionID } from "../../src/permission/schema"
import { ProjectID } from "../../src/project/schema"
import { QuestionID } from "../../src/question/schema"
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
import { HEADER as FenceHeader } from "../../src/server/shared/fence"
@@ -205,6 +206,30 @@ describe("instance HttpApi", () => {
}),
)
it.live("returns typed not found bodies for missing projects", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const projectID = ProjectID.make("project_missing")
const response = yield* Effect.promise(() =>
HttpApiApp.webHandler().handler(
new Request(`http://localhost/project/${projectID}`, {
method: "PATCH",
headers: { "x-opencode-directory": dir, "content-type": "application/json" },
body: JSON.stringify({ name: "Missing" }),
}),
handlerContext,
),
)
expect(response.status).toBe(404)
expect(yield* Effect.promise(() => response.json())).toEqual({
_tag: "ProjectNotFoundError",
projectID,
message: `Project not found: ${projectID}`,
})
}),
)
it.live("serves path and VCS read endpoints", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
@@ -112,6 +112,31 @@ describe("pty HttpApi bridge", () => {
const missing = await app().request(PtyPaths.get.replace(":ptyID", info.id), { headers })
expect(missing.status).toBe(404)
expect(await missing.json()).toEqual({
_tag: "PtyNotFoundError",
ptyID: info.id,
message: `PTY session not found: ${info.id}`,
})
const missingUpdate = await app().request(PtyPaths.update.replace(":ptyID", info.id), {
method: "PUT",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ title: "missing" }),
})
expect(missingUpdate.status).toBe(404)
expect(await missingUpdate.json()).toEqual({
_tag: "PtyNotFoundError",
ptyID: info.id,
message: `PTY session not found: ${info.id}`,
})
const missingRemove = await app().request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers })
expect(missingRemove.status).toBe(404)
expect(await missingRemove.json()).toEqual({
_tag: "PtyNotFoundError",
ptyID: info.id,
message: `PTY session not found: ${info.id}`,
})
})
test("returns 404 for missing PTY websocket before upgrade", async () => {
@@ -121,6 +146,63 @@ describe("pty HttpApi bridge", () => {
})
expect(response.status).toBe(404)
})
test("returns typed not found errors for missing PTY HTTP resources", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const headers = { "x-opencode-directory": tmp.path }
const missingID = String(PtyID.ascending())
const expected = {
_tag: "PtyNotFoundError",
ptyID: missingID,
message: `PTY session not found: ${missingID}`,
}
const found = await app().request(PtyPaths.get.replace(":ptyID", missingID), { headers })
expect(found.status).toBe(404)
expect(await found.json()).toEqual(expected)
const updated = await app().request(PtyPaths.update.replace(":ptyID", missingID), {
method: "PUT",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ title: "missing" }),
})
expect(updated.status).toBe(404)
expect(await updated.json()).toEqual(expected)
const removed = await app().request(PtyPaths.remove.replace(":ptyID", missingID), { method: "DELETE", headers })
expect(removed.status).toBe(404)
expect(await removed.json()).toEqual(expected)
})
test("returns typed errors for PTY connect token failures", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const headers = { "x-opencode-directory": tmp.path }
const missingID = String(PtyID.ascending())
const forbidden = await app().request(PtyPaths.connectToken.replace(":ptyID", missingID), {
method: "POST",
headers,
})
expect(forbidden.status).toBe(403)
expect(await forbidden.json()).toEqual({
_tag: "PtyForbiddenError",
message: "Invalid PTY connect token request",
})
const missing = await app().request(PtyPaths.connectToken.replace(":ptyID", missingID), {
method: "POST",
headers: {
...headers,
"x-opencode-ticket": "1",
},
})
expect(missing.status).toBe(404)
expect(await missing.json()).toEqual({
_tag: "PtyNotFoundError",
ptyID: missingID,
message: `PTY session not found: ${missingID}`,
})
})
;(process.platform === "win32" ? effectIt.live.skip : effectIt.live)(
"serves PTY websocket output and input through Effect routes",
() =>
@@ -190,4 +190,30 @@ describe("PublicApi OpenAPI v2 errors", () => {
)
}
})
test("documents PTY resource and ticket errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["get", "/pty/{ptyID}"],
["put", "/pty/{ptyID}"],
["delete", "/pty/{ptyID}"],
["post", "/pty/{ptyID}/connect-token"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
"PtyNotFoundError",
)
}
expect(componentName(responseRef(spec.paths["/pty/{ptyID}/connect-token"]?.post?.responses?.["403"]) ?? "")).toBe(
"PtyForbiddenError",
)
})
test("documents project not-found errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(componentName(responseRef(spec.paths["/project/{projectID}"]?.patch?.responses?.["404"]) ?? "")).toBe(
"ProjectNotFoundError",
)
})
})
@@ -5,6 +5,7 @@ import path from "node:path"
import { Effect, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceID } from "../../src/control-plane/schema"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
@@ -250,6 +251,26 @@ describe("workspace HttpApi", () => {
}),
)
it.live("returns a declared not found error when warping into a missing workspace", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const session = yield* Session.use.create({}).pipe(provideInstance(dir))
const workspaceID = WorkspaceID.ascending("wrk_missing_warp")
const response = yield* request(WorkspacePaths.warp, dir, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ id: workspaceID, sessionID: session.id }),
})
expect(response.status).toBe(404)
expect(yield* Effect.promise(() => response.json())).toEqual({
name: "NotFoundError",
data: { message: `Workspace not found: ${workspaceID}` },
})
}),
)
it.live("creates workspace with the TUI payload shape", () =>
Effect.gen(function* () {
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true