import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path"; import { Launcher, readServiceState, writeServiceState } from "./serviceLauncher.ts";import { compareExactServiceVersions, decodeServiceState, isExactServiceVersion, SERVICE_LAUNCHER_PROTOCOL, SERVICE_STOP_MARKER_FILE,} from "./cloud/serviceProtocol.ts"; it("accepts only exact semantic versions", () => { for (const version of ["0.0.0", "1.2.3", "1.2.3-alpha.1", "1.2.3-0", "1.2.3+001"]) { assert.isTrue(isExactServiceVersion(version), version); } for (const version of ["latest", "01.2.3", "1.2.3-01", "1.2.3-alpha..1", "1.2.3+."]) { assert.isFalse(isExactServiceVersion(version), version); }}); it("orders exact semantic versions without treating build metadata as precedence", () => { assert.equal(compareExactServiceVersions("1.2.3", "1.2.3"), 0); assert.equal(compareExactServiceVersions("1.2.4", "1.2.3"), 1); assert.equal(compareExactServiceVersions("2.0.0-alpha.1", "2.0.0-alpha.2"), -1); assert.equal(compareExactServiceVersions("2.0.0-alpha.2", "2.0.0-alpha.beta"), -1); assert.equal(compareExactServiceVersions("2.0.0-alpha-beta", "2.0.0-alpha-alpha"), 1); assert.equal(compareExactServiceVersions("2.0.0", "2.0.0-rc.1"), 1); assert.equal(compareExactServiceVersions("2.0.0+one", "2.0.0+two"), 0);}); it("rejects contradictory service state", () => { assert.isUndefined( decodeServiceState({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "0.0.31", update: { id: "update-1", fromVersion: "0.0.30", targetVersion: "0.0.32", dbPath: "/tmp/state.sqlite", status: "pending", }, }), ); assert.isUndefined( decodeServiceState({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", update: { id: "update-3", fromVersion: "1.0.0", targetVersion: "1.1.0", status: "pending", }, }), ); assert.isUndefined( decodeServiceState({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", update: { id: "update-2", fromVersion: "1.0.0", targetVersion: "0.9.0", dbPath: "/tmp/state.sqlite", status: "pending", }, }), );}); it.layer(NodeServices.layer)("service state persistence", (it) => { it.effect("durably replaces and strictly reads one state document", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-test-" }); const statePath = path.join(root, "runtime", "service-state.json"); const state = { protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "0.0.31", } as const; yield* Effect.promise(() => writeServiceState(statePath, state)); assert.deepEqual(yield* Effect.promise(() => readServiceState(statePath)), state); }), ); it.effect("serializes shutdown with launcher recovery", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-stop-" }); const statePath = path.join(root, "runtime", "service-state.json"); const versionDir = path.join(root, "runtime", "versions", "1.0.0"); const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs"); yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true }); yield* fs.writeFileString(entryPath, "setInterval(() => {}, 1_000);\n"); yield* fs.writeFileString(path.join(versionDir, ".install-complete"), "1.0.0\n"); yield* Effect.promise(() => writeServiceState(statePath, { protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", }), ); const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath))); const running = launcher.run(); const stopping = launcher.stop("SIGTERM"); // An explicit stop leaves the marker that tells a child shutting down // mid-update that no replacement server is coming. It is present as // soon as stop() returns its promise, before queued transitions run. assert.isTrue(yield* fs.exists(path.join(root, "runtime", SERVICE_STOP_MARKER_FILE))); yield* Effect.promise(() => stopping); yield* Effect.promise(() => running); }), ); it.effect("commits only after the trial reports prepared", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-flow-" }); const statePath = path.join(root, "runtime", "service-state.json"); const databasePath = path.join(root, "userdata", "state.sqlite"); yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true }); yield* fs.writeFileString(databasePath, "before trial"); // @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source. const encodedDatabasePath = JSON.stringify(databasePath); const childSource = `const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);if (context.update?.status === "pending") { process.send({ type: "prepared", updateId: context.update.id }); process.on("message", (message) => { if (message.type === "committed") process.exit(0); });} else if (context.update === undefined) { process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} }); setInterval(() => {}, 1_000);} else { process.exit(0);}`; for (const version of ["1.0.0", "1.1.0"]) { const versionDir = path.join(root, "runtime", "versions", version); const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs"); yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true }); yield* fs.writeFileString(entryPath, childSource); yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`); } yield* Effect.promise(() => writeServiceState(statePath, { protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", }), ); const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath))); yield* Effect.promise(() => launcher.run().then( () => Promise.reject(new Error("launcher unexpectedly completed")), () => Promise.resolve(), ), ); const state = yield* Effect.promise(() => readServiceState(statePath)); assert.equal(state.activeVersion, "1.1.0"); assert.equal(state.update?.status, "committed"); }), ); it.effect("rolls back a trial that reports the wrong update ID", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-rollback-" }); const statePath = path.join(root, "runtime", "service-state.json"); const databasePath = path.join(root, "userdata", "state.sqlite"); yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true }); yield* fs.writeFileString(databasePath, "before trial"); // @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source. const encodedDatabasePath = JSON.stringify(databasePath); const childSource = `const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);if (context.update?.status === "pending") { process.send({ type: "prepared", updateId: "wrong-update" });} else if (context.update === undefined) { process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} }); setInterval(() => {}, 1_000);} else { process.exit(0);}`; for (const version of ["1.0.0", "1.1.0"]) { const versionDir = path.join(root, "runtime", "versions", version); const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs"); yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true }); yield* fs.writeFileString(entryPath, childSource); yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`); } yield* Effect.promise(() => writeServiceState(statePath, { protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", }), ); const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath))); yield* Effect.promise(() => launcher.run().then( () => Promise.reject(new Error("launcher unexpectedly completed")), () => Promise.resolve(), ), ); const state = yield* Effect.promise(() => readServiceState(statePath)); assert.equal(state.activeVersion, "1.0.0"); assert.equal(state.update?.status, "rolled-back"); assert.equal( state.update?.status === "rolled-back" ? state.update.reason : undefined, "invalid-prepared", ); }), ); it.effect("restores the database when a migrating trial exits", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-db-" }); const statePath = path.join(root, "runtime", "service-state.json"); const databasePath = path.join(root, "userdata", "state.sqlite"); const original = "database before migration"; yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true }); yield* fs.writeFileString(databasePath, original); // @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source. const encodedDatabasePath = JSON.stringify(databasePath); const childSource = `import { writeFileSync } from "node:fs";const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);if (context.update?.status === "pending") { writeFileSync(context.update.dbPath, "database after migration"); writeFileSync(context.update.dbPath + "-wal", "trial wal"); writeFileSync(context.update.dbPath + "-shm", "trial shm"); process.exit(1);} else if (context.update === undefined) { process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} }); setInterval(() => {}, 1_000);} else { process.exit(0);}`; for (const version of ["1.0.0", "1.1.0"]) { const versionDir = path.join(root, "runtime", "versions", version); const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs"); yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true }); yield* fs.writeFileString(entryPath, childSource); yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`); } yield* Effect.promise(() => writeServiceState(statePath, { protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.0.0", }), ); const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath))); yield* Effect.promise(() => launcher.run().then( () => Promise.reject(new Error("launcher unexpectedly completed")), () => Promise.resolve(), ), ); const state = yield* Effect.promise(() => readServiceState(statePath)); assert.equal(state.activeVersion, "1.0.0"); assert.equal(state.update?.status, "rolled-back"); assert.equal(yield* fs.readFileString(databasePath), original); assert.isFalse(yield* fs.exists(`${databasePath}-wal`)); assert.isFalse(yield* fs.exists(`${databasePath}-shm`)); const updateId = state.update?.id; assert.isDefined(updateId); assert.isFalse(yield* fs.exists(path.join(root, "runtime", "db-backup", updateId))); }), );});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { Launcher, readServiceState, writeServiceState } from "./serviceLauncher.ts";
import {
compareExactServiceVersions,
decodeServiceState,
isExactServiceVersion,
SERVICE_LAUNCHER_PROTOCOL,
SERVICE_STOP_MARKER_FILE,
} from "./cloud/serviceProtocol.ts";
it("accepts only exact semantic versions", () => {
for (const version of ["0.0.0", "1.2.3", "1.2.3-alpha.1", "1.2.3-0", "1.2.3+001"]) {
assert.isTrue(isExactServiceVersion(version), version);
}
for (const version of ["latest", "01.2.3", "1.2.3-01", "1.2.3-alpha..1", "1.2.3+."]) {
assert.isFalse(isExactServiceVersion(version), version);
}
});
it("orders exact semantic versions without treating build metadata as precedence", () => {
assert.equal(compareExactServiceVersions("1.2.3", "1.2.3"), 0);
assert.equal(compareExactServiceVersions("1.2.4", "1.2.3"), 1);
assert.equal(compareExactServiceVersions("2.0.0-alpha.1", "2.0.0-alpha.2"), -1);
assert.equal(compareExactServiceVersions("2.0.0-alpha.2", "2.0.0-alpha.beta"), -1);
assert.equal(compareExactServiceVersions("2.0.0-alpha-beta", "2.0.0-alpha-alpha"), 1);
assert.equal(compareExactServiceVersions("2.0.0", "2.0.0-rc.1"), 1);
assert.equal(compareExactServiceVersions("2.0.0+one", "2.0.0+two"), 0);
});
it("rejects contradictory service state", () => {
assert.isUndefined(
decodeServiceState({
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "0.0.31",
update: {
id: "update-1",
fromVersion: "0.0.30",
targetVersion: "0.0.32",
dbPath: "/tmp/state.sqlite",
status: "pending",
},
}),
);
assert.isUndefined(
decodeServiceState({
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
update: {
id: "update-3",
fromVersion: "1.0.0",
targetVersion: "1.1.0",
status: "pending",
},
}),
);
assert.isUndefined(
decodeServiceState({
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
update: {
id: "update-2",
fromVersion: "1.0.0",
targetVersion: "0.9.0",
dbPath: "/tmp/state.sqlite",
status: "pending",
},
}),
);
});
it.layer(NodeServices.layer)("service state persistence", (it) => {
it.effect("durably replaces and strictly reads one state document", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-test-" });
const statePath = path.join(root, "runtime", "service-state.json");
const state = {
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "0.0.31",
} as const;
yield* Effect.promise(() => writeServiceState(statePath, state));
assert.deepEqual(yield* Effect.promise(() => readServiceState(statePath)), state);
}),
);
it.effect("serializes shutdown with launcher recovery", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-stop-" });
const statePath = path.join(root, "runtime", "service-state.json");
const versionDir = path.join(root, "runtime", "versions", "1.0.0");
const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs");
yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fs.writeFileString(entryPath, "setInterval(() => {}, 1_000);\n");
yield* fs.writeFileString(path.join(versionDir, ".install-complete"), "1.0.0\n");
yield* Effect.promise(() =>
writeServiceState(statePath, {
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
}),
);
const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath)));
const running = launcher.run();
const stopping = launcher.stop("SIGTERM");
// An explicit stop leaves the marker that tells a child shutting down
// mid-update that no replacement server is coming. It is present as
// soon as stop() returns its promise, before queued transitions run.
assert.isTrue(yield* fs.exists(path.join(root, "runtime", SERVICE_STOP_MARKER_FILE)));
yield* Effect.promise(() => stopping);
yield* Effect.promise(() => running);
}),
);
it.effect("commits only after the trial reports prepared", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-flow-" });
const statePath = path.join(root, "runtime", "service-state.json");
const databasePath = path.join(root, "userdata", "state.sqlite");
yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true });
yield* fs.writeFileString(databasePath, "before trial");
// @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source.
const encodedDatabasePath = JSON.stringify(databasePath);
const childSource = `
const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);
if (context.update?.status === "pending") {
process.send({ type: "prepared", updateId: context.update.id });
process.on("message", (message) => {
if (message.type === "committed") process.exit(0);
});
} else if (context.update === undefined) {
process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} });
setInterval(() => {}, 1_000);
} else {
process.exit(0);
}
`;
for (const version of ["1.0.0", "1.1.0"]) {
const versionDir = path.join(root, "runtime", "versions", version);
const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs");
yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fs.writeFileString(entryPath, childSource);
yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`);
}
yield* Effect.promise(() =>
writeServiceState(statePath, {
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
}),
);
const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath)));
yield* Effect.promise(() =>
launcher.run().then(
() => Promise.reject(new Error("launcher unexpectedly completed")),
() => Promise.resolve(),
),
);
const state = yield* Effect.promise(() => readServiceState(statePath));
assert.equal(state.activeVersion, "1.1.0");
assert.equal(state.update?.status, "committed");
}),
);
it.effect("rolls back a trial that reports the wrong update ID", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-rollback-" });
const statePath = path.join(root, "runtime", "service-state.json");
const databasePath = path.join(root, "userdata", "state.sqlite");
yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true });
yield* fs.writeFileString(databasePath, "before trial");
// @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source.
const encodedDatabasePath = JSON.stringify(databasePath);
const childSource = `
const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);
if (context.update?.status === "pending") {
process.send({ type: "prepared", updateId: "wrong-update" });
} else if (context.update === undefined) {
process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} });
setInterval(() => {}, 1_000);
} else {
process.exit(0);
}
`;
for (const version of ["1.0.0", "1.1.0"]) {
const versionDir = path.join(root, "runtime", "versions", version);
const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs");
yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fs.writeFileString(entryPath, childSource);
yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`);
}
yield* Effect.promise(() =>
writeServiceState(statePath, {
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
}),
);
const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath)));
yield* Effect.promise(() =>
launcher.run().then(
() => Promise.reject(new Error("launcher unexpectedly completed")),
() => Promise.resolve(),
),
);
const state = yield* Effect.promise(() => readServiceState(statePath));
assert.equal(state.activeVersion, "1.0.0");
assert.equal(state.update?.status, "rolled-back");
assert.equal(
state.update?.status === "rolled-back" ? state.update.reason : undefined,
"invalid-prepared",
);
}),
);
it.effect("restores the database when a migrating trial exits", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-launcher-db-" });
const statePath = path.join(root, "runtime", "service-state.json");
const databasePath = path.join(root, "userdata", "state.sqlite");
const original = "database before migration";
yield* fs.makeDirectory(path.dirname(databasePath), { recursive: true });
yield* fs.writeFileString(databasePath, original);
// @effect-diagnostics-next-line preferSchemaOverJson:off - embeds a path in fake child source.
const encodedDatabasePath = JSON.stringify(databasePath);
const childSource = `
import { writeFileSync } from "node:fs";
const context = JSON.parse(process.env.T3_SERVICE_LAUNCHER_CONTEXT);
if (context.update?.status === "pending") {
writeFileSync(context.update.dbPath, "database after migration");
writeFileSync(context.update.dbPath + "-wal", "trial wal");
writeFileSync(context.update.dbPath + "-shm", "trial shm");
process.exit(1);
} else if (context.update === undefined) {
process.send({ type: "request-update", targetVersion: "1.1.0", dbPath: ${encodedDatabasePath} });
setInterval(() => {}, 1_000);
} else {
process.exit(0);
}
`;
for (const version of ["1.0.0", "1.1.0"]) {
const versionDir = path.join(root, "runtime", "versions", version);
const entryPath = path.join(versionDir, "node_modules", "t3", "dist", "bin.mjs");
yield* fs.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fs.writeFileString(entryPath, childSource);
yield* fs.writeFileString(path.join(versionDir, ".install-complete"), `${version}\n`);
}
yield* Effect.promise(() =>
writeServiceState(statePath, {
protocol: SERVICE_LAUNCHER_PROTOCOL,
activeVersion: "1.0.0",
}),
);
const launcher = new Launcher(root, yield* Effect.promise(() => readServiceState(statePath)));
yield* Effect.promise(() =>
launcher.run().then(
() => Promise.reject(new Error("launcher unexpectedly completed")),
() => Promise.resolve(),
),
);
const state = yield* Effect.promise(() => readServiceState(statePath));
assert.equal(state.activeVersion, "1.0.0");
assert.equal(state.update?.status, "rolled-back");
assert.equal(yield* fs.readFileString(databasePath), original);
assert.isFalse(yield* fs.exists(`${databasePath}-wal`));
assert.isFalse(yield* fs.exists(`${databasePath}-shm`));
const updateId = state.update?.id;
assert.isDefined(updateId);
assert.isFalse(yield* fs.exists(path.join(root, "runtime", "db-backup", updateId)));
}),
);
});