session-diff-missing-patch.test.ts 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /**
  2. * Regression test for the same bug class as #26574 (sibling of #26566 and
  3. * #26553). The Desktop app calls GET /session/<id>/diff; before #26574
  4. * the response was Schema-encoded against `Snapshot.FileDiff` with
  5. * `patch: Schema.String` (required), so any session whose stored
  6. * `summary_diffs` had a row without `patch` returned HTTP 400 and the
  7. * session never loaded. Legacy session-level diffs are no longer surfaced,
  8. * but the endpoint remains compatible and must still return successfully.
  9. *
  10. * This test inserts a session row with a missing-patch diff entry and
  11. * asserts that GET /session/<id>/diff returns 200 with empty data.
  12. */
  13. import { afterEach, describe, expect } from "bun:test"
  14. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  15. import { Effect, Layer } from "effect"
  16. import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
  17. import { Session } from "@/session/session"
  18. import { Storage } from "@/storage/storage"
  19. import { SessionV1 } from "@kirincode-ai/core/v1/session"
  20. import { MessageID } from "@/session/schema"
  21. import { ProviderV2 } from "@kirincode-ai/core/provider"
  22. import { ModelV2 } from "@kirincode-ai/core/model"
  23. import { resetDatabase } from "../fixture/db"
  24. import { disposeAllInstances, TestInstance } from "../fixture/fixture"
  25. import { testEffect } from "../lib/effect"
  26. import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
  27. const it = testEffect(Layer.mergeAll(LayerNode.compile(LayerNode.group([Session.node, Storage.node])), httpApiLayer))
  28. afterEach(async () => {
  29. await disposeAllInstances()
  30. await resetDatabase()
  31. })
  32. function pathFor(template: string, params: Record<string, string>) {
  33. return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), template)
  34. }
  35. const withSession = (input?: Parameters<Session.Interface["create"]>[0]) =>
  36. Effect.acquireRelease(Session.use.create(input), (created) => Session.use.remove(created.id).pipe(Effect.ignore))
  37. describe("session diff with missing patch (#26574)", () => {
  38. it.instance(
  39. "GET /session/<id>/diff ignores legacy session-level diff storage",
  40. () =>
  41. Effect.gen(function* () {
  42. const test = yield* TestInstance
  43. const session = yield* withSession({ title: "missing-patch" })
  44. // Mimic legacy/imported on-disk shape: a diff entry with no
  45. // `patch` text. Pre-fix the typed response encoder rejects
  46. // this and returns 400.
  47. yield* Storage.Service.use((storage) =>
  48. storage.write(["session_diff", session.id], [{ file: "legacy.txt", additions: 1, deletions: 0 }]),
  49. )
  50. const response = yield* requestInDirectory(
  51. pathFor(SessionPaths.diff, { sessionID: session.id }),
  52. test.directory,
  53. )
  54. expect(response.status).toBe(200)
  55. expect(yield* response.json).toEqual([])
  56. }),
  57. { git: true, config: { formatter: false, lsp: false } },
  58. )
  59. it.instance(
  60. "GET /session/<id>/diff returns requested turn diffs",
  61. () =>
  62. Effect.gen(function* () {
  63. const test = yield* TestInstance
  64. const session = yield* withSession({ title: "turn-diff" })
  65. const messageID = MessageID.ascending()
  66. yield* Session.use.updateMessage({
  67. id: messageID,
  68. sessionID: session.id,
  69. role: "user",
  70. time: { created: Date.now() },
  71. agent: "build",
  72. model: { providerID: ProviderV2.ID.make("test"), modelID: ModelV2.ID.make("model") },
  73. summary: {
  74. diffs: [{ file: "turn.ts", additions: 1, deletions: 0, status: "modified" }],
  75. },
  76. } satisfies SessionV1.User)
  77. const response = yield* requestInDirectory(
  78. `${pathFor(SessionPaths.diff, { sessionID: session.id })}?messageID=${messageID}`,
  79. test.directory,
  80. )
  81. expect(response.status).toBe(200)
  82. expect(yield* response.json).toEqual([{ file: "turn.ts", additions: 1, deletions: 0, status: "modified" }])
  83. }),
  84. { git: true, config: { formatter: false, lsp: false } },
  85. )
  86. })