Skip to content

Commit e9a29e4

Browse files
authored
fix(storage): type not found errors (anomalyco#27265)
1 parent d93a064 commit e9a29e4

6 files changed

Lines changed: 51 additions & 21 deletions

File tree

packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import type { NotFoundError as StorageNotFoundError } from "@/storage/storage"
22
import { Effect } from "effect"
33
import * as ApiError from "../errors"
44

5-
type StorageNotFound = InstanceType<typeof StorageNotFoundError>
6-
7-
export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFound, R>) {
8-
return self.pipe(Effect.mapError((error) => ApiError.notFound(error.data.message)))
5+
export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFoundError, R>) {
6+
return self.pipe(Effect.mapError((error) => ApiError.notFound(error.message)))
97
}

packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
2424
const error = defect.defect
2525
log.error("failed", { error, cause: Cause.pretty(cause) })
2626

27+
if (error instanceof NotFoundError) {
28+
return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 404 }))
29+
}
30+
2731
if (error instanceof NamedError) {
2832
return Effect.succeed(
2933
HttpServerResponse.jsonUnsafe(error.toObject(), {
3034
status: iife(() => {
31-
if (error instanceof NotFoundError) return 404
3235
if (error instanceof Provider.ModelNotFoundError) return 400
3336
if (error.name === "ProviderAuthValidationFailed") return 400
3437
if (error.name.startsWith("Worktree")) return 400

packages/opencode/src/session/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export class BusyError extends Error {
448448
}
449449
}
450450

451-
export type NotFound = InstanceType<typeof NotFoundError>
451+
export type NotFound = NotFoundError
452452

453453
export interface Interface {
454454
readonly list: (input?: ListInput) => Effect.Effect<Info[]>

packages/opencode/src/storage/storage.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as Log from "@opencode-ai/core/util/log"
22
import path from "path"
33
import { Global } from "@opencode-ai/core/global"
4-
import { NamedError } from "@opencode-ai/core/util/error"
54
import { AppFileSystem } from "@opencode-ai/core/filesystem"
65
import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
76
import { NonNegativeInt } from "@opencode-ai/core/schema"
@@ -15,11 +14,22 @@ type Migration = (
1514
git: Git.Interface,
1615
) => Effect.Effect<void, AppFileSystem.Error>
1716

18-
export const NotFoundError = NamedError.create("NotFoundError", {
17+
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {
1918
message: Schema.String,
20-
})
19+
}) {
20+
static isInstance(input: unknown): input is NotFoundError {
21+
return input instanceof NotFoundError
22+
}
23+
24+
toObject() {
25+
return {
26+
name: "NotFoundError" as const,
27+
data: { message: this.message },
28+
}
29+
}
30+
}
2131

22-
export type Error = AppFileSystem.Error | InstanceType<typeof NotFoundError>
32+
export type Error = AppFileSystem.Error | NotFoundError
2333

2434
const RootFile = Schema.Struct({
2535
path: Schema.optional(
@@ -245,7 +255,7 @@ export const layer = Layer.effect(
245255
}),
246256
)
247257

248-
const fail = (target: string): Effect.Effect<never, InstanceType<typeof NotFoundError>> =>
258+
const fail = (target: string): Effect.Effect<never, NotFoundError> =>
249259
Effect.fail(new NotFoundError({ message: `Resource not found: ${target}` }))
250260

251261
const wrap = <A>(target: string, body: Effect.Effect<A, AppFileSystem.Error>) =>

packages/opencode/test/session/messages-pagination.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@ import { Session as SessionNs } from "@/session/session"
44
import { MessageV2 } from "../../src/session/message-v2"
55
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
66
import { ModelID, ProviderID } from "../../src/provider/schema"
7+
import { NotFoundError } from "@/storage/storage"
78
import * as Log from "@opencode-ai/core/util/log"
89
import { testEffect } from "../lib/effect"
910

1011
void Log.init({ print: false })
1112

1213
const it = testEffect(SessionNs.defaultLayer)
1314

15+
function expectNotFound(fn: () => unknown, message: string) {
16+
let thrown: unknown
17+
try {
18+
fn()
19+
} catch (error) {
20+
thrown = error
21+
}
22+
expect(thrown).toBeInstanceOf(NotFoundError)
23+
if (thrown instanceof NotFoundError) {
24+
expect(thrown._tag).toBe("NotFoundError")
25+
expect(thrown.message).toBe(message)
26+
}
27+
}
28+
1429
const withSession = <A, E, R>(
1530
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
1631
) =>
@@ -186,7 +201,7 @@ describe("MessageV2.page", () => {
186201
it.instance("throws NotFoundError for non-existent session", () =>
187202
Effect.gen(function* () {
188203
const fake = "non-existent-session" as SessionID
189-
expect(() => MessageV2.page({ sessionID: fake, limit: 10 })).toThrow("NotFoundError")
204+
expectNotFound(() => MessageV2.page({ sessionID: fake, limit: 10 }), `Session not found: ${fake}`)
190205
}),
191206
)
192207

@@ -471,7 +486,8 @@ describe("MessageV2.get", () => {
471486
it.instance("throws NotFoundError for non-existent message", () =>
472487
withSession(({ sessionID }) =>
473488
Effect.gen(function* () {
474-
expect(() => MessageV2.get({ sessionID, messageID: MessageID.ascending() })).toThrow("NotFoundError")
489+
const messageID = MessageID.ascending()
490+
expectNotFound(() => MessageV2.get({ sessionID, messageID }), `Message not found: ${messageID}`)
475491
}),
476492
),
477493
)
@@ -483,7 +499,7 @@ describe("MessageV2.get", () => {
483499
const b = yield* session.create({})
484500
const [id] = yield* fill(a.id, 1)
485501

486-
expect(() => MessageV2.get({ sessionID: b.id, messageID: id })).toThrow("NotFoundError")
502+
expectNotFound(() => MessageV2.get({ sessionID: b.id, messageID: id }), `Message not found: ${id}`)
487503
const result = MessageV2.get({ sessionID: a.id, messageID: id })
488504
expect(result.info.id).toBe(id)
489505

packages/opencode/test/storage/storage.test.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,23 @@ describe("Storage", () => {
7474
it.live("maps missing reads to NotFoundError", () =>
7575
Effect.gen(function* () {
7676
const { root, svc } = yield* scope()
77-
const exit = yield* svc.read([...root, "missing", "value"]).pipe(Effect.exit)
78-
expect(Exit.isFailure(exit)).toBe(true)
77+
const error = yield* Effect.flip(svc.read([...root, "missing", "value"]))
78+
expect(error).toBeInstanceOf(Storage.NotFoundError)
79+
expect(error._tag).toBe("NotFoundError")
80+
expect(error.message).toContain(path.join(...root, "missing", "value") + ".json")
7981
}),
8082
)
8183

8284
it.live("update on missing key throws NotFoundError", () =>
8385
Effect.gen(function* () {
8486
const { root, svc } = yield* scope()
85-
const exit = yield* svc
86-
.update<{ value: number }>([...root, "missing", "key"], (draft) => {
87+
const error = yield* Effect.flip(
88+
svc.update<{ value: number }>([...root, "missing", "key"], (draft) => {
8789
draft.value += 1
88-
})
89-
.pipe(Effect.exit)
90-
expect(Exit.isFailure(exit)).toBe(true)
90+
}),
91+
)
92+
expect(error).toBeInstanceOf(Storage.NotFoundError)
93+
expect(error._tag).toBe("NotFoundError")
9194
}),
9295
)
9396

0 commit comments

Comments
 (0)