Skip to content

Commit 8ceaf90

Browse files
Nick Ficanoclaude
andcommitted
chore(effect-cleanup): fix replay stub + dedupe unbound errors
- server-effect: replace throwing replay() stub in unconfiguredEventLogLayer with Stream.fail(...).pipe(Stream.orDie) so the failure surfaces through the Stream channel like the real ops, and extract the repeated "not provided" message to a constant. - client-effect: dedupe the inline TaggedTransportError construction for submit/cancel via a shared UNBOUND_MESSAGE + unboundError() helper. - runtime tests: replace `_tag === "Some"` discriminator checks with Option.isSome (plus an explicit assertion so a None failure can't silently skip the inner expect), and the runHead Option result with Option.isNone narrowing in server-effect.test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 79b3443 commit 8ceaf90

6 files changed

Lines changed: 37 additions & 52 deletions

File tree

packages/client/src/client-effect.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,26 +99,19 @@ export interface ARCPClientServiceShape {
9999
// `ARCPClient | null`). Without the explicit annotation Effect.Service infers
100100
// `client: null` and `ARCPClientService.make({ client })` rejects a non-null
101101
// client.
102+
const UNBOUND_MESSAGE =
103+
"ARCPClientService is not bound; use ARCPClientLayer or makeARCPClientRuntime";
104+
105+
const unboundError = (): TaggedTransportError =>
106+
new TaggedTransportError({
107+
cause: new Error(UNBOUND_MESSAGE),
108+
kind: "send",
109+
});
110+
102111
const UNBOUND: ARCPClientServiceShape = {
103112
client: null,
104-
submit: () =>
105-
Effect.fail(
106-
new TaggedTransportError({
107-
cause: new Error(
108-
"ARCPClientService is not bound; use ARCPClientLayer or makeARCPClientRuntime",
109-
),
110-
kind: "send",
111-
}),
112-
),
113-
cancel: () =>
114-
Effect.fail(
115-
new TaggedTransportError({
116-
cause: new Error(
117-
"ARCPClientService is not bound; use ARCPClientLayer or makeARCPClientRuntime",
118-
),
119-
kind: "send",
120-
}),
121-
),
113+
submit: () => Effect.fail(unboundError()),
114+
cancel: () => Effect.fail(unboundError()),
122115
close: Effect.void,
123116
};
124117

packages/runtime/src/server-effect.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -144,25 +144,21 @@ function eventLogServiceLayer(
144144
return opts.eventLogLayer ?? unconfiguredEventLogLayer();
145145
}
146146

147+
const UNCONFIGURED_EVENT_LOG = "EventLogService not provided to ARCPRuntimeLayer";
148+
147149
function unconfiguredEventLogLayer(): Layer.Layer<EventLogService> {
148150
// The default-provided ops on `EventLogService` already fail-fast on every
149151
// method. We re-wrap them as an explicit layer here so the runtime layer
150152
// composition has a concrete `Layer<EventLogService>` to merge.
151153
const unconfigured: EventLogEffect = {
152-
append: () => Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
153-
appendBatch: () =>
154-
Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
155-
replay: () => {
156-
throw new Error("EventLogService not provided to ARCPRuntimeLayer");
157-
},
158-
readSince: () =>
159-
Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
160-
readSinceSeq: () =>
161-
Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
162-
count: () => Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
163-
getById: () =>
164-
Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
165-
query: () => Effect.die("EventLogService not provided to ARCPRuntimeLayer"),
154+
append: () => Effect.die(UNCONFIGURED_EVENT_LOG),
155+
appendBatch: () => Effect.die(UNCONFIGURED_EVENT_LOG),
156+
replay: () => Stream.fail(UNCONFIGURED_EVENT_LOG).pipe(Stream.orDie),
157+
readSince: () => Effect.die(UNCONFIGURED_EVENT_LOG),
158+
readSinceSeq: () => Effect.die(UNCONFIGURED_EVENT_LOG),
159+
count: () => Effect.die(UNCONFIGURED_EVENT_LOG),
160+
getById: () => Effect.die(UNCONFIGURED_EVENT_LOG),
161+
query: () => Effect.die(UNCONFIGURED_EVENT_LOG),
166162
};
167163
return Layer.succeed(EventLogService, EventLogService.make(unconfigured));
168164
}

packages/runtime/test/job-effect.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
JobResultPayload,
77
JobStateName,
88
} from "@arcp/core/messages";
9-
import { Cause, Effect, Exit, Fiber } from "effect";
9+
import { Cause, Effect, Exit, Fiber, Option } from "effect";
1010
import { describe, expect, it } from "vitest";
1111

1212
import {
@@ -135,7 +135,8 @@ describe("JobService (Effect)", () => {
135135
expect(Exit.isFailure(exit)).toBe(true);
136136
if (Exit.isFailure(exit)) {
137137
const failure = Cause.failureOption(exit.cause);
138-
if (failure._tag === "Some") {
138+
expect(Option.isSome(failure)).toBe(true);
139+
if (Option.isSome(failure)) {
139140
expect(failure.value).toBeInstanceOf(TaggedInvalidRequest);
140141
}
141142
}
@@ -238,8 +239,8 @@ describe("watchdogEffect (Effect)", () => {
238239
expect(Exit.isFailure(exit)).toBe(true);
239240
if (Exit.isFailure(exit)) {
240241
const failure = Cause.failureOption(exit.cause);
241-
expect(failure._tag).toBe("Some");
242-
if (failure._tag === "Some") {
242+
expect(Option.isSome(failure)).toBe(true);
243+
if (Option.isSome(failure)) {
243244
expect(failure.value).toBeInstanceOf(TaggedHeartbeatLost);
244245
}
245246
}

packages/runtime/test/lease-effect.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TaggedInvalidRequest, TaggedPermissionDenied } from "@arcp/core";
22
import type { Lease } from "@arcp/core/messages";
3-
import { Cause, Effect, Exit } from "effect";
3+
import { Cause, Effect, Exit, Option } from "effect";
44
import { describe, expect, it } from "vitest";
55

66
import {
@@ -34,8 +34,8 @@ describe("validateLeaseOpEffect", () => {
3434
expect(Exit.isFailure(exit)).toBe(true);
3535
if (Exit.isFailure(exit)) {
3636
const failure = Cause.failureOption(exit.cause);
37-
expect(failure._tag).toBe("Some");
38-
if (failure._tag === "Some") {
37+
expect(Option.isSome(failure)).toBe(true);
38+
if (Option.isSome(failure)) {
3939
expect(failure.value).toBeInstanceOf(TaggedPermissionDenied);
4040
}
4141
}
@@ -84,7 +84,8 @@ describe("validateLeaseConstraintsEffect", () => {
8484
expect(Exit.isFailure(exit)).toBe(true);
8585
if (Exit.isFailure(exit)) {
8686
const failure = Cause.failureOption(exit.cause);
87-
if (failure._tag === "Some") {
87+
expect(Option.isSome(failure)).toBe(true);
88+
if (Option.isSome(failure)) {
8889
expect(failure.value).toBeInstanceOf(TaggedInvalidRequest);
8990
}
9091
}

packages/runtime/test/server-effect.test.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
silentLogger,
55
type TransportEffect,
66
} from "@arcp/core";
7-
import { Effect, Fiber, Stream } from "effect";
7+
import { Effect, Fiber, Option, Stream } from "effect";
88
import { describe, expect, it, vi } from "vitest";
99

1010
import { AgentRegistryService } from "../src/agent-registry.js";
@@ -197,14 +197,8 @@ async function collectFirstFrame(
197197
);
198198
const result = await Effect.runPromise(program);
199199
// `runHead` resolves to an Option; an empty stream is a test failure.
200-
const opt = result as { _tag: "Some" | "None"; value?: Record<string, unknown> };
201-
if (opt._tag === "None") {
200+
if (Option.isNone(result)) {
202201
throw new Error(`stream ended without a "${type}" frame`);
203202
}
204-
// value is guaranteed defined when _tag is Some — narrow defensively.
205-
const frame = opt.value;
206-
if (frame === undefined) {
207-
throw new Error(`stream resolved Some without a frame value`);
208-
}
209-
return frame;
203+
return result.value;
210204
}

packages/runtime/test/session-effect.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TaggedUnauthenticated } from "@arcp/core";
33
import type { BaseEnvelope } from "@arcp/core/envelope";
44
import { UnauthenticatedError } from "@arcp/core/errors";
55
import type { JobErrorPayload } from "@arcp/core/messages";
6-
import { Cause, Effect, Exit } from "effect";
6+
import { Cause, Effect, Exit, Option } from "effect";
77
import { describe, expect, it } from "vitest";
88

99
import {
@@ -93,8 +93,8 @@ describe("SessionContextService (Effect)", () => {
9393
expect(Exit.isFailure(exit)).toBe(true);
9494
if (Exit.isFailure(exit)) {
9595
const failure = Cause.failureOption(exit.cause);
96-
expect(failure._tag).toBe("Some");
97-
if (failure._tag === "Some") {
96+
expect(Option.isSome(failure)).toBe(true);
97+
if (Option.isSome(failure)) {
9898
expect(failure.value).toBeInstanceOf(TaggedUnauthenticated);
9999
}
100100
}

0 commit comments

Comments
 (0)