@decocms/blocks-cli 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. package/tsconfig.json +7 -0
@@ -0,0 +1,446 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import {
6
+ auditFleetBindings,
7
+ auditObservabilityBlock,
8
+ auditWranglerConfig,
9
+ } from "./audit-observability-config";
10
+ import { parseJsonc, stripJsoncTrailingCommas } from "./lib/jsonc";
11
+
12
+ describe("auditObservabilityBlock", () => {
13
+ it("flags missing observability block as error", () => {
14
+ const findings = auditObservabilityBlock(undefined);
15
+ expect(findings).toHaveLength(1);
16
+ expect(findings[0].id).toBe("observability_missing");
17
+ expect(findings[0].severity).toBe("error");
18
+ expect(findings[0].fix).toContain("deco-cf-observability --write");
19
+ });
20
+
21
+ it("returns no findings for canonical block (traces 0.01, logs 1, persist)", () => {
22
+ const findings = auditObservabilityBlock({
23
+ enabled: true,
24
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
25
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
26
+ });
27
+ expect(findings).toEqual([]);
28
+ });
29
+
30
+ it("flags head_sampling_rate_elevated as error when traces rate > 0.01", () => {
31
+ const findings = auditObservabilityBlock({
32
+ enabled: true,
33
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
34
+ traces: { enabled: true, head_sampling_rate: 0.1, persist: true },
35
+ });
36
+ const elevated = findings.find((f) => f.id === "head_sampling_rate_elevated");
37
+ expect(elevated).toBeDefined();
38
+ expect(elevated?.severity).toBe("error");
39
+ expect(elevated?.message).toContain("0.1");
40
+ expect(elevated?.fix).toContain("--traces-rate 0.01");
41
+ });
42
+
43
+ it("does not flag boundary value 0.01", () => {
44
+ const findings = auditObservabilityBlock({
45
+ enabled: true,
46
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
47
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
48
+ });
49
+ expect(findings.find((f) => f.id === "head_sampling_rate_elevated")).toBeUndefined();
50
+ });
51
+
52
+ it("flags observability_disabled when enabled: false at the top", () => {
53
+ const findings = auditObservabilityBlock({
54
+ enabled: false,
55
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
56
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
57
+ });
58
+ const f = findings.find((x) => x.id === "observability_disabled");
59
+ expect(f?.severity).toBe("error");
60
+ });
61
+
62
+ it("flags traces_disabled and logs_disabled separately", () => {
63
+ const findings = auditObservabilityBlock({
64
+ enabled: true,
65
+ logs: { enabled: false, head_sampling_rate: 1, persist: true },
66
+ traces: { enabled: false, head_sampling_rate: 0.01, persist: true },
67
+ });
68
+ expect(findings.some((f) => f.id === "traces_disabled" && f.severity === "warn")).toBe(true);
69
+ expect(findings.some((f) => f.id === "logs_disabled" && f.severity === "warn")).toBe(true);
70
+ });
71
+
72
+ it("flags logs_head_sampling_rate_low when logs rate < 1", () => {
73
+ const findings = auditObservabilityBlock({
74
+ enabled: true,
75
+ logs: { enabled: true, head_sampling_rate: 0.5, persist: true },
76
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
77
+ });
78
+ const f = findings.find((x) => x.id === "logs_head_sampling_rate_low");
79
+ expect(f?.severity).toBe("warn");
80
+ });
81
+
82
+ it("flags persist_disabled_no_destination on traces", () => {
83
+ const findings = auditObservabilityBlock({
84
+ enabled: true,
85
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
86
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: false },
87
+ });
88
+ const f = findings.find((x) => x.id === "persist_disabled_no_destination");
89
+ expect(f?.severity).toBe("error");
90
+ });
91
+
92
+ it("accepts persist:false when a destination is configured", () => {
93
+ const findings = auditObservabilityBlock({
94
+ enabled: true,
95
+ logs: {
96
+ enabled: true,
97
+ head_sampling_rate: 1,
98
+ persist: false,
99
+ destinations: [{ id: "my-logs-dest" }],
100
+ },
101
+ traces: {
102
+ enabled: true,
103
+ head_sampling_rate: 0.01,
104
+ persist: false,
105
+ destinations: [{ id: "my-traces-dest" }],
106
+ },
107
+ });
108
+ expect(findings).toEqual([]);
109
+ });
110
+
111
+ it("does not flag persist on a disabled traces block", () => {
112
+ const findings = auditObservabilityBlock({
113
+ enabled: true,
114
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
115
+ traces: { enabled: false, head_sampling_rate: 0.01, persist: false },
116
+ });
117
+ // traces_disabled fires, but not persist_disabled_no_destination for traces
118
+ expect(findings.find((f) => f.id === "persist_disabled_no_destination")).toBeUndefined();
119
+ });
120
+
121
+ it("treats absent head_sampling_rate as not-elevated (no false positive)", () => {
122
+ const findings = auditObservabilityBlock({
123
+ enabled: true,
124
+ logs: { enabled: true, persist: true },
125
+ traces: { enabled: true, persist: true },
126
+ });
127
+ expect(findings.find((f) => f.id === "head_sampling_rate_elevated")).toBeUndefined();
128
+ expect(findings.find((f) => f.id === "logs_head_sampling_rate_low")).toBeUndefined();
129
+ });
130
+
131
+ it("stacks multiple findings on a deeply-broken config", () => {
132
+ const findings = auditObservabilityBlock({
133
+ enabled: true,
134
+ logs: { enabled: true, head_sampling_rate: 0.1, persist: false },
135
+ traces: { enabled: true, head_sampling_rate: 0.5, persist: false },
136
+ });
137
+ const ids = findings.map((f) => f.id).sort();
138
+ expect(ids).toContain("head_sampling_rate_elevated");
139
+ expect(ids).toContain("logs_head_sampling_rate_low");
140
+ // both traces and logs each emit a persist_disabled_no_destination
141
+ expect(ids.filter((i) => i === "persist_disabled_no_destination")).toHaveLength(2);
142
+ });
143
+ });
144
+
145
+ describe("auditFleetBindings (D-14)", () => {
146
+ const canonicalBindings = {
147
+ version_metadata: { binding: "CF_VERSION_METADATA" },
148
+ analytics_engine_datasets: [{ binding: "DECO_METRICS", dataset: "deco_metrics_site" }],
149
+ tail_consumers: [{ service: "deco-otel-tail" }],
150
+ vars: {
151
+ DECO_OTEL_METRICS_ENDPOINT: "https://deco-otel-ingest.example/v1/metrics",
152
+ DECO_OTEL_TRACES_ENDPOINT: "https://deco-otel-ingest.example/v1/traces",
153
+ DECO_OTEL_LOGS_ENDPOINT: "https://deco-otel-ingest.example/v1/logs",
154
+ },
155
+ };
156
+
157
+ it("returns no findings for canonical bindings", () => {
158
+ expect(auditFleetBindings(canonicalBindings)).toEqual([]);
159
+ });
160
+
161
+ it("flags version_metadata_binding_missing as error", () => {
162
+ const { version_metadata: _, ...rest } = canonicalBindings;
163
+ const findings = auditFleetBindings(rest);
164
+ const f = findings.find((x) => x.id === "version_metadata_binding_missing");
165
+ expect(f?.severity).toBe("error");
166
+ });
167
+
168
+ it("flags version_metadata_binding_missing when binding is empty", () => {
169
+ const findings = auditFleetBindings({
170
+ ...canonicalBindings,
171
+ version_metadata: { binding: "" },
172
+ });
173
+ const f = findings.find((x) => x.id === "version_metadata_binding_missing");
174
+ expect(f).toBeDefined();
175
+ });
176
+
177
+ it("flags analytics_engine_binding_missing as warn", () => {
178
+ const findings = auditFleetBindings({
179
+ ...canonicalBindings,
180
+ analytics_engine_datasets: [],
181
+ });
182
+ const f = findings.find((x) => x.id === "analytics_engine_binding_missing");
183
+ expect(f?.severity).toBe("warn");
184
+ });
185
+
186
+ it("flags analytics_engine_binding_missing when binding name doesn't match DECO_METRICS", () => {
187
+ const findings = auditFleetBindings({
188
+ ...canonicalBindings,
189
+ analytics_engine_datasets: [{ binding: "OTHER_NAME" }],
190
+ });
191
+ expect(findings.some((f) => f.id === "analytics_engine_binding_missing")).toBe(true);
192
+ });
193
+
194
+ it("flags tail_consumer_missing as error", () => {
195
+ const findings = auditFleetBindings({
196
+ ...canonicalBindings,
197
+ tail_consumers: [],
198
+ });
199
+ const f = findings.find((x) => x.id === "tail_consumer_missing");
200
+ expect(f?.severity).toBe("error");
201
+ });
202
+
203
+ it("flags tail_consumer_missing when an unrelated tail consumer is configured", () => {
204
+ const findings = auditFleetBindings({
205
+ ...canonicalBindings,
206
+ tail_consumers: [{ service: "another-tail" }],
207
+ });
208
+ expect(findings.some((f) => f.id === "tail_consumer_missing")).toBe(true);
209
+ });
210
+
211
+ it("flags otel_metrics_endpoint_missing when DECO_OTEL_METRICS_ENDPOINT is unset", () => {
212
+ const findings = auditFleetBindings({
213
+ ...canonicalBindings,
214
+ vars: {
215
+ ...canonicalBindings.vars,
216
+ DECO_OTEL_METRICS_ENDPOINT: "",
217
+ },
218
+ });
219
+ expect(findings.some((f) => f.id === "otel_metrics_endpoint_missing")).toBe(true);
220
+ });
221
+
222
+ it("flags otel_traces_endpoint_missing when DECO_OTEL_TRACES_ENDPOINT is missing", () => {
223
+ const { vars: _vars, ...rest } = canonicalBindings;
224
+ const findings = auditFleetBindings(rest);
225
+ expect(findings.some((f) => f.id === "otel_traces_endpoint_missing")).toBe(true);
226
+ expect(findings.some((f) => f.id === "otel_logs_endpoint_missing")).toBe(true);
227
+ expect(findings.some((f) => f.id === "otel_metrics_endpoint_missing")).toBe(true);
228
+ });
229
+
230
+ it("handles missing vars object gracefully", () => {
231
+ expect(() => auditFleetBindings({ vars: undefined })).not.toThrow();
232
+ });
233
+ });
234
+
235
+ describe("auditWranglerConfig — composition", () => {
236
+ it("composes observability + fleet rules", () => {
237
+ const findings = auditWranglerConfig({});
238
+ const ids = findings.map((f) => f.id);
239
+ expect(ids).toContain("observability_missing");
240
+ expect(ids).toContain("version_metadata_binding_missing");
241
+ expect(ids).toContain("tail_consumer_missing");
242
+ });
243
+
244
+ it("returns no findings on a fully canonical wrangler", () => {
245
+ const findings = auditWranglerConfig({
246
+ observability: {
247
+ enabled: true,
248
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
249
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
250
+ },
251
+ version_metadata: { binding: "CF_VERSION_METADATA" },
252
+ analytics_engine_datasets: [{ binding: "DECO_METRICS", dataset: "deco_metrics_x" }],
253
+ tail_consumers: [{ service: "deco-otel-tail" }],
254
+ vars: {
255
+ DECO_OTEL_METRICS_ENDPOINT: "https://ingest.example/v1/metrics",
256
+ DECO_OTEL_TRACES_ENDPOINT: "https://ingest.example/v1/traces",
257
+ DECO_OTEL_LOGS_ENDPOINT: "https://ingest.example/v1/logs",
258
+ },
259
+ });
260
+ expect(findings).toEqual([]);
261
+ });
262
+ });
263
+
264
+ describe("JSONC handling — trailing commas + comments", () => {
265
+ it("stripJsoncTrailingCommas removes commas before `}` and `]`", () => {
266
+ expect(stripJsoncTrailingCommas(`{ "a": 1, "b": 2, }`)).toBe(`{ "a": 1, "b": 2 }`);
267
+ expect(stripJsoncTrailingCommas(`{ "a": [1, 2, 3,], }`)).toBe(`{ "a": [1, 2, 3] }`);
268
+ });
269
+
270
+ it("stripJsoncTrailingCommas preserves commas INSIDE string literals", () => {
271
+ expect(stripJsoncTrailingCommas(`{ "a": "hello,], world", }`)).toBe(
272
+ `{ "a": "hello,], world" }`,
273
+ );
274
+ });
275
+
276
+ it("parseJsonc accepts both line comments and trailing commas", () => {
277
+ const src = `{
278
+ // a wrangler.jsonc-style config
279
+ "observability": {
280
+ "enabled": true,
281
+ "traces": { "enabled": true, "head_sampling_rate": 0.01, "persist": true, },
282
+ "logs": { "enabled": true, "head_sampling_rate": 1, "persist": true, },
283
+ },
284
+ }`;
285
+ expect(parseJsonc<{ observability: { enabled: boolean } }>(src).observability.enabled).toBe(
286
+ true,
287
+ );
288
+ });
289
+ });
290
+
291
+ describe("CLI gate hardness (D-16) — --mode warn|block + --github", () => {
292
+ let tmpdir: string;
293
+ const cliPath = path.resolve(__dirname, "audit-observability-config.ts");
294
+
295
+ beforeEach(() => {
296
+ tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "audit-mode-"));
297
+ });
298
+ afterEach(() => {
299
+ fs.rmSync(tmpdir, { recursive: true, force: true });
300
+ });
301
+
302
+ // Spawn the script via tsx in a child process so we exercise the real
303
+ // `process.exit()` paths instead of monkey-patching them. This is the
304
+ // contract storefront CI consumes, so it's the contract under test.
305
+ function runCli(args: string[]): {
306
+ status: number | null;
307
+ stdout: string;
308
+ stderr: string;
309
+ } {
310
+ const { spawnSync } = require("node:child_process") as typeof import(
311
+ "node:child_process"
312
+ );
313
+ const result = spawnSync(
314
+ process.execPath,
315
+ [
316
+ require.resolve("tsx/cli"),
317
+ cliPath,
318
+ "--source",
319
+ tmpdir,
320
+ ...args,
321
+ ],
322
+ { encoding: "utf8" },
323
+ );
324
+ return {
325
+ status: result.status,
326
+ stdout: result.stdout,
327
+ stderr: result.stderr,
328
+ };
329
+ }
330
+
331
+ it("default mode is warn — exits 0 even with error findings", () => {
332
+ // Empty wrangler triggers `observability_missing` (error) +
333
+ // `tail_consumer_missing` (error) + `version_metadata_*` (error). Warn
334
+ // mode must annotate but exit 0.
335
+ fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), "{}");
336
+ const { status, stdout } = runCli([]);
337
+ expect(status).toBe(0);
338
+ expect(stdout).toMatch(/observability_missing/);
339
+ });
340
+
341
+ it("--mode block exits 1 when an error-severity finding is present", () => {
342
+ fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), "{}");
343
+ const { status, stdout } = runCli(["--mode", "block"]);
344
+ expect(status).toBe(1);
345
+ expect(stdout).toMatch(/observability_missing/);
346
+ });
347
+
348
+ it("--mode block exits 0 when only warn-severity findings are present", () => {
349
+ // Canonical observability block + the rest of the fleet bindings → only
350
+ // the DECO_OTEL_*_ENDPOINT warns survive. Block mode must exit 0 because
351
+ // those are `warn`, not `error`.
352
+ fs.writeFileSync(
353
+ path.join(tmpdir, "wrangler.jsonc"),
354
+ JSON.stringify({
355
+ name: "my-store",
356
+ observability: {
357
+ enabled: true,
358
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
359
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
360
+ },
361
+ version_metadata: { binding: "CF_VERSION_METADATA" },
362
+ analytics_engine_datasets: [{ binding: "DECO_METRICS" }],
363
+ tail_consumers: [{ service: "deco-otel-tail" }],
364
+ }),
365
+ );
366
+ const { status } = runCli(["--mode", "block"]);
367
+ expect(status).toBe(0);
368
+ });
369
+
370
+ it("--mode block exits 0 on a fully clean wrangler.jsonc", () => {
371
+ fs.writeFileSync(
372
+ path.join(tmpdir, "wrangler.jsonc"),
373
+ JSON.stringify({
374
+ name: "my-store",
375
+ observability: {
376
+ enabled: true,
377
+ traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
378
+ logs: { enabled: true, head_sampling_rate: 1, persist: true },
379
+ },
380
+ version_metadata: { binding: "CF_VERSION_METADATA" },
381
+ analytics_engine_datasets: [{ binding: "DECO_METRICS" }],
382
+ tail_consumers: [{ service: "deco-otel-tail" }],
383
+ vars: {
384
+ DECO_OTEL_METRICS_ENDPOINT: "https://ingest.example.com",
385
+ DECO_OTEL_TRACES_ENDPOINT: "https://ingest.example.com",
386
+ DECO_OTEL_LOGS_ENDPOINT: "https://ingest.example.com",
387
+ },
388
+ }),
389
+ );
390
+ const { status } = runCli(["--mode", "block"]);
391
+ expect(status).toBe(0);
392
+ });
393
+
394
+ it("--github emits ::warning::/::error:: annotations matched to mode", () => {
395
+ fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), "{}");
396
+ // In warn mode, even error-severity findings annotate as `warning` (we
397
+ // never escalate to GitHub `error` annotations when we won't fail the
398
+ // check — keeps the PR annotation channel quiet at v1).
399
+ const warnRun = runCli(["--github"]);
400
+ expect(warnRun.status).toBe(0);
401
+ expect(warnRun.stdout).toMatch(/::warning title=observability_missing::/);
402
+ expect(warnRun.stdout).not.toMatch(/::error title=/);
403
+
404
+ // In block mode, error-severity findings escalate to `::error::`.
405
+ const blockRun = runCli(["--mode", "block", "--github"]);
406
+ expect(blockRun.status).toBe(1);
407
+ expect(blockRun.stdout).toMatch(/::error title=observability_missing::/);
408
+ });
409
+
410
+ it("--mode rejects values other than warn|block with exit 2", () => {
411
+ fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), "{}");
412
+ const { status, stderr } = runCli(["--mode", "advisory"]);
413
+ expect(status).toBe(2);
414
+ expect(stderr).toMatch(/--mode must be "warn" or "block"/);
415
+ });
416
+ });
417
+
418
+ describe("CLI smoke — wrangler.jsonc with trailing commas", () => {
419
+ let tmpdir: string;
420
+ beforeEach(() => {
421
+ tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "audit-jsonc-"));
422
+ });
423
+ afterEach(() => {
424
+ fs.rmSync(tmpdir, { recursive: true, force: true });
425
+ });
426
+
427
+ it("audits a canonical wrangler.jsonc containing trailing commas without parse failure", () => {
428
+ const src = `{
429
+ // canonical, with trailing commas — common in real wrangler.jsonc files
430
+ "name": "my-store",
431
+ "observability": {
432
+ "enabled": true,
433
+ "traces": { "enabled": true, "head_sampling_rate": 0.01, "persist": true, },
434
+ "logs": { "enabled": true, "head_sampling_rate": 1, "persist": true, },
435
+ },
436
+ }`;
437
+ fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), src);
438
+ // The audit's pure function still works against the parsed shape; this
439
+ // test guards the parse step itself, which previously threw on the
440
+ // trailing commas.
441
+ const parsed = parseJsonc<{
442
+ observability?: Parameters<typeof auditObservabilityBlock>[0];
443
+ }>(src);
444
+ expect(auditObservabilityBlock(parsed.observability)).toEqual([]);
445
+ });
446
+ });