1import { LoggingCli, formatPlanLines } from "@/Cli/LoggingCli.ts";
2import { Cli } from "@/Report.ts";
3import { PlatformServices } from "@/Util/PlatformServices.ts";
4import * as Layer from "effect/Layer";
5import { describe, expect, it, test } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Logger from "effect/Logger";
8import {
9 createNode,
10 deleteNode,
11 planWith,
12 replaceNode,
13 updateNode,
14} from "./PlanTestNodes.ts";
15
16describe("formatPlanLines", { tags: ["unit", "local"] }, () => {
17 test("keeps compact output unchanged by default", () => {
18 expect(
19 formatPlanLines(
20 planWith([
21 updateNode({ value: "old" }, { value: "new" }, "First"),
22 replaceNode({ engine: "v1" }, { engine: "v2" }, "Second"),
23 ]),
24 ),
25 ).toEqual([
26 "Plan: 1 to update, 1 to replace",
27 "[First] update",
28 "[Second] replace",
29 ]);
30 });
31
32 test("renders detailed creates and updates as YAML", () => {
33 const output = formatPlanLines(
34 planWith([
35 createNode({ region: "iad", ports: [80, 443] }, "Api"),
36 updateNode({ retries: 2 }, { retries: 3 }, "Worker"),
37 ]),
38 { detailed: true },
39 ).join("\n");
40 expect(output).toContain(" properties:\n ports:\n - 80");
41 expect(output).toContain(" properties:\n - retries: 2");
42 expect(output).toContain(" + retries: 3");
43 });
44
45 test("keeps deletes compact and reports non-property replacements", () => {
46 const output = formatPlanLines(
47 planWith(
48 [replaceNode({ name: "same" }, { name: "same" })],
49 [deleteNode({ secret: "old" }, "OldWorker")],
50 ),
51 { detailed: true },
52 ).join("\n");
53 expect(output).toContain("no declared property changes");
54 expect(output).not.toContain("secret:");
55 });
56});
57
58it.effect(
59 "emits plain progress through the Effect logger",
60 () => {
61 const messages: unknown[] = [];
62 const logger = Logger.make<unknown, void>((options) => {
63 messages.push(options.message);
64 });
65 return Effect.gen(function* () {
66 const cli = yield* Cli;
67
68 yield* cli.displayPlan(planWith([createNode({}, "Worker")]));
69
70 expect(messages).toEqual([["Plan: 1 to create"], ["[Worker] create"]]);
71 }).pipe(
72 Effect.provide(Layer.provide(LoggingCli, PlatformServices)),
73 Effect.provide(Logger.layer([logger])),
74 );
75 },
76 { tags: ["unit", "local"] },
77);
78
79it.effect(
80 "prints dev stack outputs with the plain apply renderer",
81 () => {
82 const messages: unknown[] = [];
83 return Effect.gen(function* () {
84 const cli = yield* Cli;
85 const session = yield* cli.startApplySession(planWith([]), { dev: true });
86 expect(session.setOutput).toBeDefined();
87 yield* session.setOutput!({ api: "http://localhost:1337/" });
88 expect(messages).toContainEqual(["{ api: 'http://localhost:1337/' }"]);
89 }).pipe(
90 Effect.provide(Layer.provide(LoggingCli, PlatformServices)),
91 Effect.provide(
92 Logger.layer([
93 Logger.make<unknown, void>((options) => {
94 messages.push(options.message);
95 }),
96 ]),
97 ),
98 );
99 },
100 { tags: ["unit", "local"] },
101);
102
103it.effect(
104 "streams apply notes as they arrive",
105 () => {
106 const messages: string[] = [];
107 const logger = Logger.make<unknown, void>((options) => {
108 messages.push(String((options.message as unknown[])[0]));
109 });
110 return Effect.gen(function* () {
111 const cli = yield* Cli;
112 const session = yield* cli.startApplySession(
113 planWith([updateNode({ v: 1 }, { v: 2 }, "Website")]),
114 );
115
116 const note = (message: string, kind?: "status" | "output") =>
117 session.emit({
118 _tag: "apply.resource.note",
119 fqn: "Website",
120 id: "Website",
121 message,
122 kind,
123 });
124 const status = (status: "updating" | "updated") =>
125 session.emit({
126 _tag: "apply.resource.status",
127 fqn: "Website",
128 id: "Website",
129 type: "Cloudflare::Worker",
130 status,
131 });
132
133 yield* status("updating");
134 yield* note("Uploading worker (1.2 MB) ...", "status");
135 yield* note("build output line", "output");
136 yield* note("Uploaded 0 of 5195 assets...");
137 yield* note("Uploaded 490 of 5195 assets...");
138 // spinner-style refresh of the same message is deduped
139 yield* note("Uploaded 490 of 5195 assets...");
140 yield* note("Reconciling custom domains (1) ...", "status");
141 yield* status("updated");
142
143 const progress = messages.slice(3); // skip plan preview + blank line
144 expect(progress).toEqual([
145 "[Website] updating",
146 "[Website] build output line",
147 "[Website] Uploaded 0 of 5195 assets...",
148 "[Website] Uploaded 490 of 5195 assets...",
149 "[Website] updated — Reconciling custom domains (1) ... (0ms)",
150 ]);
151 }).pipe(
152 Effect.provide(Layer.provide(LoggingCli, PlatformServices)),
153 Effect.provide(Logger.layer([logger])),
154 );
155 },
156 { tags: ["unit", "local"] },
157);