| 1 | import { LoggingCli, formatPlanLines } from "@/Cli/LoggingCli.ts"; |
| 2 | import { Cli } from "@/Report.ts"; |
| 3 | import { PlatformServices } from "@/Util/PlatformServices.ts"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import { describe, expect, it, test } from "alchemy-test"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Logger from "effect/Logger"; |
| 8 | import { |
| 9 | createNode, |
| 10 | deleteNode, |
| 11 | planWith, |
| 12 | replaceNode, |
| 13 | updateNode, |
| 14 | } from "./PlanTestNodes.ts"; |
| 15 | |
| 16 | describe("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 | |
| 58 | it.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 | |
| 79 | it.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 | |
| 103 | it.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 | ); |