Przeglądaj źródła

Migrate schema error body tests to Effect runner (#27172)

Kit Langton 3 miesięcy temu
rodzic
commit
2c334d9242

+ 63 - 73
packages/opencode/test/server/httpapi-schema-error-body.test.ts

@@ -3,7 +3,6 @@ import { Effect } from "effect"
 import { eq } from "drizzle-orm"
 import * as Database from "@/storage/db"
 import { ModelID, ProviderID } from "../../src/provider/schema"
-import { WithInstance } from "../../src/project/with-instance"
 import { Server } from "../../src/server/server"
 import { Session } from "@/session/session"
 import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
@@ -11,80 +10,68 @@ import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
 import { MessageID, PartID } from "../../src/session/schema"
 import { PartTable } from "@/session/session.sql"
 import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-import { it } from "../lib/effect"
+import { disposeAllInstances, TestInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const it = testEffect(Session.defaultLayer)
 
 afterEach(async () => {
   await disposeAllInstances()
   await resetDatabase()
 })
 
-const withTmp = <A, E, R>(
-  options: Parameters<typeof tmpdir>[0],
-  fn: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>,
-) =>
-  Effect.acquireRelease(
-    Effect.promise(() => tmpdir(options)),
-    (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-  ).pipe(Effect.flatMap(fn))
-
-async function seedCorruptStepFinishPart(directory: string) {
-  return WithInstance.provide({
-    directory,
-    fn: () =>
-      Effect.runPromise(
-        Effect.gen(function* () {
-          const session = yield* Session.Service
-          const info = yield* session.create({})
-          const message = yield* session.updateMessage({
-            id: MessageID.ascending(),
-            role: "user",
-            sessionID: info.id,
-            agent: "build",
-            model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
-            time: { created: Date.now() },
-          })
-          const partID = PartID.ascending()
-          yield* session.updatePart({
-            id: partID,
-            sessionID: info.id,
-            messageID: message.id,
+const seedCorruptStepFinishPart = Effect.gen(function* () {
+  const session = yield* Session.Service
+  const info = yield* session.create({})
+  const message = yield* session.updateMessage({
+    id: MessageID.ascending(),
+    role: "user",
+    sessionID: info.id,
+    agent: "build",
+    model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
+    time: { created: Date.now() },
+  })
+  const partID = PartID.ascending()
+  yield* session.updatePart({
+    id: partID,
+    sessionID: info.id,
+    messageID: message.id,
+    type: "step-finish",
+    reason: "stop",
+    cost: 0,
+    tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+  })
+  // Schema.Finite still rejects NaN at encode: exact mirror of the corrupt row
+  // that broke the user's session in the OMO/Windows bug.
+  yield* Effect.sync(() =>
+    Database.use((db) =>
+      db
+        .update(PartTable)
+        .set({
+          data: {
             type: "step-finish",
             reason: "stop",
             cost: 0,
-            tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
-          })
-          // Schema.Finite still rejects NaN at encode — exact mirror of the
-          // corrupt row that broke the user's session in the OMO/Windows bug.
-          Database.use((db) =>
-            db
-              .update(PartTable)
-              .set({
-                data: {
-                  type: "step-finish",
-                  reason: "stop",
-                  cost: 0,
-                  tokens: { input: 0, output: NaN, reasoning: 0, cache: { read: 0, write: 0 } },
-                } as never, // drizzle's .set() can't narrow the discriminated union
-              })
-              .where(eq(PartTable.id, partID))
-              .run(),
-          )
-          return info.id
-        }).pipe(Effect.provide(Session.defaultLayer)),
-      ),
-  })
-}
+            tokens: { input: 0, output: NaN, reasoning: 0, cache: { read: 0, write: 0 } },
+          } as never, // drizzle's .set() can't narrow the discriminated union
+        })
+        .where(eq(PartTable.id, partID))
+        .run(),
+    ),
+  )
+  return info.id
+})
 
 describe("schema-rejection wire shape", () => {
-  it.live(
+  it.instance(
     "Payload schema rejection returns NamedError-shaped JSON, not empty",
-    withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
+    () =>
       Effect.gen(function* () {
+        const test = yield* TestInstance
         const res = yield* Effect.promise(async () =>
           Server.Default().app.request(SyncPaths.history, {
             method: "POST",
-            headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
+            headers: { "x-opencode-directory": test.directory, "content-type": "application/json" },
             body: JSON.stringify({ aggregate: -1 }),
           }),
         )
@@ -99,36 +86,38 @@ describe("schema-rejection wire shape", () => {
         expect(parsed.data.message).toEqual(expect.any(String))
         expect(parsed.data.message.length).toBeGreaterThan(0)
       }),
-    ),
+    { git: true, config: { formatter: false, lsp: false } },
   )
 
-  it.live(
+  it.instance(
     "Query schema rejection returns NamedError-shaped JSON",
-    withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
+    () =>
       Effect.gen(function* () {
+        const test = yield* TestInstance
         // /find/file?limit=999999 violates the limit constraint check.
-        const url = `/find/file?query=foo&limit=999999&directory=${encodeURIComponent(tmp.path)}`
+        const url = `/find/file?query=foo&limit=999999&directory=${encodeURIComponent(test.directory)}`
         const res = yield* Effect.promise(async () => Server.Default().app.request(url))
         const body = yield* Effect.promise(async () => res.text())
         expect(res.status).toBe(400)
         const parsed = JSON.parse(body)
         expect(parsed).toMatchObject({ name: "BadRequest", data: { kind: "Query" } })
       }),
-    ),
+    { git: true, config: { formatter: false, lsp: false } },
   )
 
-  it.live(
+  it.instance(
     "rejected request body never echoes back unbounded — message is capped",
     // Defense against DoS-amplification + secret-echo: Effect's Issue formatter
     // dumps the rejected `actual` verbatim. A multi-MB invalid array would
     // become a multi-MB 400 response and log line. Cap kicks in around 1KB.
-    withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
+    () =>
       Effect.gen(function* () {
+        const test = yield* TestInstance
         const huge = "X".repeat(50_000)
         const res = yield* Effect.promise(async () =>
           Server.Default().app.request(SyncPaths.history, {
             method: "POST",
-            headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
+            headers: { "x-opencode-directory": test.directory, "content-type": "application/json" },
             body: JSON.stringify({ aggregate: huge }),
           }),
         )
@@ -139,15 +128,16 @@ describe("schema-rejection wire shape", () => {
         const parsed = JSON.parse(body)
         expect(parsed.data.message).not.toContain(huge)
       }),
-    ),
+    { git: true, config: { formatter: false, lsp: false } },
   )
 
-  it.live(
+  it.instance(
     "response-encode failure: corrupted stored row returns NamedError-shaped JSON with field path",
-    withTmp({ config: { formatter: false, lsp: false } }, (tmp) =>
+    () =>
       Effect.gen(function* () {
-        const sessionID = yield* Effect.promise(() => seedCorruptStepFinishPart(tmp.path))
-        const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(tmp.path)}`
+        const test = yield* TestInstance
+        const sessionID = yield* seedCorruptStepFinishPart
+        const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(test.directory)}`
         const res = yield* Effect.promise(async () => Server.Default().app.request(url))
         const body = yield* Effect.promise(async () => res.text())
         expect(res.status).toBe(400)
@@ -157,6 +147,6 @@ describe("schema-rejection wire shape", () => {
         // Field path in data.message — what made this PR worth shipping.
         expect(parsed.data.message).toMatch(/output/)
       }),
-    ),
+    { config: { formatter: false, lsp: false } },
   )
 })