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); }), );});
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);
}),
);
});