mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-29 14:31:56 +00:00
fix(server): avoid loading locations for pending reads (#45994)
This commit is contained in:
parent
ce005ce002
commit
d354c3d640
12 changed files with 231 additions and 115 deletions
|
|
@ -1367,7 +1367,7 @@ export function make(options: ClientOptions) {
|
|||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
|
|
@ -1496,7 +1496,7 @@ export function make(options: ClientOptions) {
|
|||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
|
|
|
|||
2
packages/console/app/src/global.d.ts
vendored
2
packages/console/app/src/global.d.ts
vendored
|
|
@ -1,5 +1,7 @@
|
|||
/// <reference types="@solidjs/start/env" />
|
||||
|
||||
import "@solidjs/start"
|
||||
|
||||
export declare module "@solidjs/start/server" {
|
||||
export type APIEvent = { request: Request }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function buildLocationServiceMap(
|
|||
...inner,
|
||||
get: (ref: Location.Ref) => inner.get(canonical(ref)),
|
||||
contextEffect: (ref: Location.Ref) => inner.contextEffect(canonical(ref)),
|
||||
contextEffectOption: (ref: Location.Ref) => inner.contextEffectOption(canonical(ref)),
|
||||
invalidate: (ref: Location.Ref) => inner.invalidate(canonical(ref)),
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Hash,
|
||||
Layer,
|
||||
LayerMap,
|
||||
Option,
|
||||
RcMap,
|
||||
Schema,
|
||||
Stream,
|
||||
|
|
@ -674,14 +675,21 @@ describe("LocationServiceMap", () => {
|
|||
expect(Equal.equals(absent, present)).toBe(false)
|
||||
if (process.platform === "win32") expect(absent.directory).not.toBe(present.directory)
|
||||
|
||||
expect(yield* locations.contextEffectOption(absent)).toEqual(Option.none())
|
||||
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(0)
|
||||
|
||||
const first = yield* locations.contextEffect(absent)
|
||||
expect(yield* locations.contextEffect(present)).toBe(first)
|
||||
expect(Option.getOrThrow(yield* locations.contextEffectOption(absent))).toBe(first)
|
||||
expect(Option.getOrThrow(yield* locations.contextEffectOption(present))).toBe(first)
|
||||
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toEqual([
|
||||
Location.Ref.make({ directory, workspaceID: undefined }),
|
||||
])
|
||||
|
||||
// Invalidating with the shape opposite to the one that booted must evict.
|
||||
yield* locations.invalidate(present)
|
||||
expect(yield* locations.contextEffectOption(absent)).toEqual(Option.none())
|
||||
expect(yield* locations.contextEffectOption(present)).toEqual(Option.none())
|
||||
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(0)
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -55,15 +55,13 @@ export const makeFormGroup = <
|
|||
params: { sessionID: Schema.String },
|
||||
success: Schema.Struct({ data: Schema.Array(Form.Info) }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(formLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.form.list",
|
||||
summary: "List session forms",
|
||||
description: "Retrieve pending forms for a session.",
|
||||
}),
|
||||
),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.form.list",
|
||||
summary: "List session forms",
|
||||
description: "Retrieve pending forms for a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.form.create", "/api/session/:sessionID/form", {
|
||||
|
|
|
|||
|
|
@ -90,15 +90,13 @@ export const makePermissionGroup = <
|
|||
params: { sessionID: Session.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(Permission.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.permission.get", "/api/session/:sessionID/permission/:requestID", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Form } from "@opencode-ai/core/form"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import {
|
||||
ConflictError,
|
||||
FormAlreadySettledError,
|
||||
|
|
@ -6,10 +8,10 @@ import {
|
|||
FormNotFoundError,
|
||||
InvalidRequestError,
|
||||
} from "@opencode-ai/protocol/errors"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Option } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../location"
|
||||
import { requestRef, response, sessionRef, withLoadedLocationServices } from "../location"
|
||||
|
||||
function missingForm(id: Form.ID) {
|
||||
return new FormNotFoundError({ id, message: `Form not found: ${id}` })
|
||||
|
|
@ -17,6 +19,8 @@ function missingForm(id: Form.ID) {
|
|||
|
||||
export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const database = yield* Database.Service
|
||||
const requireOwnedForm = Effect.fnUntraced(function* (sessionID: Form.Info["sessionID"], formID: Form.ID) {
|
||||
const form = yield* Form.Service
|
||||
const info = yield* form.get(formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(formID)))
|
||||
|
|
@ -35,9 +39,16 @@ export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) =
|
|||
.handle(
|
||||
"session.form.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const form = yield* Form.Service
|
||||
const forms = yield* form.list({ sessionID: ctx.params.sessionID })
|
||||
return { data: forms }
|
||||
const ref =
|
||||
ctx.params.sessionID === "global"
|
||||
? requestRef(ctx.request)
|
||||
: yield* sessionRef(database, ctx.params.sessionID)
|
||||
const forms = yield* withLoadedLocationServices(
|
||||
locations,
|
||||
ref,
|
||||
Form.Service.use((form) => form.list({ sessionID: ctx.params.sessionID })),
|
||||
)
|
||||
return { data: Option.getOrElse(forms, () => []) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Option } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
import { response, sessionRef, withLoadedLocationServices } from "../location"
|
||||
|
||||
function missingRequest(id: Permission.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
|
|
@ -13,6 +15,8 @@ function missingRequest(id: Permission.ID) {
|
|||
|
||||
export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const database = yield* Database.Service
|
||||
const requireOwnedRequest = Effect.fnUntraced(function* (
|
||||
sessionID: Permission.Request["sessionID"],
|
||||
requestID: Permission.ID,
|
||||
|
|
@ -63,8 +67,13 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
|
|||
.handle(
|
||||
"session.permission.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const permission = yield* Permission.Service
|
||||
return { data: yield* permission.forSession(ctx.params.sessionID) }
|
||||
const ref = yield* sessionRef(database, ctx.params.sessionID)
|
||||
const requests = yield* withLoadedLocationServices(
|
||||
locations,
|
||||
ref,
|
||||
Permission.Service.use((permission) => permission.forSession(ctx.params.sessionID)),
|
||||
)
|
||||
return { data: Option.getOrElse(requests, () => []) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
|
||||
|
|
@ -26,6 +31,41 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
|||
})
|
||||
}
|
||||
|
||||
const decodeSessionID = Schema.decodeUnknownEffect(Session.ID)
|
||||
|
||||
export function sessionRef(database: Context.Service.Shape<typeof Database.Service>, sessionID: unknown) {
|
||||
return Effect.gen(function* () {
|
||||
const id = yield* decodeSessionID(sessionID).pipe(
|
||||
Effect.mapError(() => new InvalidRequestError({ message: "Invalid session ID", field: "sessionID" })),
|
||||
)
|
||||
const row = yield* database.db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return yield* new SessionNotFoundError({ sessionID: id, message: `Session not found: ${id}` })
|
||||
return Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function withLoadedLocationServices<A, E>(
|
||||
locations: Context.Service.Shape<typeof LocationServiceMap.Service>,
|
||||
ref: Location.Ref,
|
||||
effect: Effect.Effect<A, E, LocationServices>,
|
||||
) {
|
||||
return Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const context = yield* locations.contextEffectOption(ref)
|
||||
if (Option.isNone(context)) return Option.none<A>()
|
||||
return Option.some(yield* effect.pipe(Effect.provide(context.value)))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function requestRef(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
const query = new URL(request.url, "http://localhost").searchParams
|
||||
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { requestRef, type LocationServices } from "../location"
|
||||
import { requestRef, sessionRef, type LocationServices } from "../location"
|
||||
|
||||
export class FormLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
FormLocationMiddleware,
|
||||
|
|
@ -19,12 +13,10 @@ export class FormLocationMiddleware extends HttpApiMiddleware.Service<
|
|||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}) {}
|
||||
|
||||
const decodeSessionID = Schema.decodeUnknownEffect(Session.ID)
|
||||
|
||||
export const formLocationLayer = Layer.effect(
|
||||
FormLocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const database = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
return FormLocationMiddleware.of((effect) =>
|
||||
|
|
@ -38,38 +30,8 @@ export const formLocationLayer = Layer.effect(
|
|||
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
|
||||
}
|
||||
|
||||
const sessionID = yield* decodeSessionID(route.params.sessionID).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new InvalidRequestError({
|
||||
message: "Invalid session ID",
|
||||
field: "sessionID",
|
||||
}),
|
||||
),
|
||||
)
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) {
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
}
|
||||
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(
|
||||
locations.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const ref = yield* sessionRef(database, route.params.sessionID)
|
||||
return yield* effect.pipe(Effect.provide(locations.get(ref)))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import type { LocationServices } from "../location"
|
||||
import { sessionRef, type LocationServices } from "../location"
|
||||
|
||||
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
SessionLocationMiddleware,
|
||||
|
|
@ -19,48 +13,17 @@ export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
|||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}) {}
|
||||
|
||||
const decodeSessionID = Schema.decodeUnknownEffect(Session.ID)
|
||||
|
||||
export const sessionLocationLayer = Layer.effect(
|
||||
SessionLocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const database = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
return SessionLocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
const sessionID = yield* decodeSessionID(route.params.sessionID).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new InvalidRequestError({
|
||||
message: "Invalid session ID",
|
||||
field: "sessionID",
|
||||
}),
|
||||
),
|
||||
)
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(
|
||||
locations.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const ref = yield* sessionRef(database, route.params.sessionID)
|
||||
return yield* effect.pipe(Effect.provide(locations.get(ref)))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { makeMemoryDriver } from "@opencode-ai/core/environment/index"
|
|||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
|
||||
import { Effect } from "effect"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerFetch } from "../src/fetch"
|
||||
|
||||
|
|
@ -276,6 +277,129 @@ it.live("serves the session view operation and missing-session error", () =>
|
|||
}),
|
||||
)
|
||||
|
||||
it.live("does not load a location when reading pending session requests", () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-pending-read-")))
|
||||
const handler = yield* ServerFetch.make({
|
||||
...options,
|
||||
config: {
|
||||
directory: config.path,
|
||||
project: false,
|
||||
content: JSON.stringify({ permissions: [{ action: "shell", resource: "*", effect: "ask" }] }),
|
||||
},
|
||||
})
|
||||
const created = (yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{}",
|
||||
}),
|
||||
).then((response) => response.json()),
|
||||
)) as { data: { id: string } }
|
||||
|
||||
const loaded = () =>
|
||||
Effect.promise(() =>
|
||||
handler(new Request("http://opencode.local/api/debug/location")).then(
|
||||
(response) => response.json() as Promise<unknown[]>,
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* loaded()).toEqual([])
|
||||
for (const resource of ["permission", "form"]) {
|
||||
const response = yield* Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session/${created.data.id}/${resource}`)),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => response.json())).toEqual({ data: [] })
|
||||
|
||||
const missing = yield* Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session/ses_missing_pending/${resource}`)),
|
||||
)
|
||||
expect(missing.status).toBe(404)
|
||||
}
|
||||
const global = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session/global/form", {
|
||||
headers: { "x-opencode-directory": encodeURIComponent(process.cwd()) },
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(global.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => global.json())).toEqual({ data: [] })
|
||||
expect(yield* loaded()).toEqual([])
|
||||
|
||||
const createdForm = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request(`http://opencode.local/api/session/${created.data.id}/form`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ title: "Test form", fields: [{ key: "answer", type: "string" }] }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(createdForm.status).toBe(200)
|
||||
|
||||
const forms = yield* Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session/${created.data.id}/form`)),
|
||||
)
|
||||
expect(forms.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => forms.json())).toMatchObject({
|
||||
data: [{ title: "Test form" }],
|
||||
})
|
||||
expect(yield* loaded()).toHaveLength(1)
|
||||
|
||||
const globalForm = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session/global/form", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-opencode-directory": encodeURIComponent(process.cwd()),
|
||||
},
|
||||
body: JSON.stringify({ title: "Global form", fields: [{ key: "answer", type: "string" }] }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(globalForm.status).toBe(200)
|
||||
|
||||
const globalForms = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session/global/form", {
|
||||
headers: { "x-opencode-directory": encodeURIComponent(process.cwd()) },
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(globalForms.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => globalForms.json())).toMatchObject({ data: [{ title: "Global form" }] })
|
||||
|
||||
// Agent permission policy is installed by plugin activation.
|
||||
expect((yield* ready(handler)).status).toBe(200)
|
||||
const createdPermission = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request(`http://opencode.local/api/session/${created.data.id}/permission`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ id: "per_pending_read", action: "shell", resources: ["pwd"] }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(createdPermission.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => createdPermission.json())).toEqual({
|
||||
data: { id: "per_pending_read", effect: "ask" },
|
||||
})
|
||||
|
||||
const permissions = yield* Effect.promise(() =>
|
||||
handler(new Request(`http://opencode.local/api/session/${created.data.id}/permission`)),
|
||||
)
|
||||
expect(permissions.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => permissions.json())).toMatchObject({
|
||||
data: [{ id: "per_pending_read", sessionID: created.data.id, action: "shell", resources: ["pwd"] }],
|
||||
})
|
||||
expect(yield* loaded()).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
|
||||
// Pins the eager-boot guarantee: the application layer is built before the handler returns, so
|
||||
// an aborted first request cannot interrupt layer construction and wedge every later request
|
||||
// (the Effect-TS/effect#6319 failure class that lazy first-request builds are prone to).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue