research: delete Hono backend (do not merge) (#25667)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { ConfigProvider, Effect, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { parse } from "./assertions"
|
||||
@@ -56,16 +55,7 @@ function app(modules: Runtime, backend: Backend, options: CallOptions) {
|
||||
const username = options.auth?.username
|
||||
const password = options.auth?.password
|
||||
const cacheKey = `${backend}:${username ?? ""}:${password ?? ""}`
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect"
|
||||
Flag.OPENCODE_SERVER_PASSWORD = password
|
||||
Flag.OPENCODE_SERVER_USERNAME = username
|
||||
if (appCache[cacheKey]) return appCache[cacheKey]
|
||||
if (backend === "legacy") {
|
||||
const legacy = modules.Server.Legacy().app
|
||||
return (appCache[cacheKey] = {
|
||||
request: (input, init) => legacy.request(input, init),
|
||||
})
|
||||
}
|
||||
|
||||
const handler = HttpRouter.toWebHandler(
|
||||
modules.ExperimentalHttpApiServer.routes.pipe(
|
||||
|
||||
@@ -22,7 +22,6 @@ process.env.OPENCODE_DB = exerciseDatabasePath
|
||||
Flag.OPENCODE_DB = exerciseDatabasePath
|
||||
|
||||
export const original = {
|
||||
OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
|
||||
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
|
||||
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* End-to-end exerciser for the legacy Hono instance routes and the Effect HttpApi routes.
|
||||
* End-to-end exerciser for the Effect HttpApi routes.
|
||||
*
|
||||
* The goal is not to be a normal unit test file. This is a route-coverage and parity
|
||||
* harness we can run while deleting Hono: every public route should eventually have a
|
||||
* small scenario that proves the Effect route decodes requests, uses the right instance
|
||||
* context, mutates storage when expected, and returns a compatible response shape.
|
||||
* The goal is not to be a normal unit test file. This is a route-coverage harness:
|
||||
* every public route should have a small scenario that proves the route decodes
|
||||
* requests, uses the right instance context, mutates storage when expected, and
|
||||
* returns the expected response shape.
|
||||
*
|
||||
* The script intentionally isolates `OPENCODE_DB` before importing modules that touch
|
||||
* storage. Scenarios may create/delete sessions and reset the database after each run,
|
||||
@@ -15,8 +15,7 @@
|
||||
* - `.seeded(...)` creates typed per-scenario state using Effect helpers on `ctx`.
|
||||
* - `.at(...)` builds the request from that typed state.
|
||||
* - `.json(...)` / `.jsonEffect(...)` assert response shape and optional side effects.
|
||||
* - `.mutating()` tells parity mode to run Effect and Hono in separate isolated contexts
|
||||
* so destructive routes compare equivalent fresh setups instead of sharing one DB.
|
||||
* - `.mutating()` tells the runner to reset isolated state after destructive routes.
|
||||
*/
|
||||
import { Effect } from "effect"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -1263,7 +1262,6 @@ const main = Effect.gen(function* () {
|
||||
const options = parseOptions(Bun.argv.slice(2))
|
||||
const modules = yield* Effect.promise(() => runtime())
|
||||
const effectRoutes = routeKeys(OpenApi.fromApi(modules.PublicApi))
|
||||
const honoRoutes = routeKeys(yield* Effect.promise(() => modules.Server.openapiHono()))
|
||||
const selected = selectedScenarios(options, scenarios)
|
||||
const missing = effectRoutes.filter((route) => !scenarios.some((scenario) => route === routeKey(scenario)))
|
||||
const extra = scenarios.filter((scenario) => !effectRoutes.includes(routeKey(scenario)))
|
||||
@@ -1274,7 +1272,7 @@ const main = Effect.gen(function* () {
|
||||
}
|
||||
}
|
||||
|
||||
printHeader(options, effectRoutes, honoRoutes, selected, missing, extra, {
|
||||
printHeader(options, effectRoutes, selected, missing, extra, {
|
||||
database: exerciseDatabasePath,
|
||||
global: exerciseGlobalRoot,
|
||||
})
|
||||
|
||||
@@ -14,7 +14,6 @@ export const color = {
|
||||
export function printHeader(
|
||||
options: Options,
|
||||
effectRoutes: string[],
|
||||
honoRoutes: string[],
|
||||
selected: Scenario[],
|
||||
missing: string[],
|
||||
extra: Scenario[],
|
||||
@@ -24,7 +23,7 @@ export function printHeader(
|
||||
console.log(`${color.dim}db=${paths.database}${color.reset}`)
|
||||
console.log(`${color.dim}global=${paths.global}${color.reset}`)
|
||||
console.log(
|
||||
`${color.dim}mode=${options.mode} selected=${selected.length} scenarioTimeout=${Duration.format(options.scenarioTimeout)} effectRoutes=${effectRoutes.length} missing=${missing.length} extra=${extra.length} onlyEffect=${effectRoutes.filter((route) => !honoRoutes.includes(route)).length} onlyHono=${honoRoutes.filter((route) => !effectRoutes.includes(route)).length}${color.reset}`,
|
||||
`${color.dim}mode=${options.mode} selected=${selected.length} scenarioTimeout=${Duration.format(options.scenarioTimeout)} effectRoutes=${effectRoutes.length} missing=${missing.length} extra=${extra.length}${color.reset}`,
|
||||
)
|
||||
console.log("")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function coverageResult(scenario: Scenario): Result {
|
||||
|
||||
export function parseOptions(args: string[]): Options {
|
||||
const mode = option(args, "--mode") ?? "effect"
|
||||
if (mode !== "effect" && mode !== "parity" && mode !== "coverage" && mode !== "auth")
|
||||
if (mode !== "effect" && mode !== "coverage" && mode !== "auth")
|
||||
throw new Error(`invalid --mode ${mode}`)
|
||||
return {
|
||||
mode,
|
||||
|
||||
@@ -5,13 +5,11 @@ import type { Config } from "../../../src/config/config"
|
||||
import { ModelID, ProviderID } from "../../../src/provider/schema"
|
||||
import type { MessageV2 } from "../../../src/session/message-v2"
|
||||
import { MessageID, PartID } from "../../../src/session/schema"
|
||||
import { stable } from "./assertions"
|
||||
import { call, callAuthProbe } from "./backend"
|
||||
import { original } from "./environment"
|
||||
import { runtime } from "./runtime"
|
||||
import type {
|
||||
ActiveScenario,
|
||||
CallResult,
|
||||
Options,
|
||||
ProjectOptions,
|
||||
Result,
|
||||
@@ -38,16 +36,6 @@ export function runScenario(options: Options) {
|
||||
function runActive(options: Options, scenario: ActiveScenario) {
|
||||
if (options.mode === "auth") return runAuth(scenario)
|
||||
|
||||
if (options.mode === "parity" && scenario.mutates && scenario.compare !== "none") {
|
||||
return Effect.gen(function* () {
|
||||
const effect = yield* runBackend(options, "effect", scenario)
|
||||
const legacy = yield* runBackend(options, "legacy", scenario)
|
||||
yield* trace(options, scenario, "compare start")
|
||||
yield* compare(scenario, effect, legacy)
|
||||
yield* trace(options, scenario, "compare done")
|
||||
})
|
||||
}
|
||||
|
||||
return withContext(options, scenario, "shared", (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* trace(options, scenario, "effect request start")
|
||||
@@ -56,17 +44,6 @@ function runActive(options: Options, scenario: ActiveScenario) {
|
||||
yield* trace(options, scenario, "effect expect start")
|
||||
yield* scenario.expect(ctx, ctx.state, effect)
|
||||
yield* trace(options, scenario, "effect expect done")
|
||||
if (options.mode === "parity" && scenario.compare !== "none") {
|
||||
yield* trace(options, scenario, "legacy request start")
|
||||
const legacy = yield* call("legacy", scenario, ctx)
|
||||
yield* trace(options, scenario, `legacy response ${legacy.status}`)
|
||||
yield* trace(options, scenario, "legacy expect start")
|
||||
yield* scenario.expect(ctx, ctx.state, legacy)
|
||||
yield* trace(options, scenario, "legacy expect done")
|
||||
yield* trace(options, scenario, "compare start")
|
||||
yield* compare(scenario, effect, legacy)
|
||||
yield* trace(options, scenario, "compare done")
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -74,38 +51,18 @@ function runActive(options: Options, scenario: ActiveScenario) {
|
||||
function runAuth(scenario: ActiveScenario) {
|
||||
return Effect.gen(function* () {
|
||||
const effect = yield* callAuthProbe("effect", scenario, "missing")
|
||||
const legacy = yield* callAuthProbe("legacy", scenario, "missing")
|
||||
if (scenario.auth === "protected") {
|
||||
if (effect.status !== 401) throw new Error(`effect auth expected 401, got ${effect.status}`)
|
||||
if (legacy.status !== 401) throw new Error(`legacy auth expected 401, got ${legacy.status}`)
|
||||
const effectAuthed = yield* callAuthProbe("effect", scenario, "valid")
|
||||
const legacyAuthed = yield* callAuthProbe("legacy", scenario, "valid")
|
||||
if (effectAuthed.status === 401) throw new Error("effect auth rejected valid credentials")
|
||||
if (legacyAuthed.status === 401) throw new Error("legacy auth rejected valid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
if (effect.status === 401) throw new Error("effect auth expected public access, got 401")
|
||||
if (legacy.status === 401) throw new Error("legacy auth expected public access, got 401")
|
||||
if (effect.timedOut) throw new Error("effect auth expected public access, probe timed out")
|
||||
if (legacy.timedOut) throw new Error("legacy auth expected public access, probe timed out")
|
||||
})
|
||||
}
|
||||
|
||||
function runBackend(options: Options, backend: "effect" | "legacy", scenario: ActiveScenario) {
|
||||
return withContext(options, scenario, backend, (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* trace(options, scenario, `${backend} request start`)
|
||||
const result = yield* call(backend, scenario, ctx)
|
||||
yield* trace(options, scenario, `${backend} response ${result.status}`)
|
||||
yield* trace(options, scenario, `${backend} expect start`)
|
||||
yield* scenario.expect(ctx, ctx.state, result)
|
||||
yield* trace(options, scenario, `${backend} expect done`)
|
||||
return result
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function withContext<A, E>(
|
||||
options: Options,
|
||||
scenario: ActiveScenario,
|
||||
@@ -300,19 +257,8 @@ function fakeLlmConfig(url: string): Partial<Config.Info> {
|
||||
}
|
||||
}
|
||||
|
||||
function compare(scenario: ActiveScenario, effect: CallResult, legacy: CallResult) {
|
||||
return Effect.sync(() => {
|
||||
if (effect.status !== legacy.status)
|
||||
throw new Error(`legacy returned ${legacy.status}, effect returned ${effect.status}`)
|
||||
if (scenario.compare === "status") return
|
||||
if (stable(effect.body) !== stable(legacy.body))
|
||||
throw new Error(`JSON parity mismatch\nlegacy: ${stable(legacy.body)}\neffect: ${stable(effect.body)}`)
|
||||
})
|
||||
}
|
||||
|
||||
const resetState = Effect.promise(async () => {
|
||||
const modules = await runtime()
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
|
||||
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
|
||||
await modules.disposeAllInstances()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export type Runtime = {
|
||||
PublicApi: (typeof import("../../../src/server/routes/instance/httpapi/public"))["PublicApi"]
|
||||
ExperimentalHttpApiServer: (typeof import("../../../src/server/routes/instance/httpapi/server"))["ExperimentalHttpApiServer"]
|
||||
Server: (typeof import("../../../src/server/server"))["Server"]
|
||||
AppLayer: (typeof import("../../../src/effect/app-runtime"))["AppLayer"]
|
||||
InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"]
|
||||
Instance: (typeof import("../../../src/project/instance"))["Instance"]
|
||||
@@ -22,7 +21,6 @@ export function runtime() {
|
||||
return (runtimePromise ??= (async () => {
|
||||
const publicApi = await import("../../../src/server/routes/instance/httpapi/public")
|
||||
const httpApiServer = await import("../../../src/server/routes/instance/httpapi/server")
|
||||
const server = await import("../../../src/server/server")
|
||||
const appRuntime = await import("../../../src/effect/app-runtime")
|
||||
const instanceRef = await import("../../../src/effect/instance-ref")
|
||||
const instance = await import("../../../src/project/instance")
|
||||
@@ -37,7 +35,6 @@ export function runtime() {
|
||||
return {
|
||||
PublicApi: publicApi.PublicApi,
|
||||
ExperimentalHttpApiServer: httpApiServer.ExperimentalHttpApiServer,
|
||||
Server: server.Server,
|
||||
AppLayer: appRuntime.AppLayer,
|
||||
InstanceRef: instanceRef.InstanceRef,
|
||||
Instance: instance.Instance,
|
||||
|
||||
@@ -10,8 +10,8 @@ export const Methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const
|
||||
|
||||
export type Method = (typeof Methods)[number]
|
||||
export type OpenApiMethod = (typeof OpenApiMethods)[number]
|
||||
export type Mode = "effect" | "parity" | "coverage" | "auth"
|
||||
export type Backend = "effect" | "legacy"
|
||||
export type Mode = "effect" | "coverage" | "auth"
|
||||
export type Backend = "effect"
|
||||
export type Comparison = "none" | "status" | "json"
|
||||
export type CaptureMode = "full" | "stream"
|
||||
export type AuthPolicy = "protected" | "public" | "public-bypass" | "ticket-bypass"
|
||||
|
||||
Reference in New Issue
Block a user