Quellcode durchsuchen

test(session): regression test for #26574 + mirror loosening on Vcs.FileDiff (#26578)

Co-authored-by: Developer <temp@example.com>
Kit Langton vor 3 Monaten
Ursprung
Commit
805af011c9

+ 4 - 1
packages/opencode/src/project/vcs.ts

@@ -230,7 +230,10 @@ export type Info = Schema.Schema.Type<typeof Info>
 
 export const FileDiff = Schema.Struct({
   file: Schema.String,
-  patch: Schema.String,
+  // Mirrors Snapshot.FileDiff (see #26574). The current producer always
+  // populates patch, but loosening matches the sibling schema so a
+  // future code path that omits it can't crash /instance/vcs/diff.
+  patch: Schema.optional(Schema.String),
   additions: NonNegativeInt,
   deletions: NonNegativeInt,
   status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),

+ 3 - 0
packages/opencode/src/snapshot/index.ts

@@ -21,6 +21,9 @@ export type Patch = typeof Patch.Type
 
 export const FileDiff = Schema.Struct({
   file: Schema.String,
+  // Optional because legacy/imported `summary_diffs` on disk may omit
+  // the patch text (see #26574). Required-Schema rejected the whole
+  // /session/<id>/diff response and broke session loading on Desktop.
   patch: Schema.optional(Schema.String),
   additions: NonNegativeInt,
   deletions: NonNegativeInt,

+ 79 - 0
packages/opencode/test/server/session-diff-missing-patch.test.ts

@@ -0,0 +1,79 @@
+/**
+ * Regression test for the same bug class as #26574 (sibling of #26566 and
+ * #26553). The Desktop app calls GET /session/<id>/diff; before #26574
+ * the response was Schema-encoded against `Snapshot.FileDiff` with
+ * `patch: Schema.String` (required), so any session whose stored
+ * `summary_diffs` had a row without `patch` returned HTTP 400 and the
+ * session never loaded.
+ *
+ * This test inserts a session row with a missing-patch diff entry and
+ * asserts that GET /session/<id>/diff returns 200 with the row intact.
+ */
+import { afterEach, describe, expect } from "bun:test"
+import { Effect } from "effect"
+import { Server } from "@/server/server"
+import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
+import { Session } from "@/session/session"
+import { Storage } from "@/storage/storage"
+import { WithInstance } from "@/project/with-instance"
+import { resetDatabase } from "../fixture/db"
+import { disposeAllInstances, tmpdir } from "../fixture/fixture"
+import { it } from "../lib/effect"
+import * as Log from "@opencode-ai/core/util/log"
+
+void Log.init({ print: false })
+
+afterEach(async () => {
+  await disposeAllInstances()
+  await resetDatabase()
+})
+
+function pathFor(template: string, params: Record<string, string>) {
+  return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), template)
+}
+
+describe("session diff with missing patch (#26574)", () => {
+  it.live("GET /session/<id>/diff returns 200 when summary_diffs row has no patch", () =>
+    Effect.gen(function* () {
+      const tmp = yield* Effect.acquireRelease(
+        Effect.promise(() => tmpdir({ git: true, config: { formatter: false, lsp: false } })),
+        (t) => Effect.promise(() => t[Symbol.asyncDispose]()),
+      )
+
+      yield* Effect.promise(() =>
+        WithInstance.provide({
+          directory: tmp.path,
+          fn: async () => {
+            const session = await Effect.runPromise(
+              Effect.provide(Session.Service.use((s) => s.create({ title: "missing-patch" })), Session.defaultLayer),
+            )
+
+            // Mimic legacy/imported on-disk shape: a diff entry with no
+            // `patch` text. Pre-fix the typed response encoder rejects
+            // this and returns 400.
+            await Effect.runPromise(
+              Effect.provide(
+                Storage.Service.use((s) =>
+                  s.write(["session_diff", session.id], [{ file: "legacy.txt", additions: 1, deletions: 0 }]),
+                ),
+                Storage.defaultLayer,
+              ),
+            )
+
+            const headers = { "x-opencode-directory": tmp.path }
+            const response = await Server.Default().app.request(
+              pathFor(SessionPaths.diff, { sessionID: session.id }),
+              { headers },
+            )
+            expect(response.status).toBe(200)
+            const body = (await response.json()) as Array<{ file: string; patch?: string; additions: number }>
+            expect(body).toHaveLength(1)
+            expect(body[0]?.file).toBe("legacy.txt")
+            expect(body[0]?.additions).toBe(1)
+            expect(body[0]?.patch).toBeUndefined()
+          },
+        }),
+      )
+    }),
+  )
+})