|
5 | 5 | // negative. The pre-fix `safe()` clamp only guarded against non-finite. The |
6 | 6 | // strict `NonNegativeInt` schema then made every load of the message list |
7 | 7 | // fail to encode, killing Desktop boot for every user with such a row. |
8 | | -import { afterEach, describe, expect } from "bun:test" |
| 8 | +import { describe, expect } from "bun:test" |
9 | 9 | import { Effect } from "effect" |
10 | 10 | import { eq } from "drizzle-orm" |
11 | 11 | import { ModelID, ProviderID } from "../../src/provider/schema" |
12 | | -import { WithInstance } from "../../src/project/with-instance" |
13 | 12 | import { Server } from "../../src/server/server" |
14 | 13 | import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" |
15 | 14 | import { Session } from "@/session/session" |
16 | 15 | import { MessageID, PartID } from "../../src/session/schema" |
17 | 16 | import * as Database from "@/storage/db" |
18 | 17 | import { PartTable } from "@/session/session.sql" |
19 | 18 | import { resetDatabase } from "../fixture/db" |
20 | | -import { disposeAllInstances, tmpdir } from "../fixture/fixture" |
21 | | -import { it } from "../lib/effect" |
| 19 | +import { TestInstance } from "../fixture/fixture" |
| 20 | +import { testEffect } from "../lib/effect" |
22 | 21 |
|
23 | | -afterEach(async () => { |
24 | | - await disposeAllInstances() |
25 | | - await resetDatabase() |
26 | | -}) |
| 22 | +const it = testEffect(Session.defaultLayer) |
27 | 23 |
|
28 | | -function seedNegativeTokenSession(directory: string) { |
29 | | - return Effect.promise(async () => |
30 | | - WithInstance.provide({ |
31 | | - directory, |
32 | | - fn: () => |
33 | | - Effect.runPromise( |
34 | | - Effect.gen(function* () { |
35 | | - const session = yield* Session.Service |
36 | | - const info = yield* session.create({}) |
37 | | - const message = yield* session.updateMessage({ |
38 | | - id: MessageID.ascending(), |
39 | | - role: "user", |
40 | | - sessionID: info.id, |
41 | | - agent: "build", |
42 | | - model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, |
43 | | - time: { created: Date.now() }, |
44 | | - }) |
45 | | - const partID = PartID.ascending() |
46 | | - yield* session.updatePart({ |
47 | | - id: partID, |
48 | | - sessionID: info.id, |
49 | | - messageID: message.id, |
50 | | - type: "step-finish", |
51 | | - reason: "stop", |
52 | | - cost: 0, |
53 | | - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, |
54 | | - }) |
| 24 | +function seedNegativeTokenSession() { |
| 25 | + return Effect.gen(function* () { |
| 26 | + const session = yield* Session.Service |
| 27 | + const info = yield* session.create({}) |
| 28 | + const message = yield* session.updateMessage({ |
| 29 | + id: MessageID.ascending(), |
| 30 | + role: "user", |
| 31 | + sessionID: info.id, |
| 32 | + agent: "build", |
| 33 | + model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, |
| 34 | + time: { created: Date.now() }, |
| 35 | + }) |
| 36 | + const partID = PartID.ascending() |
| 37 | + yield* session.updatePart({ |
| 38 | + id: partID, |
| 39 | + sessionID: info.id, |
| 40 | + messageID: message.id, |
| 41 | + type: "step-finish", |
| 42 | + reason: "stop", |
| 43 | + cost: 0, |
| 44 | + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, |
| 45 | + }) |
55 | 46 |
|
56 | | - // Bypass the schema with a direct SQL update to install the |
57 | | - // negative `output` value we want to test loading. |
58 | | - Database.use((db) => |
59 | | - db |
60 | | - .update(PartTable) |
61 | | - .set({ |
62 | | - data: { |
63 | | - type: "step-finish", |
64 | | - reason: "stop", |
65 | | - cost: 0, |
66 | | - tokens: { input: 0, output: -42, reasoning: 0, cache: { read: 0, write: 0 } }, |
67 | | - } as never, |
68 | | - }) |
69 | | - .where(eq(PartTable.id, partID)) |
70 | | - .run(), |
71 | | - ) |
| 47 | + // Bypass the schema with a direct SQL update to install the |
| 48 | + // negative `output` value we want to test loading. |
| 49 | + Database.use((db) => |
| 50 | + db |
| 51 | + .update(PartTable) |
| 52 | + .set({ |
| 53 | + data: { |
| 54 | + type: "step-finish", |
| 55 | + reason: "stop", |
| 56 | + cost: 0, |
| 57 | + tokens: { input: 0, output: -42, reasoning: 0, cache: { read: 0, write: 0 } }, |
| 58 | + } as never, |
| 59 | + }) |
| 60 | + .where(eq(PartTable.id, partID)) |
| 61 | + .run(), |
| 62 | + ) |
72 | 63 |
|
73 | | - return info.id |
74 | | - }).pipe(Effect.provide(Session.defaultLayer)), |
75 | | - ), |
76 | | - }), |
77 | | - ) |
| 64 | + return info.id |
| 65 | + }) |
78 | 66 | } |
79 | 67 |
|
80 | 68 | describe("messages endpoint tolerates legacy negative token counts", () => { |
81 | | - it.live( |
| 69 | + it.instance( |
82 | 70 | "returns 200 even when a step-finish part has tokens.output < 0", |
83 | | - Effect.acquireRelease( |
84 | | - Effect.promise(() => tmpdir({ config: { formatter: false, lsp: false } })), |
85 | | - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), |
86 | | - ).pipe( |
87 | | - Effect.flatMap((tmp) => |
88 | | - Effect.gen(function* () { |
89 | | - const sessionID = yield* seedNegativeTokenSession(tmp.path) |
90 | | - const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(tmp.path)}` |
91 | | - const res = yield* Effect.promise(async () => Server.Default().app.request(url)) |
92 | | - expect(res.status, "messages endpoint 400'd on legacy negative tokens").not.toBe(400) |
93 | | - }), |
94 | | - ), |
95 | | - ), |
| 71 | + Effect.gen(function* () { |
| 72 | + yield* Effect.addFinalizer(() => Effect.promise(() => resetDatabase())) |
| 73 | + const test = yield* TestInstance |
| 74 | + const sessionID = yield* seedNegativeTokenSession() |
| 75 | + const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(test.directory)}` |
| 76 | + const res = yield* Effect.promise(async () => Server.Default().app.request(url)) |
| 77 | + expect(res.status, "messages endpoint 400'd on legacy negative tokens").not.toBe(400) |
| 78 | + }), |
| 79 | + { git: true, config: { formatter: false, lsp: false } }, |
96 | 80 | ) |
97 | 81 | }) |
0 commit comments