Skip to content
Source
Sign inSign up

servicePreflight.test.ts

126 lines5.4 KB

1import * as NodeSqlite from "node:sqlite";
2​
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
8​
9import {
10 BUILD_MIGRATION_HEAD,
11 databaseMigrationHead,
12} from "../persistence/databaseMigrationHead.ts";
13import { migrationManifest } from "../persistence/Migrations.ts";
14​
15import { runServicePreflight } from "./servicePreflight.ts";
16import { SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts";
17​
18// regression: #13239 a stale CLI shadowing the updated one kept "updating" to itself
19it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (launcherProtocol) => {
20 const result = runServicePreflight({
21 databasePath: "/missing/state.sqlite",
22 launcherProtocol,
23 version: "9.8.7",
24 });
25​
26 expect(result).toMatchObject({ status: "blocked", version: "9.8.7" });
27 const reason = result.status === "blocked" ? result.reason : "";
28 expect(reason).toMatch(new RegExp(`\\b${launcherProtocol}\\b`));
29 expect(reason).toMatch(new RegExp(`\\b${SERVICE_LAUNCHER_PROTOCOL}\\b`));
30 expect(reason).toContain("curl -fsSL https://t3.codes/install.sh | T3CODE_VERSION=9.8.7 sh");
31 expect(reason).toContain("~/.local/bin/t3 service install");
32 expect(reason).toContain("t3 --version");
33 expect(reason).toContain("which -a t3");
34});
35​
36// regression: #34502 every installed launcher was protocol 3, so protocol 4 blocked every remote update
37it.each([3, SERVICE_LAUNCHER_PROTOCOL])(
38 "is ready under launcher protocol %i and reports it back",
39 (launcherProtocol) => {
40 // A released server accepts a ready result only when it names that server's own protocol.
41 expect(
42 runServicePreflight({
43 databasePath: "/missing/state.sqlite",
44 launcherProtocol,
45 version: "9.8.7",
46 }),
47 ).toEqual({ status: "ready", version: "9.8.7", launcherProtocol });
48 },
49);
50​
51it.layer(NodeServices.layer)("service preflight database", (it) => {
52 it.effect("blocks a database that a newer build migrated", () =>
53 Effect.gen(function* () {
54 const fs = yield* FileSystem.FileSystem;
55 const path = yield* Path.Path;
56 const databasePath = path.join(
57 yield* fs.makeTempDirectoryScoped({ prefix: "t3-preflight-" }),
58 "state.sqlite",
59 );
60 const db = new NodeSqlite.DatabaseSync(databasePath);
61 db.exec("CREATE TABLE effect_sql_migrations (migration_id INTEGER PRIMARY KEY, name TEXT)");
62 const insert = db.prepare("INSERT INTO effect_sql_migrations VALUES (?, ?)");
63 for (const [id, name] of migrationManifest) insert.run(id, name);
64 const preflight = () =>
65 runServicePreflight({ databasePath, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL });
66​
67 expect(preflight().status).toBe("ready");
68 expect(databaseMigrationHead(databasePath)).toBe(BUILD_MIGRATION_HEAD);
69​
70 db.prepare("UPDATE effect_sql_migrations SET name = ? WHERE migration_id = ?").run(
71 "ForkMigration",
72 BUILD_MIGRATION_HEAD,
73 );
74 const conflict = preflight();
75 expect(conflict.status).toBe("blocked");
76 expect(conflict.status === "blocked" ? conflict.reason : "").toContain("ForkMigration");
77 db.prepare("UPDATE effect_sql_migrations SET name = ? WHERE migration_id = ?").run(
78 migrationManifest.at(-1)![1],
79 BUILD_MIGRATION_HEAD,
80 );
81​
82 // Startup reconciles this V2 preview history, so it must not block an update.
83 const preview = path.join(path.dirname(databasePath), "preview.sqlite");
84 const previewDb = new NodeSqlite.DatabaseSync(preview);
85 previewDb.exec(
86 "CREATE TABLE effect_sql_migrations (migration_id INTEGER PRIMARY KEY, name TEXT)",
87 );
88 const insertPreview = previewDb.prepare("INSERT INTO effect_sql_migrations VALUES (?, ?)");
89 for (const [id, name] of migrationManifest.filter(([id]) => id < 53)) {
90 insertPreview.run(id, name);
91 }
92 insertPreview.run(53, "PullRequestFilesViewed");
93 insertPreview.run(54, "OrchestrationV2");
94 insertPreview.run(55, "RemoveRedundantProjectionIndexes");
95 previewDb.close();
96 expect(
97 runServicePreflight({ databasePath: preview, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL })
98 .status,
99 ).toBe("ready");
100​
101 const newerId = Math.max(...migrationManifest.map(([id]) => id)) + 1;
102 insert.run(newerId, "FromNightly");
103 db.close();
104 const result = preflight();
105 expect(result.status).toBe("blocked");
106 expect(result.status === "blocked" ? result.reason : "").toContain(`${newerId}_FromNightly`);
107 // Servers report this head, so clients see a database a newer build migrated.
108 expect(databaseMigrationHead(databasePath)).toBe(newerId);
109​
110 // A database that exists but cannot be read has an unknown history, so it blocks.
111 const unreadable = path.join(path.dirname(databasePath), "unreadable.sqlite");
112 yield* fs.writeFileString(unreadable, "not a database");
113 expect(databaseMigrationHead(unreadable)).toBeUndefined();
114 expect(
115 runServicePreflight({
116 databasePath: unreadable,
117 launcherProtocol: SERVICE_LAUNCHER_PROTOCOL,
118 }).status,
119 ).toBe("blocked");
120 // A missing database is a fresh install.
121 expect(databaseMigrationHead(path.join(path.dirname(databasePath), "missing.sqlite"))).toBe(
122 BUILD_MIGRATION_HEAD,
123 );
124 }),
125 );
126});