954 lines · 32.1 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { EnvironmentId } from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Latch from "effect/Latch";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as SubscriptionRef from "effect/SubscriptionRef";import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; import {  AVAILABLE_CONNECTION_STATE,  ConnectionBlockedError,  ConnectionTransientError,  PrimaryConnectionTarget,  type PreparedConnection,  type SupervisorConnectionState,} from "../connection/model.ts";import * as EnvironmentRegistry from "../connection/registry.ts";import * as EnvironmentSupervisor from "../connection/supervisor.ts";import { EnvironmentRpcUnavailableError } from "../rpc/client.ts";import type * as RpcSession from "../rpc/session.ts";import {  environmentRpcKey,  createAtomCommandScheduler,  createEnvironmentQueryAtomFamily,  createRuntimeCommand,  scheduleAtomCommandEffect,  executeAtomCommand,  executeAtomQuery,  isAtomCommandInterrupted,  mapAtomCommandResult,  runAtomCommand,  settleAsyncResult,  settlePromise,  squashAtomCommandFailure,} from "./runtime.ts"; const QUERY_ENVIRONMENT = new PrimaryConnectionTarget({  environmentId: EnvironmentId.make("query-environment"),  label: "Query environment",  httpBaseUrl: "https://query.example.test",  wsBaseUrl: "wss://query.example.test",}); const QUERY_RPC_SESSION = {} as RpcSession.RpcSession; class TestQueryError extends Schema.TaggedError<TestQueryError>()("TestQueryError", {  message: Schema.String,}) {} const OFFLINE_QUERY_FAILURE = new ConnectionTransientError({  reason: "transport",  detail: "Relay is unavailable.",}); const BLOCKED_QUERY_FAILURE = new ConnectionBlockedError({  reason: "permission",  detail: "Access denied.",}); function queryConnectionState(  overrides: Partial<SupervisorConnectionState> = {},): SupervisorConnectionState {  return {    ...AVAILABLE_CONNECTION_STATE,    desired: true,    network: "online",    phase: "connected",    attempt: 1,    generation: 1,    ...overrides,  };} const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness")(function* <A, E>(  execute: Effect.Effect<A, E>,) {  const supervisorState = yield* SubscriptionRef.make(queryConnectionState());  const supervisorSession = yield* SubscriptionRef.make(Option.some(QUERY_RPC_SESSION));  const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({    target: QUERY_ENVIRONMENT,    state: supervisorState,    session: supervisorSession,    prepared: yield* SubscriptionRef.make<Option.Option<PreparedConnection>>(Option.none()),    connect: Effect.void,    disconnect: Effect.void,    retryNow: Effect.void,  } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);  const run: EnvironmentRegistry.EnvironmentRegistry["Service"]["run"] = (_environmentId, effect) =>    Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor);  const followStream: EnvironmentRegistry.EnvironmentRegistry["Service"]["followStream"] = (    _environmentId,    stream,  ) => Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor);  const environmentRegistry = EnvironmentRegistry.EnvironmentRegistry.of({    run,    followStream,    stateChanges: () => SubscriptionRef.changes(supervisorState),  } as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"]);  const runtime = Atom.runtime(    Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environmentRegistry),  );  const family = createEnvironmentQueryAtomFamily(runtime, {    label: "test.environment-query",    staleTimeMs: 60_000,    execute: () => execute,  });   return {    atom: family({ environmentId: QUERY_ENVIRONMENT.environmentId, input: undefined }),    supervisorSession,    supervisorState,  };}); const mountEnvironmentQuery = Effect.fn("TestEnvironmentQuery.mount")(function* <A, E>(  atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,) {  const registry = AtomRegistry.make();  const unmount = registry.mount(atom);  yield* Effect.addFinalizer(() =>    Effect.sync(() => {      unmount();      registry.dispose();    }),  );  return registry;}); describe("settleAsyncResult", () => {  it("preserves successful values and typed failures", async () => {    const success = await settleAsyncResult(() => Promise.resolve(Exit.succeed("done")));    expect(AsyncResult.isSuccess(success)).toBe(true);    if (AsyncResult.isSuccess(success)) {      expect(success.value).toBe("done");    }     const expectedFailure = new Error("request failed");    const failure = await settleAsyncResult(() => Promise.resolve(Exit.fail(expectedFailure)));    expect(AsyncResult.isFailure(failure)).toBe(true);    if (AsyncResult.isFailure(failure)) {      expect(Cause.hasDies(failure.cause)).toBe(false);      expect(Cause.squash(failure.cause)).toBe(expectedFailure);    }  });   it("encodes thrown and rejected promises as defects", async () => {    const thrownDefect = new Error("thrown defect");    const thrown = await settleAsyncResult<void, never>(() => {      throw thrownDefect;    });    expect(AsyncResult.isFailure(thrown)).toBe(true);    if (AsyncResult.isFailure(thrown)) {      expect(Cause.hasDies(thrown.cause)).toBe(true);      expect(Cause.squash(thrown.cause)).toBe(thrownDefect);    }     const rejectedDefect = new Error("rejected defect");    const rejected = await settleAsyncResult<void, never>(() => Promise.reject(rejectedDefect));    expect(AsyncResult.isFailure(rejected)).toBe(true);    if (AsyncResult.isFailure(rejected)) {      expect(Cause.hasDies(rejected.cause)).toBe(true);      expect(Cause.squash(rejected.cause)).toBe(rejectedDefect);    }  });}); describe("atom command result helpers", () => {  it("maps successful command values", () => {    const result = mapAtomCommandResult(AsyncResult.success(2), (value) => value * 3);     expect(result._tag).toBe("Success");    if (result._tag === "Success") {      expect(result.value).toBe(6);    }  });   it("preserves failures while mapping", () => {    const result = mapAtomCommandResult(      AsyncResult.failure<number, string>(Cause.fail("nope")),      (value) => value * 3,    );     expect(result._tag).toBe("Failure");    if (result._tag === "Failure") {      expect(Cause.squash(result.cause)).toBe("nope");    }  });   it("distinguishes interruption from other failures", () => {    const interrupted = AsyncResult.failure(Cause.interrupt(1));    const failed = AsyncResult.failure(Cause.fail("nope"));     expect(isAtomCommandInterrupted(interrupted)).toBe(true);    expect(isAtomCommandInterrupted(failed)).toBe(false);    expect(squashAtomCommandFailure(failed)).toBe("nope");  });   it("settles raw promise boundaries as successes or defects", async () => {    const success = await settlePromise(() => Promise.resolve("done"));    expect(success._tag).toBe("Success");     const defect = new Error("raw promise rejected");    const failure = await settlePromise(() => Promise.reject(defect));    expect(failure._tag).toBe("Failure");    if (failure._tag === "Failure") {      expect(Cause.hasDies(failure.cause)).toBe(true);      expect(Cause.squash(failure.cause)).toBe(defect);    }  });   it("reports expected failures and defects through separate policies", async () => {    const warnings: string[] = [];    const errors: string[] = [];    const reporter = {      warn: (message: string) => {        warnings.push(message);      },      error: (message: string) => {        errors.push(message);      },    };     await executeAtomCommand(() => Promise.resolve(Exit.fail("nope")), { label: "save" }, reporter);    await executeAtomCommand(      () => Promise.resolve(Exit.fail("ignored")),      { label: "quiet save", reportFailure: false },      reporter,    );    await executeAtomCommand(      () => Promise.reject(new Error("defect")),      { label: "quiet save", reportFailure: false },      reporter,    );    await executeAtomCommand(      () => Promise.resolve(Exit.interrupt(1)),      { label: "interrupted" },      reporter,    );     expect(warnings).toEqual(["[atom-command] save failed"]);    expect(errors).toEqual(["[atom-command] quiet save defected"]);  });}); describe("environmentRpcKey", () => {  it("isolates subscription state by environment and cwd", () => {    const environmentId = EnvironmentId.make("environment-1");    const originalTarget = {      environmentId,      input: { cwd: "/repo/original" },    };    const nextTarget = {      environmentId,      input: { cwd: "/repo/next" },    };     expect(environmentRpcKey(originalTarget)).not.toBe(environmentRpcKey(nextTarget));    expect(environmentRpcKey(originalTarget)).toBe(environmentRpcKey({ ...originalTarget }));    expect(      environmentRpcKey({        environmentId: EnvironmentId.make("environment-2"),        input: originalTarget.input,      }),    ).not.toBe(environmentRpcKey(originalTarget));  });}); describe("environment query lifecycle", () => {  it.effect(    "retries an interrupted query without exposing a failure during session replacement",    () =>      Effect.scoped(        Effect.gen(function* () {          const firstStarted = Latch.makeUnsafe();          const failFirst = Latch.makeUnsafe();          const firstSettled = Latch.makeUnsafe();          const unavailable = new EnvironmentRpcUnavailableError({            environmentId: QUERY_ENVIRONMENT.environmentId,            message: "Query environment is not connected.",          });          let executions = 0;          const execute = Effect.suspend(() => {            executions += 1;            if (executions > 1) {              return Effect.succeed("recovered");            }            firstStarted.openUnsafe();            return failFirst.await.pipe(              Effect.andThen(Effect.fail(unavailable)),              Effect.ensuring(                Effect.sync(() => {                  firstSettled.openUnsafe();                }),              ),            );          });          const harness = yield* makeEnvironmentQueryHarness(execute);          const registry = AtomRegistry.make();          const observed: Array<AsyncResult.AsyncResult<string, unknown>> = [];          const unsubscribe = registry.subscribe(            harness.atom,            (result) => {              observed.push(result);            },            { immediate: true },          );          yield* Effect.addFinalizer(() =>            Effect.sync(() => {              unsubscribe();              registry.dispose();            }),          );           yield* firstStarted.await;          yield* SubscriptionRef.set(harness.supervisorSession, Option.none());          yield* Effect.yieldNow;          failFirst.openUnsafe();          yield* firstSettled.await;          yield* Effect.yieldNow;           expect(observed.some(AsyncResult.isFailure)).toBe(false);           yield* SubscriptionRef.set(            harness.supervisorState,            queryConnectionState({ phase: "connecting", stage: "preparing" }),          );          yield* Effect.yieldNow;          yield* SubscriptionRef.set(            harness.supervisorState,            queryConnectionState({              phase: "backoff",              stage: null,              lastFailure: new ConnectionTransientError({                reason: "transport",                detail: "Relay session is reconnecting.",              }),              retryAt: 1,            }),          );          yield* Effect.yieldNow;           yield* SubscriptionRef.set(harness.supervisorSession, Option.some(QUERY_RPC_SESSION));          yield* SubscriptionRef.set(            harness.supervisorState,            queryConnectionState({ generation: 2 }),          );          expect(            yield* AtomRegistry.getResult(registry, harness.atom, {              suspendOnWaiting: true,            }),          ).toBe("recovered");        }),      ),  );   it.effect.each([    {      condition: "after a manual disconnect",      state: queryConnectionState({        desired: false,        phase: "available",        stage: null,        attempt: 0,      }),      expectedFailure: null,    },    {      condition: "while the environment is offline",      state: queryConnectionState({        network: "offline",        phase: "offline",        stage: null,        lastFailure: OFFLINE_QUERY_FAILURE,      }),      expectedFailure: OFFLINE_QUERY_FAILURE,    },    {      condition: "when connection recovery is blocked",      state: queryConnectionState({        phase: "blocked",        stage: null,        lastFailure: BLOCKED_QUERY_FAILURE,      }),      expectedFailure: BLOCKED_QUERY_FAILURE,    },  ] as const)("settles as unavailable $condition", ({ state, expectedFailure }) =>    Effect.scoped(      Effect.gen(function* () {        const harness = yield* makeEnvironmentQueryHarness(Effect.succeed("connected"));        const registry = yield* mountEnvironmentQuery(harness.atom);         expect(          yield* AtomRegistry.getResult(registry, harness.atom, {            suspendOnWaiting: true,          }),        ).toBe("connected");         yield* SubscriptionRef.set(harness.supervisorState, state);        yield* Effect.yieldNow;         const result = registry.get(harness.atom);        expect(AsyncResult.isFailure(result)).toBe(true);        expect(result.waiting).toBe(false);        if (AsyncResult.isFailure(result) && expectedFailure !== null) {          expect(Cause.squash(result.cause)).toBe(expectedFailure);        }      }),    ),  );   it.effect("keeps a genuine query failure settled while reconnecting", () =>    Effect.scoped(      Effect.gen(function* () {        const expectedFailure = new TestQueryError({ message: "Query failed." });        const firstStarted = Latch.makeUnsafe();        const failFirst = Latch.makeUnsafe();        const refreshStarted = Latch.makeUnsafe();        const finishRefresh = Latch.makeUnsafe();        let executions = 0;        const execute = Effect.suspend(() => {          executions += 1;          if (executions === 1) {            firstStarted.openUnsafe();            return failFirst.await.pipe(Effect.andThen(Effect.fail(expectedFailure)));          }          refreshStarted.openUnsafe();          return finishRefresh.await.pipe(Effect.as("recovered"));        });        const harness = yield* makeEnvironmentQueryHarness(execute);        const registry = yield* mountEnvironmentQuery(harness.atom);         yield* firstStarted.await;        failFirst.openUnsafe();        const initial = yield* AtomRegistry.getResult(registry, harness.atom, {          suspendOnWaiting: true,        }).pipe(Effect.exit);        expect(Exit.isFailure(initial)).toBe(true);        if (Exit.isFailure(initial)) {          expect(Cause.squash(initial.cause)).toBe(expectedFailure);        }         yield* SubscriptionRef.set(          harness.supervisorState,          queryConnectionState({ phase: "connecting", stage: "opening" }),        );        yield* Effect.yieldNow;         const refreshing = registry.get(harness.atom);        expect(AsyncResult.isFailure(refreshing)).toBe(true);        expect(refreshing.waiting).toBe(true);        if (AsyncResult.isFailure(refreshing)) {          expect(Cause.squash(refreshing.cause)).toBe(expectedFailure);        }         yield* SubscriptionRef.set(          harness.supervisorState,          queryConnectionState({ generation: 2 }),        );        yield* refreshStarted.await;        finishRefresh.openUnsafe();        expect(          yield* AtomRegistry.getResult(registry, harness.atom, {            suspendOnWaiting: true,          }),        ).toBe("recovered");      }),    ),  );   it.effect("retains the last successful value while reconnecting", () =>    Effect.scoped(      Effect.gen(function* () {        const refreshStarted = Latch.makeUnsafe();        const finishRefresh = Latch.makeUnsafe();        let executions = 0;        const execute = Effect.suspend(() => {          executions += 1;          if (executions === 1) {            return Effect.succeed("cached");          }          refreshStarted.openUnsafe();          return finishRefresh.await.pipe(Effect.as("updated"));        });        const harness = yield* makeEnvironmentQueryHarness(execute);        const registry = yield* mountEnvironmentQuery(harness.atom);         expect(          yield* AtomRegistry.getResult(registry, harness.atom, {            suspendOnWaiting: true,          }),        ).toBe("cached"); 

Lines 1–500 of 954.