research: delete Hono backend (do not merge) (#25667)

This commit is contained in:
Kit Langton
2026-05-09 09:10:42 -04:00
committed by GitHub
parent 32684e70e6
commit 28b03595bf
81 changed files with 224 additions and 7566 deletions

View File

@@ -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(

View File

@@ -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,
}

View File

@@ -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,
})

View File

@@ -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("")
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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,

View File

@@ -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"