@intentius/chant 0.27.0 → 0.29.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 (106) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/components.d.ts.map +1 -1
  4. package/dist/cli/handlers/graph.d.ts.map +1 -1
  5. package/dist/cli/handlers/lifecycle.d.ts +5 -3
  6. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  7. package/dist/config.d.ts +46 -4
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/discovery/fold-import.d.ts +153 -17
  10. package/dist/discovery/fold-import.d.ts.map +1 -1
  11. package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
  12. package/dist/discovery/sandbox/config-wire.d.ts +3 -2
  13. package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
  14. package/dist/discovery/sandbox/fork.d.ts +25 -0
  15. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  16. package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
  17. package/dist/discovery/sandbox/run.d.ts.map +1 -1
  18. package/dist/env.d.ts +5 -2
  19. package/dist/env.d.ts.map +1 -1
  20. package/dist/fold/fold.d.ts +12 -0
  21. package/dist/fold/fold.d.ts.map +1 -1
  22. package/dist/graph-ir.d.ts +29 -4
  23. package/dist/graph-ir.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/kubectl-context.d.ts +100 -0
  27. package/dist/kubectl-context.d.ts.map +1 -0
  28. package/dist/lexicon.d.ts +31 -6
  29. package/dist/lexicon.d.ts.map +1 -1
  30. package/dist/lifecycle/change-set.d.ts +26 -5
  31. package/dist/lifecycle/change-set.d.ts.map +1 -1
  32. package/dist/lifecycle/live-diff.d.ts +25 -1
  33. package/dist/lifecycle/live-diff.d.ts.map +1 -1
  34. package/dist/lifecycle/observe.d.ts +4 -2
  35. package/dist/lifecycle/observe.d.ts.map +1 -1
  36. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  37. package/dist/lifecycle/status.d.ts +26 -1
  38. package/dist/lifecycle/status.d.ts.map +1 -1
  39. package/dist/lifecycle/types.d.ts +8 -0
  40. package/dist/lifecycle/types.d.ts.map +1 -1
  41. package/dist/lint/config.d.ts +80 -0
  42. package/dist/lint/config.d.ts.map +1 -1
  43. package/dist/lint/policy.d.ts +8 -2
  44. package/dist/lint/policy.d.ts.map +1 -1
  45. package/dist/lint/post-synth.d.ts +18 -1
  46. package/dist/lint/post-synth.d.ts.map +1 -1
  47. package/dist/live-endpoint.d.ts +92 -0
  48. package/dist/live-endpoint.d.ts.map +1 -0
  49. package/dist/observation.d.ts +123 -0
  50. package/dist/observation.d.ts.map +1 -0
  51. package/dist/stack-output.d.ts +9 -4
  52. package/dist/stack-output.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/cli/commands/build.test.ts +190 -0
  55. package/src/cli/commands/build.ts +34 -2
  56. package/src/cli/commands/lint.ts +17 -25
  57. package/src/cli/handlers/components.test.ts +63 -4
  58. package/src/cli/handlers/components.ts +78 -35
  59. package/src/cli/handlers/graph.test.ts +69 -6
  60. package/src/cli/handlers/graph.ts +61 -27
  61. package/src/cli/handlers/lifecycle.test.ts +285 -6
  62. package/src/cli/handlers/lifecycle.ts +297 -185
  63. package/src/config.test.ts +75 -0
  64. package/src/config.ts +61 -3
  65. package/src/discovery/fold-composite.test.ts +594 -0
  66. package/src/discovery/fold-import.ts +987 -43
  67. package/src/discovery/sandbox/config-boundary.test.ts +55 -1
  68. package/src/discovery/sandbox/config-run.ts +3 -0
  69. package/src/discovery/sandbox/config-wire.ts +3 -2
  70. package/src/discovery/sandbox/fork.ts +75 -1
  71. package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
  72. package/src/discovery/sandbox/policy-run.ts +15 -1
  73. package/src/discovery/sandbox/run.test.ts +85 -1
  74. package/src/discovery/sandbox/run.ts +3 -0
  75. package/src/env.test.ts +12 -0
  76. package/src/env.ts +12 -4
  77. package/src/fold/fold.ts +12 -2
  78. package/src/graph-ir-live.test.ts +28 -1
  79. package/src/graph-ir.ts +68 -12
  80. package/src/index.ts +1 -0
  81. package/src/kubectl-context.test.ts +94 -0
  82. package/src/kubectl-context.ts +207 -0
  83. package/src/lexicon.ts +41 -6
  84. package/src/lifecycle/change-set.test.ts +93 -1
  85. package/src/lifecycle/change-set.ts +65 -13
  86. package/src/lifecycle/live-diff.test.ts +39 -0
  87. package/src/lifecycle/live-diff.ts +51 -5
  88. package/src/lifecycle/observe.test.ts +74 -3
  89. package/src/lifecycle/observe.ts +82 -22
  90. package/src/lifecycle/snapshot.test.ts +39 -1
  91. package/src/lifecycle/snapshot.ts +34 -9
  92. package/src/lifecycle/status.test.ts +89 -8
  93. package/src/lifecycle/status.ts +53 -3
  94. package/src/lifecycle/types.ts +8 -0
  95. package/src/lint/config.test.ts +93 -1
  96. package/src/lint/config.ts +108 -0
  97. package/src/lint/policy.test.ts +90 -0
  98. package/src/lint/policy.ts +17 -5
  99. package/src/lint/post-synth.test.ts +4 -0
  100. package/src/lint/post-synth.ts +30 -1
  101. package/src/live-endpoint.test.ts +115 -0
  102. package/src/live-endpoint.ts +148 -0
  103. package/src/observation.test.ts +96 -0
  104. package/src/observation.ts +213 -0
  105. package/src/stack-output.test.ts +76 -3
  106. package/src/stack-output.ts +59 -26
