1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Option from "effect/Option";
6import * as Path from "effect/Path";
7import * as HostProcess from "@t3tools/shared/HostProcess";
8
9import { repointLauncher, resolveLauncherPath } from "./update.ts";
10
11it.layer(NodeServices.layer)("t3 update launcher", (it) => {
12 it.effect("repoints a symlink that lives in a runtime versions tree", () =>
13 Effect.gen(function* () {
14 const fs = yield* FileSystem.FileSystem;
15 const path = yield* Path.Path;
16 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-update-" });
17 const oldExe = path.join(root, "runtime/versions/1.0.0/t3");
18 const newExe = path.join(root, "runtime/versions/2.0.0/t3");
19 const launcher = path.join(root, "bin/t3");
20 for (const file of [oldExe, newExe]) {
21 yield* fs.makeDirectory(path.dirname(file), { recursive: true });
22 yield* fs.writeFileString(file, "");
23 }
24 yield* fs.makeDirectory(path.dirname(launcher), { recursive: true });
25 yield* fs.symlink(oldExe, launcher);
26
27 const repointed = yield* repointLauncher({
28 launchedAs: launcher,
29 versionsDir: path.join(root, "runtime/versions"),
30 targetEntryPath: newExe,
31 });
32
33 assert.deepStrictEqual(Option.getOrUndefined(repointed), launcher);
34 assert.equal(yield* fs.readLink(launcher), newExe);
35 }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")),
36 );
37
38 it.effect("leaves a plain copy or a foreign symlink alone", () =>
39 Effect.gen(function* () {
40 const fs = yield* FileSystem.FileSystem;
41 const path = yield* Path.Path;
42 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-update-" });
43 const newExe = path.join(root, "runtime/versions/2.0.0/t3");
44 const copy = path.join(root, "copy/t3");
45 const foreign = path.join(root, "foreign/t3");
46 const elsewhere = path.join(root, "elsewhere/t3");
47 // Another install's versions tree: same shape, different home.
48 const otherHome = path.join(root, "other/runtime/versions/1.0.0/t3");
49 const otherLauncher = path.join(root, "other/bin/t3");
50 for (const file of [newExe, copy, elsewhere, otherHome]) {
51 yield* fs.makeDirectory(path.dirname(file), { recursive: true });
52 yield* fs.writeFileString(file, "");
53 }
54 yield* fs.makeDirectory(path.dirname(foreign), { recursive: true });
55 yield* fs.symlink(elsewhere, foreign);
56 yield* fs.makeDirectory(path.dirname(otherLauncher), { recursive: true });
57 yield* fs.symlink(otherHome, otherLauncher);
58
59 for (const launchedAs of [copy, foreign, otherLauncher, undefined]) {
60 const repointed = yield* repointLauncher({
61 launchedAs,
62 versionsDir: path.join(root, "runtime/versions"),
63 targetEntryPath: newExe,
64 });
65 assert.equal(repointed._tag, "None", launchedAs ?? "undefined");
66 }
67 assert.equal(yield* fs.readLink(foreign), elsewhere);
68 assert.equal(yield* fs.readLink(otherLauncher), otherHome);
69 }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")),
70 );
71
72 it.effect("finds the launcher a bare command name resolved to on PATH", () =>
73 Effect.gen(function* () {
74 const fs = yield* FileSystem.FileSystem;
75 const path = yield* Path.Path;
76 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-update-" });
77 const launcher = path.join(root, "bin/t3");
78 yield* fs.makeDirectory(path.dirname(launcher), { recursive: true });
79 yield* fs.writeFileString(launcher, "");
80
81 const bare = yield* resolveLauncherPath.pipe(
82 Effect.provideService(HostProcess.InvokedAs, "t3"),
83 Effect.provideService(HostProcess.Environment, {
84 PATH: `${path.join(root, "missing")}:${path.join(root, "bin")}`,
85 }),
86 Effect.provideService(HostProcess.WorkingDirectory, root),
87 );
88 const relative = yield* resolveLauncherPath.pipe(
89 Effect.provideService(HostProcess.InvokedAs, "./bin/t3"),
90 Effect.provideService(HostProcess.Environment, { PATH: "" }),
91 Effect.provideService(HostProcess.WorkingDirectory, root),
92 );
93 const absent = yield* resolveLauncherPath.pipe(
94 Effect.provideService(HostProcess.InvokedAs, "t3"),
95 Effect.provideService(HostProcess.Environment, { PATH: path.join(root, "missing") }),
96 Effect.provideService(HostProcess.WorkingDirectory, root),
97 );
98
99 assert.equal(bare, launcher);
100 assert.equal(relative, launcher);
101 assert.equal(absent, undefined);
102 }).pipe(Effect.scoped, Effect.provideService(HostProcess.Platform, "linux")),
103 );
104});