Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 233 additions & 4 deletions apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { SpawnExecutableResolution } from "@t3tools/shared/shell";
import {
CommandAvailability,
type CommandAvailabilityChecker,
isCommandAvailable,
SpawnExecutableResolution,
} from "@t3tools/shared/shell";
import * as ExternalLauncher from "./externalLauncher.ts";

function makeMockDetachedHandle(onUnref: () => void = () => undefined) {
Expand All @@ -36,6 +46,7 @@ const testLayer = (input: {
readonly platform: NodeJS.Platform;
readonly env?: Record<string, string>;
readonly resolveExecutable?: (command: string) => string | undefined;
readonly commandAvailability?: CommandAvailabilityChecker;
readonly onSpawn?: (command: ChildProcess.StandardCommand) => void;
readonly onUnref?: () => void;
}) => {
Expand All @@ -60,6 +71,7 @@ const testLayer = (input: {
SpawnExecutableResolution,
(command) => input.resolveExecutable?.(command) ?? command,
),
Layer.succeed(CommandAvailability, input.commandAvailability ?? isCommandAvailable),
ConfigProvider.layer(ConfigProvider.fromEnv({ env: input.env ?? {} })),
);
};
Expand Down Expand Up @@ -138,7 +150,7 @@ it.effect("discovers editors through the service API", () =>
yield* fileSystem.writeFileString(path.join(binDir, "code.CMD"), "@echo off\r\n");
yield* fileSystem.writeFileString(path.join(binDir, "explorer.CMD"), "@echo off\r\n");

const editors = yield* Effect.gen(function* () {
const discovery = yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
return yield* launcher.resolveAvailableEditors();
}).pipe(
Expand All @@ -150,11 +162,228 @@ it.effect("discovers editors through the service API", () =>
),
);

assert.equal(editors.includes("vscode"), true);
assert.equal(editors.includes("file-manager"), true);
assert.equal(discovery.complete, true);
assert.equal(discovery.editors.includes("vscode"), true);
assert.equal(discovery.editors.includes("file-manager"), true);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("keeps responsive editors when other discovery probes time out", () => {
const discoveryTimeout = Duration.seconds(3);

return Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const discoveryFiber = yield* launcher.resolveAvailableEditors().pipe(Effect.forkScoped);

yield* Effect.yieldNow;
yield* TestClock.adjust(discoveryTimeout);

const discovery = yield* Fiber.join(discoveryFiber);
assert.deepEqual(discovery, {
editors: ["vscode", "file-manager"],
complete: false,
});
}).pipe(
Effect.scoped,
Effect.provide(
Layer.merge(
TestClock.layer(),
testLayer({
platform: "linux",
env: { PATH: "/bin" },
commandAvailability: (command) =>
command === "code" || command === "xdg-open" ? Effect.succeed(true) : Effect.never,
}),
),
),
);
});

it.effect("preserves the last complete editor list when a cached refresh is incomplete", () => {
const discoveryTimeout = Duration.seconds(3);
let incomplete = false;
let codeChecks = 0;

return Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const [initialDiscovery, concurrentDiscovery] = yield* Effect.all(
[launcher.resolveAvailableEditors(), launcher.resolveAvailableEditors()],
{ concurrency: "unbounded" },
);
assert.deepEqual(initialDiscovery, {
editors: ["vscode", "file-manager"],
complete: true,
});
assert.deepEqual(concurrentDiscovery, initialDiscovery);
assert.equal(codeChecks, 1);

const cachedDiscovery = yield* launcher.resolveAvailableEditors();
assert.deepEqual(cachedDiscovery, initialDiscovery);
assert.equal(codeChecks, 1);

incomplete = true;
yield* TestClock.adjust(Duration.minutes(1));

const refreshFiber = yield* launcher.resolveAvailableEditors().pipe(Effect.forkScoped);
yield* Effect.yieldNow;
yield* TestClock.adjust(discoveryTimeout);

const refreshedDiscovery = yield* Fiber.join(refreshFiber);
assert.deepEqual(refreshedDiscovery, {
editors: initialDiscovery.editors,
complete: false,
});
assert.equal(codeChecks, 2);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.merge(
TestClock.layer(),
testLayer({
platform: "linux",
env: { PATH: "/bin" },
commandAvailability: (command) => {
if (command === "code") {
codeChecks += 1;
}
if (command === "code" || command === "xdg-open") {
return Effect.succeed(true);
}
return incomplete ? Effect.never : Effect.succeed(false);
},
}),
),
),
);
});