@@ -1,6 +1,6 @@
1
- import { describe, test, expect, vi, beforeEach } from "vitest";
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { sep } from "node:path";
3
- import { createMockPlugin, staticDescribeResources, staticListArtifacts } from "@intentius/chant-test-utils";
3
+ import { createMockPlugin, staticDescribeResources, staticObservation, staticListArtifacts } from "@intentius/chant-test-utils";
4
4
  import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
5
5
  import type { BuildResult } from "../../build";
6
6
  import type { ParsedArgs } from "../registry";
@@ -24,11 +24,15 @@ vi.mock("../../lifecycle/git", () => ({
24
24
  vi.mock("../../lifecycle/snapshot", () => ({
25
25
  takeSnapshot: (...args: unknown[]) => takeSnapshotMock(...args),
26
26
  }));
27
- vi.mock("../../config", () => ({
28
- loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
29
- }));
27
+ vi.mock("../../config", async () => {
28
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
29
+ return {
30
+ ...actual,
31
+ loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
32
+ };
33
+ });
30
34
 
31
- const { runLifecycleDiff, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
35
+ const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
32
36
 
33
37
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
34
38
  return {
@@ -128,6 +132,64 @@ describe("runLifecycleDiff --live", () => {
128
132
  expect(output).toContain("UPDATE_COMPLETE");
129
133
  });
130
134
 
135
+ // #1089 — a hole in the read is rendered as a hole, and never silently
136
+ // inflates the missing/drift counts that the all-clear line reads.
137
+ test("renders an UNOBSERVED section and qualifies the all-clear", async () => {
138
+ buildMock.mockResolvedValue(makeBuildResult({ k8s: ["widget"] }));
139
+ fetchLifecycleMock.mockResolvedValue(undefined);
140
+ readSnapshotMock.mockResolvedValue(null);
141
+
142
+ const plugins: LexiconPlugin[] = [
143
+ createMockPlugin({
144
+ name: "k8s",
145
+ describeResources: staticObservation({}, {
146
+ widget: { type: "K8s::X::Widget", reason: "unsupported-kind", detail: "no kubectl mapping" },
147
+ }),
148
+ }),
149
+ ];
150
+
151
+ const exit = await runLifecycleDiff({
152
+ args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
153
+ plugins,
154
+ serializers: plugins.map((p) => p.serializer),
155
+ });
156
+
157
+ expect(exit).toBe(0);
158
+ const stdout = stdoutBuf.join("\n");
159
+ expect(stdout).toContain("UNOBSERVED");
160
+ expect(stdout).toContain("widget");
161
+ expect(stdout).toContain("no reader for this resource kind");
162
+ // Not counted as missing — "declared, not in cloud" is a claim we can't make.
163
+ expect(stdout).toContain("0 missing");
164
+ expect(stderrBuf.join("\n")).toContain("could not be observed");
165
+ });
166
+
167
+ test("a throwing describeResources reports its entities unobserved instead of skipping the lexicon", async () => {
168
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
169
+ fetchLifecycleMock.mockResolvedValue(undefined);
170
+ readSnapshotMock.mockResolvedValue(null);
171
+
172
+ const plugins: LexiconPlugin[] = [
173
+ createMockPlugin({
174
+ name: "aws",
175
+ describeResources: async () => { throw new Error("Unable to locate credentials"); },
176
+ }),
177
+ ];
178
+
179
+ const exit = await runLifecycleDiff({
180
+ args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
181
+ plugins,
182
+ serializers: plugins.map((p) => p.serializer),
183
+ });
184
+
185
+ expect(exit).toBe(0);
186
+ expect(stderrBuf.join("\n")).toContain("not as absent");
187
+ const stdout = stdoutBuf.join("\n");
188
+ expect(stdout).toContain("UNOBSERVED");
189
+ expect(stdout).toContain("bucket");
190
+ expect(stdout).toContain("0 missing");
191
+ });
192
+
131
193
  test("builds from config.sourceDir on a mixed-layout project", async () => {
132
194
  buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
133
195
  fetchLifecycleMock.mockResolvedValue(undefined);
@@ -289,6 +351,187 @@ describe("runLifecycleDiff --live", () => {
289
351
  expect(output).toContain("bucket");
290
352
  expect(output).toContain("added");
291
353
  });
354
+
355
+ // #1166 — an environment can declare its own endpoint (a local emulator like
356
+ // Floci), applied to the ambient var of every observing lexicon that has one
357
+ // unless the ambient shell already set it.
358
+ describe("declared endpoint (#1166)", () => {
359
+ const prevEndpoint = process.env.AWS_ENDPOINT_URL;
360
+
361
+ afterEach(() => {
362
+ if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
363
+ else process.env.AWS_ENDPOINT_URL = prevEndpoint;
364
+ });
365
+
366
+ test("applies the declared endpoint to AWS_ENDPOINT_URL for the live describe, then restores it", async () => {
367
+ delete process.env.AWS_ENDPOINT_URL;
368
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
369
+ fetchLifecycleMock.mockResolvedValue(undefined);
370
+ readSnapshotMock.mockResolvedValue(null);
371
+ loadChantConfigMock.mockResolvedValue({
372
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
373
+ });
374
+
375
+ let seenDuringDescribe: string | undefined;
376
+ const plugins: LexiconPlugin[] = [
377
+ createMockPlugin({
378
+ name: "aws",
379
+ describeResources: async () => {
380
+ seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
381
+ return {};
382
+ },
383
+ }),
384
+ ];
385
+
386
+ const exit = await runLifecycleDiff({
387
+ args: makeArgs({ path: "diff", extraPositional: "floci", live: true }),
388
+ plugins,
389
+ serializers: plugins.map((p) => p.serializer),
390
+ });
391
+
392
+ expect(exit).toBe(0);
393
+ expect(seenDuringDescribe).toBe("http://localhost:4566");
394
+ expect(process.env.AWS_ENDPOINT_URL).toBeUndefined(); // restored
395
+ expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
396
+ });
397
+
398
+ test("ambient AWS_ENDPOINT_URL still wins over the declared endpoint", async () => {
399
+ process.env.AWS_ENDPOINT_URL = "http://real-endpoint.example";
400
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
401
+ fetchLifecycleMock.mockResolvedValue(undefined);
402
+ readSnapshotMock.mockResolvedValue(null);
403
+ loadChantConfigMock.mockResolvedValue({
404
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
405
+ });
406
+
407
+ let seenDuringDescribe: string | undefined;
408
+ const plugins: LexiconPlugin[] = [
409
+ createMockPlugin({
410
+ name: "aws",
411
+ describeResources: async () => {
412
+ seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
413
+ return {};
414
+ },
415
+ }),
416
+ ];
417
+
418
+ const exit = await runLifecycleDiff({
419
+ args: makeArgs({ path: "diff", extraPositional: "floci", live: true }),
420
+ plugins,
421
+ serializers: plugins.map((p) => p.serializer),
422
+ });
423
+
424
+ expect(exit).toBe(0);
425
+ expect(seenDuringDescribe).toBe("http://real-endpoint.example");
426
+ expect(process.env.AWS_ENDPOINT_URL).toBe("http://real-endpoint.example");
427
+ expect(stderrBuf.join("\n")).toMatch(/ambient AWS_ENDPOINT_URL already set/);
428
+ });
429
+ });
430
+ });
431
+
432
+ describe("runLifecyclePlan", () => {
433
+ let stdoutBuf: string[];
434
+ let stderrBuf: string[];
435
+
436
+ beforeEach(() => {
437
+ stdoutBuf = [];
438
+ stderrBuf = [];
439
+ vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
440
+ vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
441
+ buildMock.mockReset();
442
+ fetchLifecycleMock.mockReset();
443
+ fetchLifecycleMock.mockResolvedValue(undefined);
444
+ readSnapshotMock.mockReset();
445
+ readSnapshotMock.mockResolvedValue(null);
446
+ loadChantConfigMock.mockReset();
447
+ loadChantConfigMock.mockResolvedValue({ config: {} });
448
+ });
449
+
450
+ test("happy path: proposes a create for a declared, unobserved-nowhere-else entity", async () => {
451
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
452
+ const plugins: LexiconPlugin[] = [
453
+ createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
454
+ ];
455
+ const exit = await runLifecyclePlan({
456
+ args: makeArgs({ path: "plan", extraPositional: "prod" }),
457
+ plugins,
458
+ serializers: plugins.map((p) => p.serializer),
459
+ });
460
+ expect(exit).toBe(0);
461
+ expect(stdoutBuf.join("\n")).toContain("bucket");
462
+ });
463
+
464
+ // #1166 — plan is always a live read (no `--live` flag of its own), so a
465
+ // declared environment endpoint applies here exactly as it does for
466
+ // `chant graph --live` / `chant lifecycle diff --live`.
467
+ describe("declared endpoint (#1166)", () => {
468
+ const prevEndpoint = process.env.AWS_ENDPOINT_URL;
469
+
470
+ afterEach(() => {
471
+ if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
472
+ else process.env.AWS_ENDPOINT_URL = prevEndpoint;
473
+ });
474
+
475
+ test("applies the declared endpoint to AWS_ENDPOINT_URL for the plan's describe, then restores it", async () => {
476
+ delete process.env.AWS_ENDPOINT_URL;
477
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
478
+ loadChantConfigMock.mockResolvedValue({
479
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
480
+ });
481
+
482
+ let seenDuringDescribe: string | undefined;
483
+ const plugins: LexiconPlugin[] = [
484
+ createMockPlugin({
485
+ name: "aws",
486
+ describeResources: async () => {
487
+ seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
488
+ return {};
489
+ },
490
+ }),
491
+ ];
492
+
493
+ const exit = await runLifecyclePlan({
494
+ args: makeArgs({ path: "plan", extraPositional: "floci" }),
495
+ plugins,
496
+ serializers: plugins.map((p) => p.serializer),
497
+ });
498
+
499
+ expect(exit).toBe(0);
500
+ expect(seenDuringDescribe).toBe("http://localhost:4566");
501
+ expect(process.env.AWS_ENDPOINT_URL).toBeUndefined();
502
+ expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
503
+ });
504
+
505
+ test("ambient AWS_ENDPOINT_URL still wins over the declared endpoint", async () => {
506
+ process.env.AWS_ENDPOINT_URL = "http://real-endpoint.example";
507
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
508
+ loadChantConfigMock.mockResolvedValue({
509
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
510
+ });
511
+
512
+ let seenDuringDescribe: string | undefined;
513
+ const plugins: LexiconPlugin[] = [
514
+ createMockPlugin({
515
+ name: "aws",
516
+ describeResources: async () => {
517
+ seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
518
+ return {};
519
+ },
520
+ }),
521
+ ];
522
+
523
+ const exit = await runLifecyclePlan({
524
+ args: makeArgs({ path: "plan", extraPositional: "floci" }),
525
+ plugins,
526
+ serializers: plugins.map((p) => p.serializer),
527
+ });
528
+
529
+ expect(exit).toBe(0);
530
+ expect(seenDuringDescribe).toBe("http://real-endpoint.example");
531
+ expect(process.env.AWS_ENDPOINT_URL).toBe("http://real-endpoint.example");
532
+ expect(stderrBuf.join("\n")).toMatch(/ambient AWS_ENDPOINT_URL already set/);
533
+ });
534
+ });
292
535
  });
293
536
 
294
537
  describe("runLifecycleSnapshot", () => {
@@ -365,6 +608,42 @@ describe("runLifecycleSnapshot", () => {
365
608
  expect(stderrBuf.join("\n")).toContain("Snapshot saved");
366
609
  expect(takeSnapshotMock).toHaveBeenCalledTimes(1);
367
610
  });
611
+
612
+ // #1166 — a snapshot is always a live read, so a declared environment
613
+ // endpoint applies here too, unless the ambient shell already set it.
614
+ describe("declared endpoint (#1166)", () => {
615
+ const prevEndpoint = process.env.AWS_ENDPOINT_URL;
616
+
617
+ afterEach(() => {
618
+ if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
619
+ else process.env.AWS_ENDPOINT_URL = prevEndpoint;
620
+ });
621
+
622
+ test("applies the declared endpoint to AWS_ENDPOINT_URL for takeSnapshot, then restores it", async () => {
623
+ delete process.env.AWS_ENDPOINT_URL;
624
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
625
+ loadChantConfigMock.mockResolvedValue({
626
+ config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
627
+ });
628
+ let seenDuringSnapshot: string | undefined;
629
+ takeSnapshotMock.mockImplementation(async () => {
630
+ seenDuringSnapshot = process.env.AWS_ENDPOINT_URL;
631
+ return { snapshots: [], commit: "sha", warnings: [], errors: [] };
632
+ });
633
+ const plugins: LexiconPlugin[] = [
634
+ createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
635
+ ];
636
+ const exit = await runLifecycleSnapshot({
637
+ args: makeArgs({ command: "state", path: "snapshot", extraPositional: "floci" }),
638
+ plugins,
639
+ serializers: plugins.map((p) => p.serializer),
640
+ });
641
+ expect(exit).toBe(0);
642
+ expect(seenDuringSnapshot).toBe("http://localhost:4566");
643
+ expect(process.env.AWS_ENDPOINT_URL).toBeUndefined();
644
+ expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
645
+ });
646
+ });
368
647
  });
369
648
 
370
649
  describe("runLifecycleShow", () => {