649 lines · 21.8 KB · TypeScript
RawDownload
import { afterEach, describe, it, assert } from "@effect/vitest";import {  ProviderDriverKind,  ProviderInstanceId,  type ServerProvider,  type ServerProviderUpdateState,} from "@t3tools/contracts";import { ServerProviderUpdateError } from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import { HttpClient, HttpClientResponse } from "effect/unstable/http";import { ChildProcessSpawner } from "effect/unstable/process"; import { ProviderRegistry, type ProviderRegistryShape } from "./Services/ProviderRegistry.ts";import * as ProviderMaintenanceRunner from "./providerMaintenanceRunner.ts";import {  clearLatestProviderVersionCacheForTests,  makeProviderMaintenanceCapabilities,  type ProviderMaintenanceCapabilities,} from "./providerMaintenance.ts";const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError); const CODEX_DRIVER = ProviderDriverKind.make("codex");const CURSOR_DRIVER = ProviderDriverKind.make("cursor");const OPENCODE_DRIVER = ProviderDriverKind.make("opencode");const CODEX_INSTANCE_ID = ProviderInstanceId.make("codex");const CURSOR_INSTANCE_ID = ProviderInstanceId.make("cursor");const OPENCODE_INSTANCE_ID = ProviderInstanceId.make("opencode");const encoder = new TextEncoder(); afterEach(() => {  clearLatestProviderVersionCacheForTests();}); function lifecycleFor(provider: ProviderDriverKind): ProviderMaintenanceCapabilities {  if (provider === CURSOR_DRIVER) {    return makeProviderMaintenanceCapabilities({      provider,      packageName: null,      updateExecutable: "agent",      updateArgs: ["update"],      updateLockKey: "cursor-agent",    });  }  return makeProviderMaintenanceCapabilities({    provider,    packageName: provider === OPENCODE_DRIVER ? "opencode-ai" : "@openai/codex",    updateExecutable: "npm",    updateArgs:      provider === OPENCODE_DRIVER        ? ["install", "-g", "opencode-ai@latest"]        : ["install", "-g", "@openai/codex@latest"],    updateLockKey: "npm-global",  });} const baseProvider: ServerProvider = {  instanceId: CODEX_INSTANCE_ID,  driver: CODEX_DRIVER,  enabled: true,  installed: true,  version: null,  status: "ready",  auth: { status: "authenticated" },  checkedAt: "2026-04-10T00:00:00.000Z",  models: [],  slashCommands: [],  skills: [],}; const baseCursorProvider: ServerProvider = {  ...baseProvider,  instanceId: CURSOR_INSTANCE_ID,  driver: CURSOR_DRIVER,}; const baseOpenCodeProvider: ServerProvider = {  ...baseProvider,  instanceId: OPENCODE_INSTANCE_ID,  driver: OPENCODE_DRIVER,}; const latestVersionHttpClient = (version: string) =>  Layer.succeed(    HttpClient.HttpClient,    HttpClient.make((request) =>      Effect.succeed(        HttpClientResponse.fromWeb(          request,          Response.json({ version }, { headers: { "content-type": "application/json" } }),        ),      ),    ),  ); function mockHandle(result: {  readonly stdout?: string;  readonly stderr?: string;  readonly code?: number;  readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode>;}) {  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(1),    exitCode: result.exitCode ?? Effect.succeed(ChildProcessSpawner.ExitCode(result.code ?? 0)),    isRunning: Effect.succeed(false),    kill: () => Effect.void,    unref: Effect.succeed(Effect.void),    stdin: Sink.drain,    stdout: Stream.make(encoder.encode(result.stdout ?? "")),    stderr: Stream.make(encoder.encode(result.stderr ?? "")),    all: Stream.empty,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,  });} function mockSpawnerLayer(  handler: (    command: string,    args: ReadonlyArray<string>,  ) => {    readonly stdout?: string;    readonly stderr?: string;    readonly code?: number;    readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode>;  },) {  return Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make((command) => {      const childProcess = command as unknown as {        readonly command: string;        readonly args: ReadonlyArray<string>;      };      return Effect.succeed(mockHandle(handler(childProcess.command, childProcess.args)));    }),  );} function makeRegistry(  initialProviders: ServerProvider | ReadonlyArray<ServerProvider> = baseProvider,) {  return Effect.gen(function* () {    const providersRef = yield* Ref.make<ReadonlyArray<ServerProvider>>(      Array.isArray(initialProviders) ? initialProviders : [initialProviders],    );    const updateStatesRef = yield* Ref.make<ReadonlyArray<ServerProviderUpdateState>>([]);     const setProviderMaintenanceActionState = Effect.fn(      "providerMaintenanceRunner.test.setProviderMaintenanceActionState",    )(function* (input: {      readonly instanceId: ProviderInstanceId;      readonly action: "update";      readonly state: ServerProviderUpdateState | null;    }) {      const updateState = input.state;      if (updateState) {        yield* Ref.update(updateStatesRef, (states) => [...states, updateState]);      }      return yield* Ref.updateAndGet(providersRef, (providers) =>        providers.map((candidate) => {          if (candidate.instanceId !== input.instanceId) {            return candidate;          }          if (!updateState) {            const { updateState: _updateState, ...providerWithoutUpdateState } = candidate;            return providerWithoutUpdateState;          }          return {            ...candidate,            updateState,          };        }),      );    });     const registry: ProviderRegistryShape = {      getProviders: Ref.get(providersRef),      refresh: () => Ref.get(providersRef),      refreshInstance: () => Ref.get(providersRef),      getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) =>        Effect.succeed(lifecycleFor(provider)),      setProviderMaintenanceActionState,      streamChanges: Stream.empty,    };     return {      registry,      updateStatesRef,    };  });} const makeTestRunner = (registry: ProviderRegistryShape) =>  Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe(    Effect.provide(      ProviderMaintenanceRunner.layer.pipe(        Layer.provide(Layer.succeed(ProviderRegistry, registry)),      ),    ),  ); describe("providerMaintenanceRunner", () => {  it.effect("runs the allowlisted provider update command and records success", () => {    const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];    return Effect.gen(function* () {      const { registry, updateStatesRef } = yield* makeRegistry(baseCursorProvider);      const updater = yield* makeTestRunner(registry);       const result = yield* updater.updateProvider(CURSOR_DRIVER);      assert.deepStrictEqual(calls, [        {          command: "agent",          args: ["update"],        },      ]);      assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");      assert.deepStrictEqual(        (yield* Ref.get(updateStatesRef)).map((state) => state.status),        ["queued", "running", "succeeded"],      );    }).pipe(      Effect.provide(        Layer.mergeAll(          latestVersionHttpClient("0.0.0"),          mockSpawnerLayer((command, args) => {            calls.push({ command, args });            return { stdout: "updated" };          }),        ),      ),    );  });   it.effect("uses the resolved provider capabilities when choosing the update executable", () => {    const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];    return Effect.gen(function* () {      const { registry } = yield* makeRegistry({        ...baseProvider,        versionAdvisory: {          status: "behind_latest",          currentVersion: "2.0.14",          latestVersion: "2.1.123",          updateCommand: "bun i -g @anthropic-ai/claude-code@latest",          canUpdate: true,          checkedAt: "2026-04-30T12:00:00.000Z",          message: "Update available.",        },      });      const updater = yield* makeTestRunner({        ...registry,        getProviderMaintenanceCapabilitiesForInstance: () =>          Effect.succeed(            makeProviderMaintenanceCapabilities({              provider: CODEX_DRIVER,              packageName: "@openai/codex",              updateExecutable: "bun",              updateArgs: ["i", "-g", "@openai/codex@latest"],              updateLockKey: "bun-global",            }),          ),      });       yield* updater.updateProvider(CODEX_DRIVER);      assert.deepStrictEqual(calls, [        {          command: "bun",          args: ["i", "-g", "@openai/codex@latest"],        },      ]);    }).pipe(      Effect.provide(        Layer.mergeAll(          latestVersionHttpClient("0.0.0"),          mockSpawnerLayer((command, args) => {            calls.push({ command, args });            return { stdout: "updated" };          }),        ),      ),    );  });   it.effect(    "runs update commands through Effect ChildProcess when no test runner is injected",    () => {      const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];      return Effect.gen(function* () {        const { registry } = yield* makeRegistry(baseProvider);        const runner = yield* makeTestRunner(registry);         const result = yield* runner.updateProvider(CODEX_DRIVER);         assert.deepStrictEqual(calls, [          {            command: "npm",            args: ["install", "-g", "@openai/codex@latest"],          },        ]);        assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");      }).pipe(        Effect.provide(          Layer.mergeAll(            latestVersionHttpClient("0.0.0"),            mockSpawnerLayer((command, args) => {              calls.push({ command, args });              return { stdout: "updated" };            }),          ),        ),      );    },  );   it.effect("updates a single provider instance without touching sibling instances", () => {    const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];    return Effect.gen(function* () {      const personalInstanceId = ProviderInstanceId.make("codex_personal");      const workInstanceId = ProviderInstanceId.make("codex_work");      const refreshedInstanceIds: Array<ProviderInstanceId> = [];      const { registry } = yield* makeRegistry([        {          ...baseProvider,          instanceId: personalInstanceId,          version: "0.124.0-alpha.3",        },        {          ...baseProvider,          instanceId: workInstanceId,          version: "0.124.0-alpha.3",        },      ]);      const updater = yield* makeTestRunner({        ...registry,        getProviderMaintenanceCapabilitiesForInstance: (instanceId, provider) =>          Effect.succeed(            makeProviderMaintenanceCapabilities({              provider,              packageName: "@openai/codex-instance-test",              updateExecutable: "vp",              updateArgs: ["i", "-g", "@openai/codex"],              updateLockKey: "vite-plus-global",            }),          ).pipe(            Effect.tap(() => Effect.sync(() => assert.strictEqual(instanceId, personalInstanceId))),          ),        refreshInstance: (instanceId) =>          registry.refreshInstance(instanceId).pipe(            Effect.tap(() =>              Effect.sync(() => {                refreshedInstanceIds.push(instanceId);              }),            ),          ),      });       const result = yield* updater.updateProvider({        provider: CODEX_DRIVER,        instanceId: personalInstanceId,      });       assert.deepStrictEqual(calls, [        {          command: "vp",          args: ["i", "-g", "@openai/codex"],        },      ]);      assert.deepStrictEqual(refreshedInstanceIds, [personalInstanceId]);      assert.strictEqual(result.providers[0]?.instanceId, personalInstanceId);      assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");      assert.strictEqual(result.providers[1]?.instanceId, workInstanceId);      assert.strictEqual(result.providers[1]?.updateState, undefined);    }).pipe(      Effect.provide(        Layer.mergeAll(          latestVersionHttpClient("0.124.0-alpha.3"),          mockSpawnerLayer((command, args) => {            calls.push({ command, args });            return { stdout: "updated" };          }),        ),      ),    );  });   it.effect("records command failure output in provider update state", () =>    Effect.gen(function* () {      const { registry } = yield* makeRegistry();      const updater = yield* makeTestRunner(registry);       const result = yield* updater.updateProvider(CODEX_DRIVER);      const updateState = result.providers[0]?.updateState;       assert.strictEqual(updateState?.status, "failed");      assert.strictEqual(updateState?.message, "Update command exited with code 1.");      assert.include(updateState?.output ?? "", "permission denied");    }).pipe(      Effect.provide(        Layer.mergeAll(          latestVersionHttpClient("0.0.0"),          mockSpawnerLayer(() => ({ stderr: "permission denied", code: 1 })),        ),      ),    ),  );   it.effect(    "marks successful commands as unchanged when the refreshed provider is still outdated",    () =>      Effect.gen(function* () {        const { registry } = yield* makeRegistry({          ...baseProvider,          installed: true,          version: "0.1.0",        });        const updater = yield* makeTestRunner(registry);         const result = yield* updater.updateProvider(CODEX_DRIVER);         assert.strictEqual(result.providers[0]?.updateState?.status, "unchanged");        assert.include(result.providers[0]?.updateState?.message ?? "", "still detects");      }).pipe(        Effect.provide(          Layer.mergeAll(            latestVersionHttpClient("9.9.9"),            mockSpawnerLayer(() => ({ stdout: "updated" })),          ),        ),      ),  );   it.effect("prevents concurrent updates for the same provider", () => {    const startedLatch: { resolve: () => void } = { resolve: () => {} };    const releaseLatch: { resolve: () => void } = { resolve: () => {} };    const started = new Promise<void>((resolve) => {      startedLatch.resolve = resolve;    });    const release = new Promise<void>((resolve) => {      releaseLatch.resolve = resolve;    });    return Effect.gen(function* () {      const { registry } = yield* makeRegistry();      const updater = yield* makeTestRunner(registry);       const first = yield* updater.updateProvider(CODEX_DRIVER).pipe(Effect.forkScoped);      yield* Effect.promise(() => started);       const second = yield* updater.updateProvider(CODEX_DRIVER).pipe(Effect.exit);      assert.strictEqual(Exit.isFailure(second), true);      if (Exit.isFailure(second)) {        const error = Cause.squash(second.cause);        assert.strictEqual(isServerProviderUpdateError(error), true);        if (isServerProviderUpdateError(error)) {          assert.include(error.reason, "already running");        }      }       releaseLatch.resolve();      yield* Fiber.join(first);    }).pipe(      Effect.provide(        Layer.mergeAll(          latestVersionHttpClient("0.0.0"),          mockSpawnerLayer(() => {            startedLatch.resolve();            return {              stdout: "updated",              exitCode: Effect.promise(() => release).pipe(                Effect.as(ChildProcessSpawner.ExitCode(0)),              ),            };          }),        ),      ),    );  });   it.effect("serializes different providers that share the same update lock key", () => {    const firstStartedLatch: { resolve: () => void } = { resolve: () => {} };    const releaseFirstLatch: { resolve: () => void } = { resolve: () => {} };    const firstStarted = new Promise<void>((resolve) => {      firstStartedLatch.resolve = resolve;    });    const releaseFirst = new Promise<void>((resolve) => {      releaseFirstLatch.resolve = resolve;    });    const calls: Array<string> = [];    return Effect.gen(function* () {      const { registry } = yield* makeRegistry([baseProvider, baseOpenCodeProvider]);      const updater = yield* makeTestRunner({        ...registry,        getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) =>          Effect.succeed(

Lines 1–500 of 649.