@intentius/chant 0.44.14 → 0.46.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 (220) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts +14 -0
  17. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  20. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  21. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  22. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  23. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  24. package/dist/cli/commands/lint.d.ts.map +1 -1
  25. package/dist/cli/commands/onboard.d.ts +28 -0
  26. package/dist/cli/commands/onboard.d.ts.map +1 -1
  27. package/dist/cli/handlers/build.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.d.ts.map +1 -1
  29. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  30. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts.map +1 -1
  32. package/dist/cli/handlers/search.d.ts +27 -0
  33. package/dist/cli/handlers/search.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/registry.d.ts +7 -0
  36. package/dist/cli/registry.d.ts.map +1 -1
  37. package/dist/codegen/docs-pages.d.ts +39 -0
  38. package/dist/codegen/docs-pages.d.ts.map +1 -0
  39. package/dist/codegen/docs-sections.d.ts.map +1 -1
  40. package/dist/codegen/docs-sidebar.d.ts +13 -2
  41. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  42. package/dist/codegen/docs-types.d.ts +30 -10
  43. package/dist/codegen/docs-types.d.ts.map +1 -1
  44. package/dist/codegen/docs.d.ts +14 -2
  45. package/dist/codegen/docs.d.ts.map +1 -1
  46. package/dist/codegen/lexicon-regen.d.ts +11 -0
  47. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  48. package/dist/codegen/surface-snapshot.d.ts +27 -3
  49. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  50. package/dist/codegen/validate.d.ts +21 -3
  51. package/dist/codegen/validate.d.ts.map +1 -1
  52. package/dist/components/capability-plugin.d.ts +13 -2
  53. package/dist/components/capability-plugin.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts +38 -0
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/index.d.ts +1 -1
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/config.d.ts +85 -5
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/discovery/fold-import.d.ts.map +1 -1
  62. package/dist/env.d.ts +12 -1
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/fold/fold.d.ts +100 -1
  65. package/dist/fold/fold.d.ts.map +1 -1
  66. package/dist/graph-ir.d.ts +12 -0
  67. package/dist/graph-ir.d.ts.map +1 -1
  68. package/dist/graph-ops.d.ts +17 -0
  69. package/dist/graph-ops.d.ts.map +1 -0
  70. package/dist/lexicon.d.ts +182 -2
  71. package/dist/lexicon.d.ts.map +1 -1
  72. package/dist/lifecycle/change-set.d.ts +23 -2
  73. package/dist/lifecycle/change-set.d.ts.map +1 -1
  74. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  75. package/dist/lifecycle/index.d.ts +1 -0
  76. package/dist/lifecycle/index.d.ts.map +1 -1
  77. package/dist/lifecycle/observe.d.ts +8 -0
  78. package/dist/lifecycle/observe.d.ts.map +1 -1
  79. package/dist/lifecycle/replay.d.ts.map +1 -1
  80. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  81. package/dist/lifecycle/teardown.d.ts +130 -0
  82. package/dist/lifecycle/teardown.d.ts.map +1 -0
  83. package/dist/lifecycle/types.d.ts +7 -0
  84. package/dist/lifecycle/types.d.ts.map +1 -1
  85. package/dist/lint/engine.d.ts +6 -2
  86. package/dist/lint/engine.d.ts.map +1 -1
  87. package/dist/lint/policy.d.ts.map +1 -1
  88. package/dist/lint/rule.d.ts +31 -0
  89. package/dist/lint/rule.d.ts.map +1 -1
  90. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  91. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  92. package/dist/lint/rules/index.d.ts +2 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/managed-fields.d.ts +33 -21
  95. package/dist/managed-fields.d.ts.map +1 -1
  96. package/dist/observation.d.ts +23 -1
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/op/builders.d.ts +36 -7
  99. package/dist/op/builders.d.ts.map +1 -1
  100. package/dist/op/index.d.ts +1 -1
  101. package/dist/op/index.d.ts.map +1 -1
  102. package/dist/testing.d.ts +136 -0
  103. package/dist/testing.d.ts.map +1 -0
  104. package/dist/yaml.d.ts +0 -8
  105. package/dist/yaml.d.ts.map +1 -1
  106. package/package.json +6 -1
  107. package/src/__snapshots__/okf.test.ts.snap +20 -0
  108. package/src/audit/discover.test.ts +55 -1
  109. package/src/audit/discover.ts +70 -2
  110. package/src/audit/edge-init-safety.test.ts +149 -18
  111. package/src/audit/report-model.ts +12 -1
  112. package/src/audit/rules-doc.ts +1 -0
  113. package/src/build-params.test.ts +52 -0
  114. package/src/cli/build-params-cli.test.ts +25 -8
  115. package/src/cli/build-params-cli.ts +37 -23
  116. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  117. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  118. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  119. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  120. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  121. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  122. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  123. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  124. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  125. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  126. package/src/cli/commands/audit.test.ts +83 -1
  127. package/src/cli/commands/audit.ts +121 -28
  128. package/src/cli/commands/build.test.ts +275 -5
  129. package/src/cli/commands/build.ts +86 -14
  130. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  131. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  132. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  133. package/src/cli/commands/check-lexicon.test.ts +46 -2
  134. package/src/cli/commands/check-lexicon.ts +64 -4
  135. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  136. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  137. package/src/cli/commands/init-lexicon.test.ts +42 -1
  138. package/src/cli/commands/init-lexicon.ts +3 -1
  139. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  140. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  141. package/src/cli/commands/lexicon-surface-diff.ts +10 -1
  142. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  143. package/src/cli/commands/lint.ts +19 -6
  144. package/src/cli/commands/onboard.test.ts +319 -225
  145. package/src/cli/commands/onboard.ts +144 -58
  146. package/src/cli/handlers/build.test.ts +3 -3
  147. package/src/cli/handlers/build.ts +2 -0
  148. package/src/cli/handlers/components.ts +2 -2
  149. package/src/cli/handlers/graph.test.ts +44 -0
  150. package/src/cli/handlers/graph.ts +24 -4
  151. package/src/cli/handlers/lifecycle.test.ts +348 -1
  152. package/src/cli/handlers/lifecycle.ts +235 -3
  153. package/src/cli/handlers/run.test.ts +5 -5
  154. package/src/cli/handlers/run.ts +1 -0
  155. package/src/cli/handlers/search-live.test.ts +217 -0
  156. package/src/cli/handlers/search.test.ts +41 -1
  157. package/src/cli/handlers/search.ts +60 -8
  158. package/src/cli/main.ts +17 -4
  159. package/src/cli/registry.ts +7 -0
  160. package/src/cli/security-boundary.test.ts +2 -1
  161. package/src/codegen/docs-pages.test.ts +194 -0
  162. package/src/codegen/docs-pages.ts +138 -0
  163. package/src/codegen/docs-sections.ts +0 -8
  164. package/src/codegen/docs-sidebar.ts +50 -37
  165. package/src/codegen/docs-types.ts +32 -4
  166. package/src/codegen/docs.ts +78 -40
  167. package/src/codegen/lexicon-regen.ts +22 -2
  168. package/src/codegen/rolling-upgrade.test.ts +1 -1
  169. package/src/codegen/rolling-upgrade.ts +1 -1
  170. package/src/codegen/surface-snapshot.test.ts +139 -0
  171. package/src/codegen/surface-snapshot.ts +297 -122
  172. package/src/codegen/validate.test.ts +49 -3
  173. package/src/codegen/validate.ts +36 -7
  174. package/src/components/auto-release.ts +1 -1
  175. package/src/components/capability-plugin.ts +23 -2
  176. package/src/components/driver.test.ts +45 -0
  177. package/src/components/driver.ts +64 -27
  178. package/src/components/index.ts +2 -0
  179. package/src/components/starter-plugin.ts +5 -2
  180. package/src/config.test.ts +40 -0
  181. package/src/config.ts +159 -8
  182. package/src/discovery/fold-import.test.ts +427 -2
  183. package/src/discovery/fold-import.ts +163 -7
  184. package/src/discovery/sandbox/driver.test.ts +11 -0
  185. package/src/env.test.ts +35 -1
  186. package/src/env.ts +17 -3
  187. package/src/fold/fold.test.ts +41 -0
  188. package/src/fold/fold.ts +372 -2
  189. package/src/graph-ir-live.test.ts +42 -0
  190. package/src/graph-ir.ts +42 -2
  191. package/src/graph-ops.test.ts +69 -0
  192. package/src/graph-ops.ts +42 -0
  193. package/src/lexicon.ts +182 -2
  194. package/src/lifecycle/change-set.test.ts +41 -0
  195. package/src/lifecycle/change-set.ts +30 -2
  196. package/src/lifecycle/deep-observe.ts +4 -0
  197. package/src/lifecycle/index.ts +1 -0
  198. package/src/lifecycle/observe.ts +29 -2
  199. package/src/lifecycle/replay.ts +3 -0
  200. package/src/lifecycle/snapshot.ts +3 -0
  201. package/src/lifecycle/teardown.test.ts +537 -0
  202. package/src/lifecycle/teardown.ts +357 -0
  203. package/src/lifecycle/types.ts +7 -0
  204. package/src/lint/engine.ts +7 -1
  205. package/src/lint/policy.ts +14 -3
  206. package/src/lint/rule.ts +23 -0
  207. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  208. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  209. package/src/lint/rules/index.ts +4 -1
  210. package/src/managed-fields.test.ts +15 -42
  211. package/src/managed-fields.ts +43 -26
  212. package/src/observation.test.ts +24 -8
  213. package/src/observation.ts +48 -4
  214. package/src/op/builders.ts +40 -7
  215. package/src/op/index.ts +1 -1
  216. package/src/ownership.test.ts +63 -1
  217. package/src/testing.test.ts +261 -0
  218. package/src/testing.ts +338 -0
  219. package/src/yaml.test.ts +15 -0
  220. package/src/yaml.ts +19 -2
