diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 15:34:59 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 15:34:59 +0900 |
| commit | 350b9b8e247bb1c24f49a884fdade18e44b115eb (patch) | |
| tree | 502df8f103d53f1089eeee7278865d73077f9edc /packages/exec-backend/src/extension.test.ts | |
| parent | 3647acfb7f078b2f035dd325f6959980c5b46c9a (diff) | |
| download | dispatch-350b9b8e247bb1c24f49a884fdade18e44b115eb.tar.gz dispatch-350b9b8e247bb1c24f49a884fdade18e44b115eb.zip | |
feat(ssh): wave 5a — exec-backend remote-backend factory handle
exec-backend declares remoteExecBackendFactoryHandle (a consumer-defined
ServiceHandle<(computerId) => ExecBackend>) that the ssh package will provide
(standard→core layering). The resolver's computerId-set branch now lazy-looks-up
this factory (at tool-execute time, runtime) and calls it; if ssh isn't loaded,
getService throws → a clear 'SSH remote execution is not configured' error. The
computerId-undefined (local) branch is byte-identical to before.
This is the seam wave 5b (the ssh package) plugs into. +tests for both branches.
Verified: tsc -b EXIT 0, biome clean. No merge or push.
Diffstat (limited to 'packages/exec-backend/src/extension.test.ts')
| -rw-r--r-- | packages/exec-backend/src/extension.test.ts | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/packages/exec-backend/src/extension.test.ts b/packages/exec-backend/src/extension.test.ts new file mode 100644 index 0000000..57161a5 --- /dev/null +++ b/packages/exec-backend/src/extension.test.ts @@ -0,0 +1,121 @@ +import type { HostAPI, ServiceHandle } from "@dispatch/kernel"; +import { describe, expect, it } from "vitest"; +import type { ExecBackend } from "./backend.js"; +import { createExecBackendExtension } from "./extension.js"; +import { localExecBackend } from "./local.js"; +import { execBackendHandle, remoteExecBackendFactoryHandle } from "./service.js"; + +/** + * Resolver tests — pure core, zero internal mocks. + * + * The resolver's ONLY external dependency is `host.getService` (the service + * registry — the outermost edge). We inject a minimal fake host that mirrors + * the real `bus.getService` contract: returns the provided impl, or throws when + * nothing provided the handle. No `vi.mock("@dispatch/*")` — the resolver + + * handles + local backend under test are all real. + * + * Three cases (matching the task spec): + * 1. `computerId` undefined → `localExecBackend` (byte-identical local path). + * 2. `computerId` set + factory provided → the factory's backend. + * 3. `computerId` set + factory NOT provided (ssh not loaded) → a clear + * "SSH remote execution is not configured" error, not a crash. + */ + +/** + * A minimal fake host exposing only the service-registry surface the resolver + * touches (`getService`/`provideService`). Throws on a missing service exactly + * like the real `bus.getService`, so the resolver's try/catch path is exercised + * against behavior-equivalent input. + */ +function createFakeHost(services: Map<string, unknown>): HostAPI { + const api = { + provideService<T>(handle: ServiceHandle<T>, impl: T): void { + services.set(handle.id, impl); + }, + getService<T>(handle: ServiceHandle<T>): T { + const impl = services.get(handle.id); + if (impl === undefined) { + throw new Error( + `Service "${handle.id}" has no provider. Call provideService before getService.`, + ); + } + return impl as T; + }, + }; + // The resolver only calls getService; the rest of HostAPI is unused here. + return api as unknown as HostAPI; +} + +/** A fake remote backend — identifiable so we can assert it's the one returned. */ +function createFakeRemoteBackend(marker: string): ExecBackend { + const fail = (): never => { + throw new Error(`fake remote backend (${marker}) should not be called in this test`); + }; + return { + spawn: fail, + readFile: fail, + writeFile: fail, + stat: fail, + readdir: fail, + exists: fail, + }; +} + +describe("ExecBackend resolver", () => { + it("returns localExecBackend for computerId === undefined (local path unchanged)", () => { + const services = new Map<string, unknown>(); + const host = createFakeHost(services); + + // Activate the extension so it registers its resolver, then retrieve it. + createExecBackendExtension().activate(host); + const resolver = host.getService(execBackendHandle); + + expect(resolver(undefined)).toBe(localExecBackend); + expect(resolver()).toBe(localExecBackend); + }); + + it("returns the factory's backend for a set computerId when the factory is provided", () => { + const services = new Map<string, unknown>(); + const host = createFakeHost(services); + + // The `ssh` extension (not built yet) would do this: + const remoteBackend = createFakeRemoteBackend("ssh-alias"); + const factory = (computerId: string): ExecBackend => { + // Confirm the alias is threaded through to the factory. + expect(computerId).toBe("ssh-alias"); + return remoteBackend; + }; + host.provideService(remoteExecBackendFactoryHandle, factory); + + createExecBackendExtension().activate(host); + const resolver = host.getService(execBackendHandle); + + expect(resolver("ssh-alias")).toBe(remoteBackend); + }); + + it("throws a clear 'not configured' error when the factory is NOT provided (ssh not loaded)", () => { + const services = new Map<string, unknown>(); + const host = createFakeHost(services); + + // No remoteExecBackendFactoryHandle provided → simulates ssh not loaded. + createExecBackendExtension().activate(host); + const resolver = host.getService(execBackendHandle); + + // Not a crash: a clear, actionable error mentioning computerId + ssh. + expect(() => resolver("some-host")).toThrow(/SSH remote execution is not configured/); + expect(() => resolver("some-host")).toThrow(/ssh extension is not loaded/); + expect(() => resolver("some-host")).toThrow(/some-host/); + }); + + it("local path is unaffected by whether the factory is provided", () => { + // Even with a factory present, computerId === undefined still returns local. + const services = new Map<string, unknown>(); + const host = createFakeHost(services); + host.provideService(remoteExecBackendFactoryHandle, () => createFakeRemoteBackend("unused")); + + createExecBackendExtension().activate(host); + const resolver = host.getService(execBackendHandle); + + expect(resolver(undefined)).toBe(localExecBackend); + }); +}); |