it.effect("does not cache an incomplete first discovery", () => {
const discoveryTimeout = Duration.seconds(3);
let stalled = true;
let codeChecks = 0;

return Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const initialFiber = yield* launcher.resolveAvailableEditors().pipe(Effect.forkScoped);

yield* Effect.yieldNow;
yield* TestClock.adjust(discoveryTimeout);

assert.deepEqual(yield* Fiber.join(initialFiber), {
editors: [],
complete: false,
});

stalled = false;
const retryDiscovery = yield* launcher.resolveAvailableEditors();
assert.deepEqual(retryDiscovery, {
editors: ["vscode", "file-manager"],
complete: true,
});
assert.equal(codeChecks, 2);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.merge(
TestClock.layer(),
testLayer({
platform: "linux",
env: { PATH: "/bin" },
commandAvailability: (command) => {
if (command === "code") {
codeChecks += 1;
}
return stalled
? Effect.never
: Effect.succeed(command === "code" || command === "xdg-open");
},
}),
),
),
);
});

it.effect("keeps shared discovery alive when its first caller is interrupted", () => {
let codeChecks = 0;

return Effect.gen(function* () {
const lookupStarted = yield* Deferred.make<void>();
const releaseLookup = yield* Deferred.make<void>();

yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const firstCaller = yield* launcher.resolveAvailableEditors().pipe(Effect.forkScoped);

yield* Deferred.await(lookupStarted);
yield* Fiber.interrupt(firstCaller);

const secondCaller = yield* launcher.resolveAvailableEditors().pipe(Effect.forkScoped);
yield* Deferred.succeed(releaseLookup, undefined);

assert.deepEqual(yield* Fiber.join(secondCaller), {
editors: ["vscode", "file-manager"],
complete: true,
});
assert.equal(codeChecks, 1);
}).pipe(
Effect.provide(
testLayer({
platform: "linux",
env: { PATH: "/bin" },
commandAvailability: (command) => {
if (command === "code") {
codeChecks += 1;
}
return (
command === "code" ? Deferred.succeed(lookupStarted, undefined) : Effect.void
).pipe(
Effect.andThen(Deferred.await(releaseLookup)),
Effect.as(command === "code" || command === "xdg-open"),
);
},
}),
),
);
}).pipe(Effect.scoped);
});

it.effect("allows editor discovery to retry after a shared lookup defects", () => {
let shouldDefect = true;
let codeChecks = 0;

return Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const failedDiscovery = yield* launcher.resolveAvailableEditors().pipe(Effect.exit);

assert.equal(Exit.isFailure(failedDiscovery), true);

shouldDefect = false;
const retryDiscovery = yield* launcher.resolveAvailableEditors();

assert.deepEqual(retryDiscovery, {
editors: ["vscode", "file-manager"],
complete: true,
});
assert.equal(codeChecks, 2);
}).pipe(
Effect.provide(
testLayer({
platform: "linux",
env: { PATH: "/bin" },
commandAvailability: (command) => {
if (command === "code") {
codeChecks += 1;
if (shouldDefect) {
return Effect.die("discovery defect");
}
}
return Effect.succeed(command === "code" || command === "xdg-open");
},
}),
),
);
});

it.effect("rejects unknown editors through the service API", () =>
Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
Expand Down
Loading
Loading