History

e2e: recorded film — integration survives a real reboot (terminal cast)

A watchable companion to restart-persistence: drives the same register → reboot → survived flow through the chat theater so the on-screen tool spinner runs for the ACTUAL guest reboot. Produces a terminal.cast the viewer plays in the matrix — press-play evidence instead of trusting a green check. Runs against the cli-* VM targets; verified on cli-macos (357-event cast).

Rhys Sullivan committed 5 months ago

Commit 5bacd3c1 parent e2bff6e1 file · +128 −0Browse files

Unified · Split

e2e/cli/service-lifecycle.test.ts+128−0
@@ -0,0 +1,128 @@
1+// The supervised daemon's durability as a WATCHABLE terminal recording: register
2+// a REAL integration, REBOOT the daemon's machine for real (the on-screen
3+// spinner runs for the actual reboot), then show the integration still there.
4+// Same assertions as restart-persistence — but filmed, so you can press play
5+// instead of trusting a green check. Runs against the cli-* VM targets.
6+import { randomBytes } from "node:crypto";
7+import { join } from "node:path";
8+
9+import { expect } from "@effect/vitest";
10+import { Effect } from "effect";
11+
12+import { composePluginApi } from "@executor-js/api/server";
13+import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
14+
15+import { withChatTheater } from "../src/clients/chat-theater";
16+import { scenario } from "../src/scenario";
17+import { Api, Cli, Restart, RunDir, Target } from "../src/services";
18+
19+const api = composePluginApi([openApiHttpPlugin()] as const);
20+
21+/** Inline OpenAPI 3 spec with a single GET /ping (its server is never called). */
22+const pingSpec = JSON.stringify({
23+ openapi: "3.0.3",
24+ info: { title: "Reboot Lifecycle API", version: "1.0.0" },
25+ servers: [{ url: "http://127.0.0.1:59998" }],
26+ paths: {
27+ "/ping": {
28+ get: {
29+ operationId: "getPing",
30+ summary: "Liveness ping",
31+ responses: { "200": { description: "pong" } },
32+ },
33+ },
34+ },
35+});
36+
37+scenario(
38+ "Supervised daemon · an integration survives a real machine reboot (recorded)",
39+ {},
40+ Effect.gen(function* () {
41+ const target = yield* Target;
42+ const restart = yield* Restart;
43+ const { client } = yield* Api;
44+ const cli = yield* Cli;
45+ const runDir = yield* RunDir;
46+
47+ const slug = `reboot-film-${randomBytes(4).toString("hex")}`;
48+
49+ yield* withChatTheater(
50+ cli,
51+ { title: "executor — supervised daemon", record: join(runDir, "terminal.cast") },
52+ (chat) =>
53+ Effect.gen(function* () {
54+ yield* chat.user("If this machine reboots, do my connected integrations survive?");
55+ yield* chat.assistant(
56+ "Let's prove it — register a real integration, reboot the daemon's machine for real, then check it's still there.",
57+ );
58+
59+ const before = yield* client(api, yield* target.newIdentity());
60+
61+ const added = yield* chat.tool(
62+ {
63+ name: "executor call executor.openapi.addSpec",
64+ input: `slug: ${slug}\nspec: inline OpenAPI (GET /ping)`,
65+ result: (a) => `registered — ${a.toolCount} tool(s)`,
66+ },
67+ before.openapi.addSpec({
68+ payload: {
69+ spec: { kind: "blob", value: pingSpec },
70+ slug,
71+ authenticationTemplate: [],
72+ },
73+ }),
74+ );
75+ expect(added.toolCount, "the spec registered with tools").toBeGreaterThan(0);
76+
77+ const listed = yield* chat.tool(
78+ {
79+ name: "executor tools sources",
80+ result: (rows) =>
81+ rows.map((r) => String(r.slug)).includes(slug) ? `${slug} is listed` : "NOT listed",
82+ },
83+ before.integrations.list(),
84+ );
85+ expect(
86+ listed.map((i) => String(i.slug)),
87+ "listed before the reboot",
88+ ).toContain(slug);
89+
90+ // The spinner here runs for the ENTIRE real reboot — the supervised
91+ // service must auto-start at boot for this to ever return.
92+ yield* chat.tool(
93+ {
94+ name: "reboot the daemon's machine",
95+ input: "guest OS reboot — the OS service manager must auto-start the daemon at boot",
96+ result: () => "back online; daemon auto-started",
97+ },
98+ restart(),
99+ );
100+
101+ const after = yield* client(api, yield* target.newIdentity());
102+ yield* Effect.ensuring(
103+ Effect.gen(function* () {
104+ const survived = yield* chat.tool(
105+ {
106+ name: "executor tools sources",
107+ result: (rows) =>
108+ rows.map((r) => String(r.slug)).includes(slug)
109+ ? `${slug} SURVIVED the reboot`
110+ : "VANISHED",
111+ },
112+ after.integrations.list(),
113+ );
114+ expect(
115+ survived.map((i) => String(i.slug)),
116+ "survived the reboot",
117+ ).toContain(slug);
118+ yield* chat.assistant(
119+ "It survived — the OS restarted the daemon at boot and its data was intact.",
120+ );
121+ }),
122+ // Shared guest, but ephemeral; still, never leave the spec behind.
123+ after.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore),
124+ );
125+ }),
126+ );
127+ }),
128+);