@@ -2,6 +2,7 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { sep } from "node:path";
3
3
  import { createMockPlugin, staticDescribeResources, staticObservation, staticDeepObservation, staticListArtifacts } from "@intentius/chant-test-utils";
4
4
  import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
5
+ import { deepObservation } from "../../deep-observation";
5
6
  import type { BuildResult } from "../../build";
6
7
  import type { ParsedArgs } from "../registry";
7
8
 
@@ -52,7 +53,7 @@ vi.mock("../../config", async () => {
52
53
  };
53
54
  });
54
55
 
55
- const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
56
+ const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleTeardown, runLifecycleUnknown } = await import("./lifecycle");
56
57
 
57
58
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
58
59
  return {
@@ -300,6 +301,81 @@ describe("runLifecycleDiff --live", () => {
300
301
  expect(readKeys).toContain("loom-agents__aws");
301
302
  });
302
303
 
304
+ // #1264 — each stack declares the region it deploys to. The diff dropped it
305
+ // on the way to the live read, so a multi-region estate compared every stack
306
+ // against the ambient region and reported the out-of-region ones as absent.
307
+ test("multi-stack: each stack's declared region reaches the thin and deep reads", async () => {
308
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
309
+ fetchLifecycleMock.mockResolvedValue(undefined);
310
+ readSnapshotMock.mockResolvedValue(null);
311
+ loadChantConfigMock.mockResolvedValue({ config: { stacks: [
312
+ { name: "app-us-east-1", src: "src/us-east-1", region: "us-east-1" },
313
+ { name: "app-us-west-2", src: "src/us-west-2", region: "us-west-2" },
314
+ ] } });
315
+
316
+ const thinReads: Array<{ stack?: string; region?: string }> = [];
317
+ const deepReads: Array<{ stack?: string; region?: string }> = [];
318
+ const plugins: LexiconPlugin[] = [
319
+ createMockPlugin({
320
+ name: "aws",
321
+ emulator: awsEmulatorStub,
322
+ describeResources: async (options: { stack?: string; region?: string }) => {
323
+ thinReads.push({ stack: options.stack, region: options.region });
324
+ return {};
325
+ },
326
+ observeResourcesDeep: async (options: { stack?: string; region?: string }) => {
327
+ deepReads.push({ stack: options.stack, region: options.region });
328
+ return deepObservation({});
329
+ },
330
+ }),
331
+ ];
332
+
333
+ const exit = await runLifecycleDiff({
334
+ args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
335
+ plugins,
336
+ serializers: plugins.map((p) => p.serializer),
337
+ });
338
+
339
+ expect(exit).toBe(0);
340
+ expect(thinReads).toEqual([
341
+ { stack: "app-us-east-1", region: "us-east-1" },
342
+ { stack: "app-us-west-2", region: "us-west-2" },
343
+ ]);
344
+ expect(deepReads).toEqual([
345
+ { stack: "app-us-east-1", region: "us-east-1" },
346
+ { stack: "app-us-west-2", region: "us-west-2" },
347
+ ]);
348
+ });
349
+
350
+ test("stack without a declared region: no region reaches the live read", async () => {
351
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
352
+ fetchLifecycleMock.mockResolvedValue(undefined);
353
+ readSnapshotMock.mockResolvedValue(null);
354
+ loadChantConfigMock.mockResolvedValue({ config: { stacks: [{ name: "app", src: "src/app" }] } });
355
+
356
+ const reads: Array<Record<string, unknown>> = [];
357
+ const plugins: LexiconPlugin[] = [
358
+ createMockPlugin({
359
+ name: "aws",
360
+ emulator: awsEmulatorStub,
361
+ describeResources: async (options: Record<string, unknown>) => {
362
+ reads.push(options);
363
+ return {};
364
+ },
365
+ }),
366
+ ];
367
+
368
+ await runLifecycleDiff({
369
+ args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
370
+ plugins,
371
+ serializers: plugins.map((p) => p.serializer),
372
+ });
373
+
374
+ expect(reads).toHaveLength(1);
375
+ expect(reads[0].stack).toBe("app");
376
+ expect("region" in reads[0]).toBe(false);
377
+ });
378
+
303
379
  test("warns and skips lexicons without describeResources", async () => {
304
380
  buildMock.mockResolvedValue(makeBuildResult({ k8s: ["pod"] }));
305
381
  fetchLifecycleMock.mockResolvedValue(undefined);
@@ -704,6 +780,47 @@ describe("runLifecyclePlan", () => {
704
780
  expect(web.queried).toBe("/apis/apps/v1/namespaces/default/deployments/web");
705
781
  });
706
782
 
783
+ // #1674 — the plan merges every lexicon's change set into one flat
784
+ // `entries[]`; each row must still say which lexicon observed it, and carry
785
+ // the provider's physical id so an adopt/delete/runtime row named by a live
786
+ // key can be told apart from a declared entity name.
787
+ test("--json carries lexicon attribution and physicalId per entry across the merge", async () => {
788
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"], k8s: ["web"] }));
789
+ const plugins: LexiconPlugin[] = [
790
+ createMockPlugin({
791
+ name: "aws",
792
+ emulator: awsEmulatorStub,
793
+ describeResources: async () => ({
794
+ observation: "v1" as const,
795
+ resources: {
796
+ bucket: meta({ type: "AWS::S3::Bucket", physicalId: "arn:aws:s3:::my-bucket" }),
797
+ "sg-0abc123": meta({ type: "AWS::EC2::SecurityGroup", physicalId: "sg-0abc123", ownership: "foreign" }),
798
+ },
799
+ }),
800
+ }),
801
+ createMockPlugin({
802
+ name: "k8s",
803
+ describeResources: async () => ({
804
+ observation: "v1" as const,
805
+ resources: { web: meta({ type: "K8s::Apps::Deployment", physicalId: "default/web" }) },
806
+ }),
807
+ }),
808
+ ];
809
+ const exit = await runLifecyclePlan({
810
+ args: makeArgs({ path: "plan", extraPositional: "prod", json: true }),
811
+ plugins,
812
+ serializers: plugins.map((p) => p.serializer),
813
+ });
814
+ expect(exit).toBe(0);
815
+ const plan = JSON.parse(stdoutBuf.join("\n"));
816
+ const byName = Object.fromEntries(plan.entries.map((e: { name: string }) => [e.name, e]));
817
+ expect(byName.bucket).toMatchObject({ lexicon: "aws", physicalId: "arn:aws:s3:::my-bucket", action: "noop" });
818
+ expect(byName["sg-0abc123"]).toMatchObject({ lexicon: "aws", physicalId: "sg-0abc123", action: "adopt" });
819
+ expect(byName.web).toMatchObject({ lexicon: "k8s", physicalId: "default/web", action: "noop" });
820
+ // `name` is untouched: the adopt row is still keyed by the live key.
821
+ expect(byName["sg-0abc123"].name).toBe("sg-0abc123");
822
+ });
823
+
707
824
  // #1166 — plan is always a live read (no `--live` flag of its own), so a
708
825
  // declared environment endpoint applies here exactly as it does for
709
826
  // `chant graph --live` / `chant lifecycle diff --live`.
@@ -1103,3 +1220,233 @@ describe("runLifecycleUnknown", () => {
1103
1220
  expect(stderr).toContain("log");
1104
1221
  });
1105
1222
  });
1223
+
1224
+ describe("runLifecycleTeardown (#1222 — plan only)", () => {
1225
+ let stdoutBuf: string[];
1226
+ let stderrBuf: string[];
1227
+
1228
+ beforeEach(() => {
1229
+ stdoutBuf = [];
1230
+ stderrBuf = [];
1231
+ vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
1232
+ vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
1233
+ loadChantConfigMock.mockReset();
1234
+ loadChantConfigMock.mockResolvedValue({
1235
+ config: { environments: ["dev", "prod"], ownership: { stack: "shop", env: "dev" } },
1236
+ });
1237
+ });
1238
+
1239
+ afterEach(() => {
1240
+ vi.restoreAllMocks();
1241
+ });
1242
+
1243
+ const run = (overrides: Partial<ParsedArgs>, plugins: LexiconPlugin[]) =>
1244
+ runLifecycleTeardown({
1245
+ args: makeArgs({ command: "lifecycle", path: "teardown", ...overrides }),
1246
+ plugins,
1247
+ serializers: [],
1248
+ });
1249
+
1250
+ test("requires an environment", async () => {
1251
+ expect(await run({}, [])).toBe(1);
1252
+ expect(stderrBuf.join("\n")).toContain("Environment is required");
1253
+ });
1254
+
1255
+ test("exits nonzero on an environment the project does not declare", async () => {
1256
+ expect(await run({ extraPositional: "staging" }, [])).toBe(1);
1257
+ const stderr = stderrBuf.join("\n");
1258
+ expect(stderr).toContain('Unknown environment "staging"');
1259
+ expect(stderr).toContain("dev, prod");
1260
+ });
1261
+
1262
+ test("--yes executes the planned set and prints per-candidate outcomes", async () => {
1263
+ const executeTeardown = vi.fn(async () => ({
1264
+ outcomes: [{ name: "web", outcome: "deleted" as const }],
1265
+ }));
1266
+ const plugin = createMockPlugin({
1267
+ name: "k8s",
1268
+ teardownOwned: async () => ({
1269
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
1270
+ }),
1271
+ executeTeardown,
1272
+ });
1273
+ expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(0);
1274
+ expect(executeTeardown).toHaveBeenCalledWith({
1275
+ environment: "dev",
1276
+ marker: { stack: "shop", env: "dev" },
1277
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
1278
+ });
1279
+ const out = stdoutBuf.join("\n");
1280
+ expect(out).toContain("Outcomes:");
1281
+ expect(out).toContain("deleted");
1282
+ expect(out).toContain("1 deleted, 0 failed, 0 not prunable, 0 skipped");
1283
+ expect(out).not.toContain("plan only");
1284
+ });
1285
+
1286
+ test("--yes exits nonzero when a candidate stays failed after the retry pass", async () => {
1287
+ const executeTeardown = vi.fn(async () => ({
1288
+ outcomes: [{ name: "web", outcome: "failed" as const, detail: "conflict" }],
1289
+ }));
1290
+ const plugin = createMockPlugin({
1291
+ name: "k8s",
1292
+ teardownOwned: async () => ({
1293
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
1294
+ }),
1295
+ executeTeardown,
1296
+ });
1297
+ expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(1);
1298
+ expect(executeTeardown).toHaveBeenCalledTimes(2); // pass + bounded retry
1299
+ expect(stderrBuf.join("\n")).toContain("failed to delete after the retry pass");
1300
+ });
1301
+
1302
+ test("--yes with a lexicon that only enumerates reports skipped, not deleted", async () => {
1303
+ const plugin = createMockPlugin({
1304
+ name: "gcp",
1305
+ teardownOwned: async () => ({
1306
+ candidates: [{ name: "bucket", type: "GCP::Storage::Bucket", marker: { stack: "shop", env: "dev" } }],
1307
+ }),
1308
+ });
1309
+ expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(0);
1310
+ expect(stdoutBuf.join("\n")).toContain("skipped");
1311
+ expect(stderrBuf.join("\n")).toContain("no teardown execution in these lexicons yet");
1312
+ });
1313
+
1314
+ test("--yes on a production-like environment demands --confirm-prod when not interactive", async () => {
1315
+ const executeTeardown = vi.fn();
1316
+ const plugin = createMockPlugin({
1317
+ name: "k8s",
1318
+ teardownOwned: async () => ({ candidates: [] }),
1319
+ executeTeardown,
1320
+ });
1321
+ expect(await run({ extraPositional: "prod", yes: true }, [plugin])).toBe(1);
1322
+ expect(stderrBuf.join("\n")).toContain("--confirm-prod");
1323
+ expect(executeTeardown).not.toHaveBeenCalled();
1324
+ });
1325
+
1326
+ test("--yes --confirm-prod tears a production-like environment down non-interactively", async () => {
1327
+ const executeTeardown = vi.fn(async () => ({
1328
+ outcomes: [{ name: "web", outcome: "deleted" as const }],
1329
+ }));
1330
+ const plugin = createMockPlugin({
1331
+ name: "k8s",
1332
+ teardownOwned: async () => ({
1333
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "prod" } }],
1334
+ }),
1335
+ executeTeardown,
1336
+ });
1337
+ expect(await run({ extraPositional: "prod", yes: true, confirmProd: true }, [plugin])).toBe(0);
1338
+ expect(executeTeardown).toHaveBeenCalledTimes(1);
1339
+ });
1340
+
1341
+ test("the prod guard never fires without --yes — planning production is safe", async () => {
1342
+ const plugin = createMockPlugin({
1343
+ name: "k8s",
1344
+ teardownOwned: async () => ({ candidates: [] }),
1345
+ });
1346
+ expect(await run({ extraPositional: "prod" }, [plugin])).toBe(0);
1347
+ expect(stderrBuf.join("\n")).not.toContain("--confirm-prod");
1348
+ });
1349
+
1350
+ test("--yes --json emits the execution report", async () => {
1351
+ const plugin = createMockPlugin({
1352
+ name: "k8s",
1353
+ teardownOwned: async () => ({
1354
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
1355
+ }),
1356
+ executeTeardown: async () => ({ outcomes: [{ name: "web", outcome: "deleted" as const }] }),
1357
+ });
1358
+ expect(await run({ extraPositional: "dev", yes: true, json: true }, [plugin])).toBe(0);
1359
+ const parsed = JSON.parse(stdoutBuf.join("\n"));
1360
+ expect(parsed.outcomes).toEqual([
1361
+ {
1362
+ lexicon: "k8s",
1363
+ name: "web",
1364
+ type: "K8s::Apps::Deployment",
1365
+ marker: { stack: "shop", env: "dev" },
1366
+ outcome: "deleted",
1367
+ },
1368
+ ]);
1369
+ });
1370
+
1371
+ test("refuses when the project declares no ownership.stack", async () => {
1372
+ loadChantConfigMock.mockResolvedValue({ config: { environments: ["dev"] } });
1373
+ expect(await run({ extraPositional: "dev" }, [])).toBe(1);
1374
+ expect(stderrBuf.join("\n")).toContain("no ownership.stack");
1375
+ });
1376
+
1377
+ test("prints the would-delete set with resource, type, and marker identity", async () => {
1378
+ const plugin = createMockPlugin({
1379
+ name: "k8s",
1380
+ teardownOwned: async () => ({
1381
+ candidates: [
1382
+ { name: "web", type: "K8s::Apps::Deployment", physicalId: "web-1", marker: { stack: "shop", env: "dev" } },
1383
+ ],
1384
+ }),
1385
+ });
1386
+ expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
1387
+ const out = stdoutBuf.join("\n");
1388
+ expect(out).toContain("Teardown plan");
1389
+ expect(out).toContain("plan only");
1390
+ expect(out).toContain("web");
1391
+ expect(out).toContain("K8s::Apps::Deployment");
1392
+ expect(out).toContain("shop/dev");
1393
+ expect(stderrBuf.join("\n")).toContain("re-run with --yes to execute");
1394
+ });
1395
+
1396
+ test("never selects foreign-stack or foreign-env resources on the fallback path", async () => {
1397
+ const plugin = createMockPlugin({
1398
+ name: "k8s",
1399
+ describeResources: staticObservation({
1400
+ mine: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "shop", env: "dev" } },
1401
+ theirs: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "blog", env: "dev" } },
1402
+ prodTwin: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "shop", env: "prod" } },
1403
+ unmarked: { type: "K8s::Apps::Deployment", status: "READY" },
1404
+ }),
1405
+ });
1406
+ expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
1407
+ const out = stdoutBuf.join("\n");
1408
+ expect(out).toContain("mine");
1409
+ expect(out).not.toContain("theirs");
1410
+ expect(out).not.toContain("prodTwin");
1411
+ expect(out).not.toContain("unmarked");
1412
+ });
1413
+
1414
+ test("an empty would-delete set is loud, not silent", async () => {
1415
+ const plugin = createMockPlugin({
1416
+ name: "k8s",
1417
+ teardownOwned: async () => ({ candidates: [] }),
1418
+ });
1419
+ expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
1420
+ expect(stderrBuf.join("\n")).toContain("nothing would be deleted");
1421
+ });
1422
+
1423
+ test("holes are surfaced loudly (#1089)", async () => {
1424
+ const plugin = createMockPlugin({
1425
+ name: "k8s",
1426
+ teardownOwned: async () => ({
1427
+ candidates: [],
1428
+ holes: [{ name: "crds", type: "K8s::CRD", reason: "unsupported-kind" as const, detail: "no reader" }],
1429
+ }),
1430
+ });
1431
+ expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
1432
+ expect(stderrBuf.join("\n")).toContain("incomplete, not clean");
1433
+ const out = stdoutBuf.join("\n");
1434
+ expect(out).toContain("HOLES");
1435
+ expect(out).toContain("crds");
1436
+ expect(out).toContain("unsupported-kind");
1437
+ });
1438
+
1439
+ test("--json emits the plan as JSON", async () => {
1440
+ const plugin = createMockPlugin({
1441
+ name: "k8s",
1442
+ teardownOwned: async () => ({
1443
+ candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
1444
+ }),
1445
+ });
1446
+ expect(await run({ extraPositional: "dev", json: true }, [plugin])).toBe(0);
1447
+ const parsed = JSON.parse(stdoutBuf.join("\n"));
1448
+ expect(parsed.environment).toBe("dev");
1449
+ expect(parsed.stack).toBe("shop");
1450
+ expect(parsed.entries).toHaveLength(1);
1451
+ });
1452
+ });