summaryrefslogtreecommitdiffhomepage
path: root/packages/exec-backend/src/extension.test.ts
blob: f32901b9fc0706811f125eec3a8749d41df64d1f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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);
  });
});