fix: normalize uncharacterized read failures in the mock Studio gateway

`idempotent()` was fixed to wrap anything `work()` throws, with a documented
rationale: this port's contract is `StudioGatewayError` only, so nothing else
may cross it. Its `read()` sibling was left unwrapped, so an uncharacterized
internal failure on any of the seven read operations escaped as a raw `Error`
and reached UI code written to catch `StudioGatewayError`.

`read()` now applies the same `failureOf` classification. It has no
idempotency ledger, so only the problem half is used. `boundary()` stays
outside the wrap so an aborted request still surfaces as `AbortError`,
exactly as `idempotent()` arranges it -- asserted by the new test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 09:09:36 +09:00
co-authored by Claude Opus 5
parent e4f9f81a3f
commit e2a0e695dd
2 changed files with 63 additions and 1 deletions
@@ -104,7 +104,19 @@ export function createMockStudioGateway(supplied: Partial<MockStudioDependencies
async function boundary(options?: RequestOptions) {
options?.signal?.throwIfAborted(); await Promise.resolve(); options?.signal?.throwIfAborted();
}
async function read<T>(options: RequestOptions | undefined, work: () => T) { await boundary(options); return clone(work()); }
async function read<T>(options: RequestOptions | undefined, work: () => T) {
// `boundary` stays outside the wrap so an abort still surfaces as
// `AbortError`, exactly as `idempotent()` arranges it.
await boundary(options);
try { return clone(work()); }
catch (error) {
// Same reason as `idempotent()`'s catch below: this port's contract is
// `StudioGatewayError` only. A read has no idempotency ledger, so the
// `replayable` half of the classification has nothing to record here --
// only the problem is used.
throw new StudioGatewayError(clone(failureOf(error).problem));
}
}
async function idempotent<T>(operation: string, target: string, request: () => unknown, options: IdempotentOptions, work: () => T): Promise<T> {
await boundary(options);
if (typeof options.idempotencyKey !== "string" || cp(options.idempotencyKey) < 1 || cp(options.idempotencyKey) > 200) throw requestError([{ path: "/idempotencyKey", message: "Idempotency key must be 1-200 characters." }]);