@intentius/chant 0.45.0 → 0.49.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.
- package/dist/audit/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +223 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -53,7 +53,7 @@ vi.mock("../../config", async () => {
|
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
|
|
56
|
+
const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleTeardown, runLifecycleUnknown } = await import("./lifecycle");
|
|
57
57
|
|
|
58
58
|
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
59
59
|
return {
|
|
@@ -821,6 +821,120 @@ describe("runLifecyclePlan", () => {
|
|
|
821
821
|
expect(byName["sg-0abc123"].name).toBe("sg-0abc123");
|
|
822
822
|
});
|
|
823
823
|
|
|
824
|
+
// #1832 — effect receipts are declared, diffed, and observed like any
|
|
825
|
+
// resource, but observe-only to the generic apply path: the plan compares
|
|
826
|
+
// live value to resolved expectation and proposes the fire, never a create.
|
|
827
|
+
describe("effect receipts (#1832)", () => {
|
|
828
|
+
const receiptEntity = (lexicon: string) => ({
|
|
829
|
+
lexicon,
|
|
830
|
+
entityType: lexicon === "chant" ? "Chant::EffectReceipt" : `${lexicon}::Receipt`,
|
|
831
|
+
name: "dbMigrated",
|
|
832
|
+
effect: "db-migrate",
|
|
833
|
+
flavor: "existence" as const,
|
|
834
|
+
inputs: {},
|
|
835
|
+
[Symbol.for("chant.declarable")]: true,
|
|
836
|
+
[Symbol.for("chant.effect-receipt")]: true,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
const buildWithReceipt = (lexicon: string): BuildResult => {
|
|
840
|
+
const base = makeBuildResult({ aws: ["bucket"] });
|
|
841
|
+
base.entities.set("dbMigrated", receiptEntity(lexicon) as unknown as Parameters<typeof base.entities.set>[1]);
|
|
842
|
+
if (lexicon !== "aws") (base.manifest.lexicons as string[]).push(lexicon);
|
|
843
|
+
return base;
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
test("an absent receipt is an effect row, never a create", async () => {
|
|
847
|
+
buildMock.mockResolvedValue(buildWithReceipt("aws"));
|
|
848
|
+
const plugins: LexiconPlugin[] = [
|
|
849
|
+
createMockPlugin({
|
|
850
|
+
name: "aws",
|
|
851
|
+
emulator: awsEmulatorStub,
|
|
852
|
+
describeResources: async () => ({
|
|
853
|
+
observation: "v1" as const,
|
|
854
|
+
resources: { bucket: meta() },
|
|
855
|
+
// dbMigrated neither returned nor unobserved: confirmed absent.
|
|
856
|
+
}),
|
|
857
|
+
}),
|
|
858
|
+
];
|
|
859
|
+
const exit = await runLifecyclePlan({
|
|
860
|
+
args: makeArgs({ path: "plan", extraPositional: "prod", json: true }),
|
|
861
|
+
plugins,
|
|
862
|
+
serializers: plugins.map((p) => p.serializer),
|
|
863
|
+
});
|
|
864
|
+
expect(exit).toBe(0);
|
|
865
|
+
const plan = JSON.parse(stdoutBuf.join("\n"));
|
|
866
|
+
const row = plan.entries.find((e: { name: string }) => e.name === "dbMigrated");
|
|
867
|
+
expect(row).toMatchObject({ action: "effect", effect: "db-migrate", effectReason: "receipt-absent", lexicon: "aws" });
|
|
868
|
+
expect(plan.entries.filter((e: { name: string; action: string }) => e.name === "dbMigrated")).toHaveLength(1);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("a live receipt carrying the existence marker is a clean noop, and the render says nothing fires", async () => {
|
|
872
|
+
buildMock.mockResolvedValue(buildWithReceipt("aws"));
|
|
873
|
+
const plugins: LexiconPlugin[] = [
|
|
874
|
+
createMockPlugin({
|
|
875
|
+
name: "aws",
|
|
876
|
+
emulator: awsEmulatorStub,
|
|
877
|
+
describeResources: async () => ({
|
|
878
|
+
observation: "v1" as const,
|
|
879
|
+
resources: {
|
|
880
|
+
bucket: meta(),
|
|
881
|
+
dbMigrated: meta({ type: "aws::Receipt", attributes: { value: "chant.effect-receipt:exists" } }),
|
|
882
|
+
},
|
|
883
|
+
}),
|
|
884
|
+
}),
|
|
885
|
+
];
|
|
886
|
+
const exit = await runLifecyclePlan({
|
|
887
|
+
args: makeArgs({ path: "plan", extraPositional: "prod" }),
|
|
888
|
+
plugins,
|
|
889
|
+
serializers: plugins.map((p) => p.serializer),
|
|
890
|
+
});
|
|
891
|
+
expect(exit).toBe(0);
|
|
892
|
+
expect(stdoutBuf.join("\n")).toContain("0 effect");
|
|
893
|
+
expect(stdoutBuf.join("\n")).not.toContain("effect will fire");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("a stale receipt renders effect will fire", async () => {
|
|
897
|
+
buildMock.mockResolvedValue(buildWithReceipt("aws"));
|
|
898
|
+
const plugins: LexiconPlugin[] = [
|
|
899
|
+
createMockPlugin({
|
|
900
|
+
name: "aws",
|
|
901
|
+
emulator: awsEmulatorStub,
|
|
902
|
+
describeResources: async () => ({
|
|
903
|
+
observation: "v1" as const,
|
|
904
|
+
resources: {
|
|
905
|
+
bucket: meta(),
|
|
906
|
+
dbMigrated: meta({ type: "aws::Receipt", attributes: { value: "something-else" } }),
|
|
907
|
+
},
|
|
908
|
+
}),
|
|
909
|
+
}),
|
|
910
|
+
];
|
|
911
|
+
const exit = await runLifecyclePlan({
|
|
912
|
+
args: makeArgs({ path: "plan", extraPositional: "prod" }),
|
|
913
|
+
plugins,
|
|
914
|
+
serializers: plugins.map((p) => p.serializer),
|
|
915
|
+
});
|
|
916
|
+
expect(exit).toBe(0);
|
|
917
|
+
expect(stdoutBuf.join("\n")).toContain("effect will fire: db-migrate");
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("a receipt no loaded lexicon observes is unobserved, loudly", async () => {
|
|
921
|
+
buildMock.mockResolvedValue(buildWithReceipt("chant"));
|
|
922
|
+
const plugins: LexiconPlugin[] = [
|
|
923
|
+
createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({ bucket: meta() }) }),
|
|
924
|
+
];
|
|
925
|
+
const exit = await runLifecyclePlan({
|
|
926
|
+
args: makeArgs({ path: "plan", extraPositional: "prod", json: true }),
|
|
927
|
+
plugins,
|
|
928
|
+
serializers: plugins.map((p) => p.serializer),
|
|
929
|
+
});
|
|
930
|
+
expect(exit).toBe(0);
|
|
931
|
+
const plan = JSON.parse(stdoutBuf.join("\n"));
|
|
932
|
+
const row = plan.entries.find((e: { name: string }) => e.name === "dbMigrated");
|
|
933
|
+
expect(row).toMatchObject({ action: "unobserved", unobservedReason: "unsupported-kind" });
|
|
934
|
+
expect(stderrBuf.join("\n")).toContain("could not be observed");
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
824
938
|
// #1166 — plan is always a live read (no `--live` flag of its own), so a
|
|
825
939
|
// declared environment endpoint applies here exactly as it does for
|
|
826
940
|
// `chant graph --live` / `chant lifecycle diff --live`.
|
|
@@ -1220,3 +1334,233 @@ describe("runLifecycleUnknown", () => {
|
|
|
1220
1334
|
expect(stderr).toContain("log");
|
|
1221
1335
|
});
|
|
1222
1336
|
});
|
|
1337
|
+
|
|
1338
|
+
describe("runLifecycleTeardown (#1222 — plan only)", () => {
|
|
1339
|
+
let stdoutBuf: string[];
|
|
1340
|
+
let stderrBuf: string[];
|
|
1341
|
+
|
|
1342
|
+
beforeEach(() => {
|
|
1343
|
+
stdoutBuf = [];
|
|
1344
|
+
stderrBuf = [];
|
|
1345
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
|
|
1346
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
|
|
1347
|
+
loadChantConfigMock.mockReset();
|
|
1348
|
+
loadChantConfigMock.mockResolvedValue({
|
|
1349
|
+
config: { environments: ["dev", "prod"], ownership: { stack: "shop", env: "dev" } },
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
afterEach(() => {
|
|
1354
|
+
vi.restoreAllMocks();
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
const run = (overrides: Partial<ParsedArgs>, plugins: LexiconPlugin[]) =>
|
|
1358
|
+
runLifecycleTeardown({
|
|
1359
|
+
args: makeArgs({ command: "lifecycle", path: "teardown", ...overrides }),
|
|
1360
|
+
plugins,
|
|
1361
|
+
serializers: [],
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
test("requires an environment", async () => {
|
|
1365
|
+
expect(await run({}, [])).toBe(1);
|
|
1366
|
+
expect(stderrBuf.join("\n")).toContain("Environment is required");
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
test("exits nonzero on an environment the project does not declare", async () => {
|
|
1370
|
+
expect(await run({ extraPositional: "staging" }, [])).toBe(1);
|
|
1371
|
+
const stderr = stderrBuf.join("\n");
|
|
1372
|
+
expect(stderr).toContain('Unknown environment "staging"');
|
|
1373
|
+
expect(stderr).toContain("dev, prod");
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
test("--yes executes the planned set and prints per-candidate outcomes", async () => {
|
|
1377
|
+
const executeTeardown = vi.fn(async () => ({
|
|
1378
|
+
outcomes: [{ name: "web", outcome: "deleted" as const }],
|
|
1379
|
+
}));
|
|
1380
|
+
const plugin = createMockPlugin({
|
|
1381
|
+
name: "k8s",
|
|
1382
|
+
teardownOwned: async () => ({
|
|
1383
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
|
|
1384
|
+
}),
|
|
1385
|
+
executeTeardown,
|
|
1386
|
+
});
|
|
1387
|
+
expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(0);
|
|
1388
|
+
expect(executeTeardown).toHaveBeenCalledWith({
|
|
1389
|
+
environment: "dev",
|
|
1390
|
+
marker: { stack: "shop", env: "dev" },
|
|
1391
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
|
|
1392
|
+
});
|
|
1393
|
+
const out = stdoutBuf.join("\n");
|
|
1394
|
+
expect(out).toContain("Outcomes:");
|
|
1395
|
+
expect(out).toContain("deleted");
|
|
1396
|
+
expect(out).toContain("1 deleted, 0 failed, 0 not prunable, 0 retained, 0 skipped");
|
|
1397
|
+
expect(out).not.toContain("plan only");
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("--yes exits nonzero when a candidate stays failed after the retry pass", async () => {
|
|
1401
|
+
const executeTeardown = vi.fn(async () => ({
|
|
1402
|
+
outcomes: [{ name: "web", outcome: "failed" as const, detail: "conflict" }],
|
|
1403
|
+
}));
|
|
1404
|
+
const plugin = createMockPlugin({
|
|
1405
|
+
name: "k8s",
|
|
1406
|
+
teardownOwned: async () => ({
|
|
1407
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
|
|
1408
|
+
}),
|
|
1409
|
+
executeTeardown,
|
|
1410
|
+
});
|
|
1411
|
+
expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(1);
|
|
1412
|
+
expect(executeTeardown).toHaveBeenCalledTimes(2); // pass + bounded retry
|
|
1413
|
+
expect(stderrBuf.join("\n")).toContain("failed to delete after the retry pass");
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("--yes with a lexicon that only enumerates reports skipped, not deleted", async () => {
|
|
1417
|
+
const plugin = createMockPlugin({
|
|
1418
|
+
name: "gcp",
|
|
1419
|
+
teardownOwned: async () => ({
|
|
1420
|
+
candidates: [{ name: "bucket", type: "GCP::Storage::Bucket", marker: { stack: "shop", env: "dev" } }],
|
|
1421
|
+
}),
|
|
1422
|
+
});
|
|
1423
|
+
expect(await run({ extraPositional: "dev", yes: true }, [plugin])).toBe(0);
|
|
1424
|
+
expect(stdoutBuf.join("\n")).toContain("skipped");
|
|
1425
|
+
expect(stderrBuf.join("\n")).toContain("no teardown execution in these lexicons yet");
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
test("--yes on a production-like environment demands --confirm-prod when not interactive", async () => {
|
|
1429
|
+
const executeTeardown = vi.fn();
|
|
1430
|
+
const plugin = createMockPlugin({
|
|
1431
|
+
name: "k8s",
|
|
1432
|
+
teardownOwned: async () => ({ candidates: [] }),
|
|
1433
|
+
executeTeardown,
|
|
1434
|
+
});
|
|
1435
|
+
expect(await run({ extraPositional: "prod", yes: true }, [plugin])).toBe(1);
|
|
1436
|
+
expect(stderrBuf.join("\n")).toContain("--confirm-prod");
|
|
1437
|
+
expect(executeTeardown).not.toHaveBeenCalled();
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test("--yes --confirm-prod tears a production-like environment down non-interactively", async () => {
|
|
1441
|
+
const executeTeardown = vi.fn(async () => ({
|
|
1442
|
+
outcomes: [{ name: "web", outcome: "deleted" as const }],
|
|
1443
|
+
}));
|
|
1444
|
+
const plugin = createMockPlugin({
|
|
1445
|
+
name: "k8s",
|
|
1446
|
+
teardownOwned: async () => ({
|
|
1447
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "prod" } }],
|
|
1448
|
+
}),
|
|
1449
|
+
executeTeardown,
|
|
1450
|
+
});
|
|
1451
|
+
expect(await run({ extraPositional: "prod", yes: true, confirmProd: true }, [plugin])).toBe(0);
|
|
1452
|
+
expect(executeTeardown).toHaveBeenCalledTimes(1);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
test("the prod guard never fires without --yes — planning production is safe", async () => {
|
|
1456
|
+
const plugin = createMockPlugin({
|
|
1457
|
+
name: "k8s",
|
|
1458
|
+
teardownOwned: async () => ({ candidates: [] }),
|
|
1459
|
+
});
|
|
1460
|
+
expect(await run({ extraPositional: "prod" }, [plugin])).toBe(0);
|
|
1461
|
+
expect(stderrBuf.join("\n")).not.toContain("--confirm-prod");
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
test("--yes --json emits the execution report", async () => {
|
|
1465
|
+
const plugin = createMockPlugin({
|
|
1466
|
+
name: "k8s",
|
|
1467
|
+
teardownOwned: async () => ({
|
|
1468
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
|
|
1469
|
+
}),
|
|
1470
|
+
executeTeardown: async () => ({ outcomes: [{ name: "web", outcome: "deleted" as const }] }),
|
|
1471
|
+
});
|
|
1472
|
+
expect(await run({ extraPositional: "dev", yes: true, json: true }, [plugin])).toBe(0);
|
|
1473
|
+
const parsed = JSON.parse(stdoutBuf.join("\n"));
|
|
1474
|
+
expect(parsed.outcomes).toEqual([
|
|
1475
|
+
{
|
|
1476
|
+
lexicon: "k8s",
|
|
1477
|
+
name: "web",
|
|
1478
|
+
type: "K8s::Apps::Deployment",
|
|
1479
|
+
marker: { stack: "shop", env: "dev" },
|
|
1480
|
+
outcome: "deleted",
|
|
1481
|
+
},
|
|
1482
|
+
]);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
test("refuses when the project declares no ownership.stack", async () => {
|
|
1486
|
+
loadChantConfigMock.mockResolvedValue({ config: { environments: ["dev"] } });
|
|
1487
|
+
expect(await run({ extraPositional: "dev" }, [])).toBe(1);
|
|
1488
|
+
expect(stderrBuf.join("\n")).toContain("no ownership.stack");
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
test("prints the would-delete set with resource, type, and marker identity", async () => {
|
|
1492
|
+
const plugin = createMockPlugin({
|
|
1493
|
+
name: "k8s",
|
|
1494
|
+
teardownOwned: async () => ({
|
|
1495
|
+
candidates: [
|
|
1496
|
+
{ name: "web", type: "K8s::Apps::Deployment", physicalId: "web-1", marker: { stack: "shop", env: "dev" } },
|
|
1497
|
+
],
|
|
1498
|
+
}),
|
|
1499
|
+
});
|
|
1500
|
+
expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
|
|
1501
|
+
const out = stdoutBuf.join("\n");
|
|
1502
|
+
expect(out).toContain("Teardown plan");
|
|
1503
|
+
expect(out).toContain("plan only");
|
|
1504
|
+
expect(out).toContain("web");
|
|
1505
|
+
expect(out).toContain("K8s::Apps::Deployment");
|
|
1506
|
+
expect(out).toContain("shop/dev");
|
|
1507
|
+
expect(stderrBuf.join("\n")).toContain("re-run with --yes to execute");
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
test("never selects foreign-stack or foreign-env resources on the fallback path", async () => {
|
|
1511
|
+
const plugin = createMockPlugin({
|
|
1512
|
+
name: "k8s",
|
|
1513
|
+
describeResources: staticObservation({
|
|
1514
|
+
mine: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "shop", env: "dev" } },
|
|
1515
|
+
theirs: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "blog", env: "dev" } },
|
|
1516
|
+
prodTwin: { type: "K8s::Apps::Deployment", status: "READY", marker: { stack: "shop", env: "prod" } },
|
|
1517
|
+
unmarked: { type: "K8s::Apps::Deployment", status: "READY" },
|
|
1518
|
+
}),
|
|
1519
|
+
});
|
|
1520
|
+
expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
|
|
1521
|
+
const out = stdoutBuf.join("\n");
|
|
1522
|
+
expect(out).toContain("mine");
|
|
1523
|
+
expect(out).not.toContain("theirs");
|
|
1524
|
+
expect(out).not.toContain("prodTwin");
|
|
1525
|
+
expect(out).not.toContain("unmarked");
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
test("an empty would-delete set is loud, not silent", async () => {
|
|
1529
|
+
const plugin = createMockPlugin({
|
|
1530
|
+
name: "k8s",
|
|
1531
|
+
teardownOwned: async () => ({ candidates: [] }),
|
|
1532
|
+
});
|
|
1533
|
+
expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
|
|
1534
|
+
expect(stderrBuf.join("\n")).toContain("nothing would be deleted");
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
test("holes are surfaced loudly (#1089)", async () => {
|
|
1538
|
+
const plugin = createMockPlugin({
|
|
1539
|
+
name: "k8s",
|
|
1540
|
+
teardownOwned: async () => ({
|
|
1541
|
+
candidates: [],
|
|
1542
|
+
holes: [{ name: "crds", type: "K8s::CRD", reason: "unsupported-kind" as const, detail: "no reader" }],
|
|
1543
|
+
}),
|
|
1544
|
+
});
|
|
1545
|
+
expect(await run({ extraPositional: "dev" }, [plugin])).toBe(0);
|
|
1546
|
+
expect(stderrBuf.join("\n")).toContain("incomplete, not clean");
|
|
1547
|
+
const out = stdoutBuf.join("\n");
|
|
1548
|
+
expect(out).toContain("HOLES");
|
|
1549
|
+
expect(out).toContain("crds");
|
|
1550
|
+
expect(out).toContain("unsupported-kind");
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
test("--json emits the plan as JSON", async () => {
|
|
1554
|
+
const plugin = createMockPlugin({
|
|
1555
|
+
name: "k8s",
|
|
1556
|
+
teardownOwned: async () => ({
|
|
1557
|
+
candidates: [{ name: "web", type: "K8s::Apps::Deployment", marker: { stack: "shop", env: "dev" } }],
|
|
1558
|
+
}),
|
|
1559
|
+
});
|
|
1560
|
+
expect(await run({ extraPositional: "dev", json: true }, [plugin])).toBe(0);
|
|
1561
|
+
const parsed = JSON.parse(stdoutBuf.join("\n"));
|
|
1562
|
+
expect(parsed.environment).toBe("dev");
|
|
1563
|
+
expect(parsed.stack).toBe("shop");
|
|
1564
|
+
expect(parsed.entries).toHaveLength(1);
|
|
1565
|
+
});
|
|
1566
|
+
});
|