353 lines · 12.7 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import { ChildProcessSpawner } from "effect/unstable/process"; import {  acquireDevShare,  claimDevShareLease,  cleanupOwnedDevShare,  type DevShareError,  DevShareLeaseClaimError,  DevServeFailedError,  shareDevServer,  unshareDevServer,} from "./dev-share.ts"; const TAILNET_STATUS = JSON.stringify({ Self: { DNSName: "host.example.ts.net." } });const NO_HANDLER_STDERR = "error: failed to remove web serve: handler does not exist"; interface CallResult {  readonly exitCode: number;  readonly stderr?: string;} const encode = (value: string) => Stream.make(new TextEncoder().encode(value)); /** * Answers `tailscale status --json` with a valid tailnet name, and lets each * test set the outcome of the `off` (pre-clear) and `serve` calls separately — * they are the same subcommand and are told apart by the trailing `off`. */const spawnerLayer = (input: {  readonly off?: CallResult;  readonly serve?: CallResult;  readonly onOff?: Effect.Effect<void>;  readonly calls?: Array<ReadonlyArray<string>>;}) =>  Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make((command) => {      const args = "args" in command ? (command.args as ReadonlyArray<string>) : [];      input.calls?.push(args);      const result: CallResult = args.includes("status")        ? { exitCode: 0 }        : args.includes("off")          ? (input.off ?? { exitCode: 0 })          : (input.serve ?? { exitCode: 0 });       return Effect.succeed(        ChildProcessSpawner.makeHandle({          pid: ChildProcessSpawner.ProcessId(1),          exitCode: (args.includes("off") ? (input.onOff ?? Effect.void) : Effect.void).pipe(            Effect.as(ChildProcessSpawner.ExitCode(result.exitCode)),          ),          isRunning: Effect.succeed(false),          kill: () => Effect.void,          unref: Effect.succeed(Effect.void),          stdin: Sink.drain,          stdout: args.includes("status") ? encode(TAILNET_STATUS) : Stream.empty,          stderr: result.stderr ? encode(result.stderr) : Stream.empty,          all: Stream.empty,          getInputFd: () => Sink.drain,          getOutputFd: () => Stream.empty,        }),      );    }),  ); describe("unshareDevServer", () => {  it.effect("treats a removed mapping as cleared", () =>    Effect.gen(function* () {      const result = yield* unshareDevServer(5788).pipe(        Effect.provide(spawnerLayer({ off: { exitCode: 0 } })),      );      assert.isTrue(result.cleared);    }),  );   // `tailscale serve … off` exits 1 when the port had no mapping, which is the  // normal first-share case — the port is clear, so this must not be an error.  it.effect("treats a missing handler as cleared", () =>    Effect.gen(function* () {      const result = yield* unshareDevServer(5788).pipe(        Effect.provide(spawnerLayer({ off: { exitCode: 1, stderr: NO_HANDLER_STDERR } })),      );      assert.isTrue(result.cleared);    }),  );   it.effect("reports a genuine removal failure as not cleared", () =>    Effect.gen(function* () {      const result = yield* unshareDevServer(5788).pipe(        Effect.provide(spawnerLayer({ off: { exitCode: 1, stderr: "permission denied" } })),      );      assert.isFalse(result.cleared);      assert.include(result.explanation, "permission denied");      // Structured, so a wrapping error can keep the real chain.      assert.equal(result.cause?._tag, "TailscaleCommandExitError");    }),  );}); describe("shareDevServer", () => {  it.effect("returns the tailnet URL for the same port", () =>    Effect.gen(function* () {      const shared = yield* shareDevServer({ webPort: 5788 }).pipe(        Effect.provide(spawnerLayer({ off: { exitCode: 1, stderr: NO_HANDLER_STDERR } })),      );       assert.equal(shared.host, "host.example.ts.net");      assert.equal(shared.url, "https://host.example.ts.net:5788/");    }),  );   // The stale-mapping clear runs before serve, so a failure here leaves the  // port serving nothing. Saying only "serve failed" would let an operator  // assume their previous mapping survived.  it.effect("reports that the prior mapping was cleared when serve fails", () =>    Effect.gen(function* () {      const error: DevShareError = yield* shareDevServer({ webPort: 5788 }).pipe(        Effect.provide(          spawnerLayer({            off: { exitCode: 0 },            serve: { exitCode: 1, stderr: "port already in use" },          }),        ),        Effect.flip,      );       assert.instanceOf(error, DevServeFailedError);      assert.equal(error.stage, "serve");      assert.equal(error.webPort, 5788);      // The underlying failure is preserved rather than flattened to a string.      assert.equal(        (error.cause as { _tag?: string } | undefined)?._tag,        "TailscaleCommandExitError",      );      // Unclassifiable stderr is never quoted (it can carry auth keys), so the      // message points at the command instead of echoing the CLI.      assert.notInclude(error.message, "port already in use");      assert.include(error.message, "run the command by hand");      assert.include(error.message, "no longer served");      assert.include(error.message, "5788");    }),  );   // A recognized failure gets our own wording for it — enough to act on  // without passing CLI text through.  it.effect("explains a recognized serve failure without quoting stderr", () =>    Effect.gen(function* () {      const error: DevShareError = yield* shareDevServer({ webPort: 5788 }).pipe(        Effect.provide(          spawnerLayer({            off: { exitCode: 0 },            serve: {              exitCode: 1,              stderr: "permission denied for tskey-auth-secret-token-value",            },          }),        ),        Effect.flip,      );       assert.instanceOf(error, DevServeFailedError);      assert.equal(error.stage, "serve");      assert.include(error.message, "permission denied");      assert.include(error.message, "elevated privileges");      assert.notInclude(error.message, "tskey-auth-secret-token-value");    }),  );   // Serving over routes we could not remove yields a URL that loads but whose  // /ws and /api quietly point at a dead backend.  it.effect("refuses to serve when the existing mapping could not be cleared", () =>    Effect.gen(function* () {      const error: DevShareError = yield* shareDevServer({ webPort: 5788 }).pipe(        Effect.provide(spawnerLayer({ off: { exitCode: 1, stderr: "permission denied" } })),        Effect.flip,      );       assert.instanceOf(error, DevServeFailedError);      // A distinct stage: the prior mapping survived, so nothing was replaced.      assert.equal(error.stage, "clear-existing");      assert.include(error.message, "could not clear the existing mapping");      assert.include(error.message, "permission denied");    }),  );}); it.layer(NodeServices.layer)("dev share cleanup ownership", (it) => {  it.effect("claims ownership via an atomic rename", () =>    Effect.gen(function* () {      const leasePath = "/leases/5788.owner";      const tempDirectory = "/leases/5788.owner.temp";      const writes: Array<readonly [path: string, contents: string]> = [];      const renames: Array<readonly [from: string, to: string]> = [];       yield* claimDevShareLease({ leasePath, ownerId: "new-runner", webPort: 5788 }).pipe(        Effect.provideService(          FileSystem.FileSystem,          FileSystem.makeNoop({            makeDirectory: () => Effect.void,            makeTempDirectoryScoped: (options) => {              assert.equal(options?.directory, "/leases");              assert.equal(options?.prefix, "5788.owner.");              return Effect.succeed(tempDirectory);            },            writeFileString: (path, contents) => {              writes.push([path, contents]);              return Effect.void;            },            rename: (from, to) => {              renames.push([from, to]);              return Effect.void;            },          }),        ),      );       assert.deepEqual(writes, [[`${tempDirectory}/owner.tmp`, "new-runner"]]);      assert.deepEqual(renames, [[`${tempDirectory}/owner.tmp`, leasePath]]);    }),  );   it.effect("keeps the prior owner when clearing its mapping fails", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const directory = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-dev-share-lease-",      });      const leasePath = `${directory}/5788.owner`;      const calls: Array<ReadonlyArray<string>> = [];       yield* fileSystem.writeFileString(leasePath, "old-runner");      yield* acquireDevShare({        leasePath,        ownerId: "new-runner",        webPort: 5788,      }).pipe(        Effect.provide(          spawnerLayer({            calls,            off: { exitCode: 1, stderr: "permission denied" },          }),        ),        Effect.flip,      );       assert.equal(yield* fileSystem.readFileString(leasePath), "old-runner");      assert.equal(calls.filter((args) => args.includes("off")).length, 1);    }),  );   it.effect("claims ownership after clearing and before publishing", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const directory = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-dev-share-lease-",      });      const leasePath = `${directory}/5788.owner`;       yield* fileSystem.writeFileString(leasePath, "old-runner");      yield* acquireDevShare({        leasePath,        ownerId: "new-runner",        webPort: 5788,      }).pipe(        Effect.provide(          spawnerLayer({            off: { exitCode: 0 },            serve: { exitCode: 1, stderr: "permission denied" },          }),        ),        Effect.flip,      );       assert.equal(yield* fileSystem.readFileString(leasePath), "new-runner");    }),  );   it.effect("restores the mapping when ownership cannot be claimed", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const directory = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-dev-share-lease-",      });      const blockedDirectory = `${directory}/not-a-directory`;      const calls: Array<ReadonlyArray<string>> = [];       yield* fileSystem.writeFileString(blockedDirectory, "blocked");      const error = yield* acquireDevShare({        leasePath: `${blockedDirectory}/5788.owner`,        ownerId: "new-runner",        webPort: 5788,      }).pipe(Effect.provide(spawnerLayer({ calls })), Effect.flip);       assert.instanceOf(error, DevShareLeaseClaimError);      assert.isTrue(calls.some((args) => args.includes("off")));      assert.isTrue(calls.some((args) => args.includes("http://127.0.0.1:5788")));    }),  );   it.effect("restores a newer runner's mapping when ownership changes during cleanup", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const directory = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-dev-share-lease-",      });      const leasePath = `${directory}/5788.owner`;      const oldLease = { leasePath, ownerId: "old-runner", webPort: 5788 };      const calls: Array<ReadonlyArray<string>> = [];       yield* claimDevShareLease(oldLease);      const result = yield* cleanupOwnedDevShare(oldLease).pipe(        Effect.provide(          spawnerLayer({            calls,            onOff: fileSystem.writeFileString(leasePath, "new-runner").pipe(Effect.orDie),          }),        ),      );       assert.equal(result.status, "restored");      assert.isTrue(calls.some((args) => args.includes("off")));      assert.isTrue(calls.some((args) => args.includes("http://127.0.0.1:5788")));    }),  );   it.effect("leaves the mapping alone when a newer runner already owns it", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const directory = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-dev-share-lease-",      });      const leasePath = `${directory}/5788.owner`;      const oldLease = { leasePath, ownerId: "old-runner", webPort: 5788 };      const calls: Array<ReadonlyArray<string>> = [];       yield* claimDevShareLease(oldLease);      yield* claimDevShareLease({ ...oldLease, ownerId: "new-runner" });      const result = yield* cleanupOwnedDevShare(oldLease).pipe(        Effect.provide(spawnerLayer({ calls })),      );       assert.equal(result.status, "superseded");      assert.isEmpty(calls);    }),  );});