@intentius/chant 0.45.0 → 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.
- 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/registry.d.ts +7 -0
- package/dist/cli/registry.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/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.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/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/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/package.json +6 -1
- package/src/cli/commands/build.test.ts +131 -0
- package/src/cli/commands/build.ts +20 -0
- 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 +19 -6
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +231 -1
- package/src/cli/handlers/lifecycle.ts +219 -3
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/registry.ts +7 -0
- 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/config.test.ts +40 -0
- package/src/config.ts +58 -1
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lint/engine.ts +7 -1
- 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/index.ts +4 -1
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
package/src/lint/rules/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
25
25
|
export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
26
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
27
27
|
export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
28
|
+
export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
28
29
|
export { isInsideCompositeFactory } from "./composite-scope";
|
|
29
30
|
|
|
30
31
|
import { flatDeclarationsRule } from "./flat-declarations";
|
|
@@ -48,9 +49,10 @@ import { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
48
49
|
import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
49
50
|
import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
50
51
|
import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
52
|
+
import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
* Load all
|
|
55
|
+
* Load all 22 core lint rules (COR + EVL).
|
|
54
56
|
*/
|
|
55
57
|
export function loadCoreRules(): LintRule[] {
|
|
56
58
|
return [
|
|
@@ -75,5 +77,6 @@ export function loadCoreRules(): LintRule[] {
|
|
|
75
77
|
evl010CompositeNoTransformRule,
|
|
76
78
|
cor017CompositeNameMatchRule,
|
|
77
79
|
cor018CompositePreferLexiconTypeRule,
|
|
80
|
+
cor021EnvLiteralNameRule,
|
|
78
81
|
];
|
|
79
82
|
}
|
package/src/op/builders.ts
CHANGED
|
@@ -137,6 +137,37 @@ export const shell = (
|
|
|
137
137
|
export const teardown = (path: string): ActivityStep =>
|
|
138
138
|
activity("chantTeardown", { path }, "longInfra");
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Tear down one environment's marker-owned resources — the durable form of
|
|
142
|
+
* `chant lifecycle teardown <env> --yes` (#1222). The activity runs core's
|
|
143
|
+
* teardown engine in-process: enumerate by ownership marker (this project's
|
|
144
|
+
* `ownership.stack` + `env`), delete through each lexicon's `executeTeardown`
|
|
145
|
+
* capability, retry failures once, and fail the step when any candidate is
|
|
146
|
+
* still failed. Distinct from {@link teardown}, which runs a project's own
|
|
147
|
+
* `npm run teardown` script.
|
|
148
|
+
*
|
|
149
|
+
* The CLI's guards apply unchanged, and a production-like environment name
|
|
150
|
+
* needs `confirmProd: true` in `opts` — the authored counterpart of
|
|
151
|
+
* `--confirm-prod`, since an Op never prompts. An ordinary {@link gate} step
|
|
152
|
+
* placed before this one composes as usual (steps run in authored order), so
|
|
153
|
+
* a human approval can precede the deletion:
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* phase("Teardown", [
|
|
157
|
+
* gate("approve-teardown", { description: "Release the staging teardown" }),
|
|
158
|
+
* envTeardown("staging"),
|
|
159
|
+
* ]),
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* `opts` also accepts `path` (the chant project directory, default the
|
|
163
|
+
* worker's cwd). Defaults to the `longInfra` profile (override via
|
|
164
|
+
* `opts.profile`).
|
|
165
|
+
*/
|
|
166
|
+
export const envTeardown = (env: string, opts?: Record<string, unknown>): ActivityStep => {
|
|
167
|
+
const { args, profile } = takeProfile(opts);
|
|
168
|
+
return activity("envTeardown", { env, ...args }, profile ?? "longInfra");
|
|
169
|
+
};
|
|
170
|
+
|
|
140
171
|
/**
|
|
141
172
|
* Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: skips
|
|
142
173
|
* creation if a cluster of the same name already exists. Defaults to the
|
|
@@ -279,8 +310,8 @@ export const azDelete = (templatePath: string, opts?: Record<string, unknown>):
|
|
|
279
310
|
* Deploy a built CloudFormation template by calling the CloudFormation API
|
|
280
311
|
* directly (create-or-update + poll) — the direct twin of {@link azApply} /
|
|
281
312
|
* {@link gcpApply} for AWS, targeting a local Floci emulator or real AWS by
|
|
282
|
-
* endpoint override. Speaks the CFN API over HTTP rather than shelling `aws`
|
|
283
|
-
*
|
|
313
|
+
* endpoint override. Speaks the CFN API over HTTP rather than shelling `aws` —
|
|
314
|
+
* `nativeApply({ target: "cloudformation" })` routes here too (#1449). Provided by
|
|
284
315
|
* the aws lexicon; loaded when the project lists `aws`. Defaults to the
|
|
285
316
|
* `longInfra` profile. `opts` requires `stackName`; accepts `endpoint`, `region`,
|
|
286
317
|
* `capabilities`, `timeoutMs`, `intervalMs`.
|
|
@@ -297,13 +328,15 @@ export const awsDelete = (templatePath: string, opts?: Record<string, unknown>):
|
|
|
297
328
|
};
|
|
298
329
|
|
|
299
330
|
/**
|
|
300
|
-
* Apply chant's built GCP (CNRM) resources directly to
|
|
331
|
+
* Apply chant's built GCP (CNRM) resources directly to their GCP REST APIs,
|
|
301
332
|
* targeting a local floci-gcp emulator or real GCP by endpoint override — the
|
|
302
|
-
* native GCP applier (#706 starter #711)
|
|
303
|
-
* Defaults to the `longInfra` profile (override via
|
|
333
|
+
* native GCP applier (#706 starter #711). `nativeApply({ target: "gcp" })`
|
|
334
|
+
* routes here too (#1449). Defaults to the `longInfra` profile (override via
|
|
335
|
+
* `opts.profile`).
|
|
304
336
|
*
|
|
305
|
-
* `opts` accepts `endpoint` (default `
|
|
306
|
-
* `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM
|
|
337
|
+
* `opts` accepts `endpoint` (default `GCP_ENDPOINT_URL` env / each kind's
|
|
338
|
+
* real-GCP host) and `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM
|
|
339
|
+
* project-id annotation).
|
|
307
340
|
*/
|
|
308
341
|
export const gcpApply = (manifestPath: string, opts?: Record<string, unknown>): ActivityStep => {
|
|
309
342
|
const { args, profile } = takeProfile(opts);
|
package/src/op/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForStack, waitForReady,
|
|
2
|
-
gitlabPipeline, lifecycleSnapshot, shell, teardown, k3dUp, k3dDown, flociUp, flociDown,
|
|
2
|
+
gitlabPipeline, lifecycleSnapshot, shell, teardown, envTeardown, k3dUp, k3dDown, flociUp, flociDown,
|
|
3
3
|
flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
|
|
4
4
|
azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
|
|
5
5
|
spriteCreate, spriteExec, spriteCheckpoint, spriteRestore, listCheckpoints, spriteDestroy,
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createMockPlugin } from "@intentius/chant-test-utils";
|
|
7
|
+
import { deployStack, testEnvName, TeardownIncompleteError } from "./testing";
|
|
8
|
+
import type { ActivityFn } from "./op/activity-registry";
|
|
9
|
+
import type { TeardownExecution } from "./lexicon";
|
|
10
|
+
|
|
11
|
+
/** A minimal deployable project: one declarable, mock lexicon, marker on. */
|
|
12
|
+
async function writeProject(
|
|
13
|
+
dir: string,
|
|
14
|
+
config: Record<string, unknown> = {},
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
await mkdir(dir, { recursive: true });
|
|
17
|
+
await writeFile(
|
|
18
|
+
join(dir, "chant.config.json"),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
lexicons: ["mock"],
|
|
21
|
+
ownership: { stack: "harness-fixture" },
|
|
22
|
+
environments: ["dev", { name: "test-*", endpoint: "http://localhost:9999" }],
|
|
23
|
+
...config,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
await writeFile(
|
|
27
|
+
join(dir, "app.infra.ts"),
|
|
28
|
+
`export const bucket = {
|
|
29
|
+
lexicon: "mock",
|
|
30
|
+
entityType: "Mock::Bucket",
|
|
31
|
+
[Symbol.for("chant.declarable")]: true,
|
|
32
|
+
};
|
|
33
|
+
`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("testEnvName", () => {
|
|
38
|
+
test("prefixes test-, slugs the suite, appends a nonce", () => {
|
|
39
|
+
const name = testEnvName("My Fancy Suite!");
|
|
40
|
+
expect(name).toMatch(/^test-my-fancy-suite-[a-z0-9]{6}$/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("two calls never collide", () => {
|
|
44
|
+
expect(testEnvName("s")).not.toBe(testEnvName("s"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("an all-symbol suite still produces a legal name", () => {
|
|
48
|
+
expect(testEnvName("!!!")).toMatch(/^test-suite-[a-z0-9]{6}$/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("deployStack", () => {
|
|
53
|
+
let dir: string;
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
dir = join(tmpdir(), `chant-testing-fixture-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
await rm(dir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const harness = (nativeApply: ActivityFn, overrides: Record<string, unknown> = {}) => ({
|
|
64
|
+
dir,
|
|
65
|
+
suite: "unit",
|
|
66
|
+
plugins: [createMockPlugin({ name: "mock" })],
|
|
67
|
+
activities: new Map([["nativeApply", nativeApply]]),
|
|
68
|
+
profiles: {},
|
|
69
|
+
applyTargets: { mock: "cloudformation" },
|
|
70
|
+
...overrides,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("deploy returns outputs, entities, and a nonce'd test env", async () => {
|
|
74
|
+
await writeProject(dir);
|
|
75
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
76
|
+
|
|
77
|
+
const stack = await deployStack(harness(nativeApply));
|
|
78
|
+
|
|
79
|
+
expect(stack.env).toMatch(/^test-unit-[a-z0-9]{6}$/);
|
|
80
|
+
expect(stack.entities.has("bucket")).toBe(true);
|
|
81
|
+
const output = stack.outputs.get("mock");
|
|
82
|
+
expect(output).toBeDefined();
|
|
83
|
+
const primary = typeof output === "string" ? output : output!.primary;
|
|
84
|
+
expect(JSON.parse(primary).resources.bucket).toEqual({ type: "Mock::Bucket" });
|
|
85
|
+
|
|
86
|
+
// The apply was additive, targeted at this env, over the written output.
|
|
87
|
+
expect(nativeApply).toHaveBeenCalledTimes(1);
|
|
88
|
+
const args = nativeApply.mock.calls[0][0] as Record<string, unknown>;
|
|
89
|
+
expect(args.target).toBe("cloudformation");
|
|
90
|
+
expect(args.env).toBe(stack.env);
|
|
91
|
+
expect(args.deleteMode).toBe("never");
|
|
92
|
+
expect(JSON.parse(readFileSync(args.output as string, "utf-8")).resources.bucket).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the declared test-* endpoint is applied during the deploy", async () => {
|
|
96
|
+
await writeProject(dir);
|
|
97
|
+
let seenDuringApply: string | undefined;
|
|
98
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => {
|
|
99
|
+
seenDuringApply = process.env.MOCK_ENDPOINT_URL;
|
|
100
|
+
return { applied: 1, pruned: 0, notAttempted: 0 };
|
|
101
|
+
});
|
|
102
|
+
const plugin = createMockPlugin({
|
|
103
|
+
name: "mock",
|
|
104
|
+
emulator: {
|
|
105
|
+
spec: { name: "chant-mock", image: "mock:1", containerPort: 1, healthPath: "/health" },
|
|
106
|
+
env: (endpoint: string) => ({ MOCK_ENDPOINT_URL: endpoint }),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
delete process.env.MOCK_ENDPOINT_URL;
|
|
111
|
+
await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
112
|
+
|
|
113
|
+
expect(seenDuringApply).toBe("http://localhost:9999");
|
|
114
|
+
// Scoped to the run — nothing leaks into the suite's own process env.
|
|
115
|
+
expect(process.env.MOCK_ENDPOINT_URL).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("an ambient endpoint var wins over the declared endpoint", async () => {
|
|
119
|
+
await writeProject(dir);
|
|
120
|
+
let seenDuringApply: string | undefined;
|
|
121
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => {
|
|
122
|
+
seenDuringApply = process.env.MOCK_ENDPOINT_URL;
|
|
123
|
+
return { applied: 1, pruned: 0, notAttempted: 0 };
|
|
124
|
+
});
|
|
125
|
+
const plugin = createMockPlugin({
|
|
126
|
+
name: "mock",
|
|
127
|
+
emulator: {
|
|
128
|
+
spec: { name: "chant-mock", image: "mock:1", containerPort: 1, healthPath: "/health" },
|
|
129
|
+
env: (endpoint: string) => ({ MOCK_ENDPOINT_URL: endpoint }),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
process.env.MOCK_ENDPOINT_URL = "http://ambient:1234";
|
|
134
|
+
try {
|
|
135
|
+
await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
136
|
+
} finally {
|
|
137
|
+
delete process.env.MOCK_ENDPOINT_URL;
|
|
138
|
+
}
|
|
139
|
+
expect(seenDuringApply).toBe("http://ambient:1234");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("destroy sweeps exactly the nonce'd env, marker-scoped", async () => {
|
|
143
|
+
await writeProject(dir);
|
|
144
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
145
|
+
const teardownOwned = vi.fn(async (args: { environment: string; marker: { stack: string; env?: string } }) => ({
|
|
146
|
+
candidates: [
|
|
147
|
+
{
|
|
148
|
+
name: "bucket",
|
|
149
|
+
type: "Mock::Bucket",
|
|
150
|
+
marker: { stack: args.marker.stack, env: args.marker.env! },
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
}));
|
|
154
|
+
const executeTeardown = vi.fn(
|
|
155
|
+
async (): Promise<TeardownExecution> => ({
|
|
156
|
+
outcomes: [{ name: "bucket", outcome: "deleted" }],
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
const plugin = createMockPlugin({ name: "mock", teardownOwned, executeTeardown });
|
|
160
|
+
|
|
161
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
162
|
+
const report = await stack.destroy();
|
|
163
|
+
|
|
164
|
+
expect(teardownOwned).toHaveBeenCalledWith({
|
|
165
|
+
environment: stack.env,
|
|
166
|
+
marker: { stack: "harness-fixture", env: stack.env },
|
|
167
|
+
});
|
|
168
|
+
expect(report.environment).toBe(stack.env);
|
|
169
|
+
expect(report.stack).toBe("harness-fixture");
|
|
170
|
+
expect(report.outcomes).toEqual([
|
|
171
|
+
expect.objectContaining({ name: "bucket", outcome: "deleted" }),
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("teardown holes surface as a thrown TeardownIncompleteError", async () => {
|
|
176
|
+
await writeProject(dir);
|
|
177
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
178
|
+
const teardownOwned = vi.fn(async () => ({
|
|
179
|
+
candidates: [],
|
|
180
|
+
holes: [{ name: "bucket", reason: "read-failed" as const, detail: "connection refused" }],
|
|
181
|
+
}));
|
|
182
|
+
const plugin = createMockPlugin({ name: "mock", teardownOwned, executeTeardown: async () => ({ outcomes: [] }) });
|
|
183
|
+
|
|
184
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
185
|
+
const err = await stack.destroy().then(
|
|
186
|
+
() => undefined,
|
|
187
|
+
(e: unknown) => e,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(err).toBeInstanceOf(TeardownIncompleteError);
|
|
191
|
+
expect((err as TeardownIncompleteError).message).toContain("hole");
|
|
192
|
+
expect((err as TeardownIncompleteError).report.plan.holes).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a failed delete surfaces as a thrown TeardownIncompleteError", async () => {
|
|
196
|
+
await writeProject(dir);
|
|
197
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
198
|
+
const teardownOwned = vi.fn(async (args: { marker: { stack: string; env?: string } }) => ({
|
|
199
|
+
candidates: [{ name: "bucket", type: "Mock::Bucket", marker: { stack: args.marker.stack, env: args.marker.env! } }],
|
|
200
|
+
}));
|
|
201
|
+
const executeTeardown = vi.fn(
|
|
202
|
+
async (): Promise<TeardownExecution> => ({
|
|
203
|
+
outcomes: [{ name: "bucket", outcome: "failed", detail: "still in use" }],
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
const plugin = createMockPlugin({ name: "mock", teardownOwned, executeTeardown });
|
|
207
|
+
|
|
208
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
209
|
+
const err = await stack.destroy().then(
|
|
210
|
+
() => undefined,
|
|
211
|
+
(e: unknown) => e,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(err).toBeInstanceOf(TeardownIncompleteError);
|
|
215
|
+
expect((err as TeardownIncompleteError).message).toContain("still in use");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a failed apply rejects with the local executor's failure", async () => {
|
|
219
|
+
await writeProject(dir);
|
|
220
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => {
|
|
221
|
+
throw new Error("emulator not reachable");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expect(deployStack(harness(nativeApply, { profiles: { longInfra: { retry: { maximumAttempts: 1 } } } })))
|
|
225
|
+
.rejects.toThrow(/deploy-test-unit/);
|
|
226
|
+
expect(nativeApply).toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("an environment the project does not declare is refused with the test-* hint", async () => {
|
|
230
|
+
await writeProject(dir, { environments: ["dev", "prod"] });
|
|
231
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
232
|
+
|
|
233
|
+
await expect(deployStack(harness(nativeApply))).rejects.toThrow(/"test-\*" pattern/);
|
|
234
|
+
expect(nativeApply).not.toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("an explicit env overrides the derived name", async () => {
|
|
238
|
+
await writeProject(dir);
|
|
239
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
240
|
+
|
|
241
|
+
const stack = await deployStack(harness(nativeApply, { env: "test-pinned" }));
|
|
242
|
+
|
|
243
|
+
expect(stack.env).toBe("test-pinned");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a project without ownership.stack is refused before anything deploys", async () => {
|
|
247
|
+
await writeProject(dir, { ownership: undefined });
|
|
248
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
249
|
+
|
|
250
|
+
await expect(deployStack(harness(nativeApply))).rejects.toThrow(/ownership\.stack/);
|
|
251
|
+
expect(nativeApply).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("a project whose outputs have no apply target is refused loudly", async () => {
|
|
255
|
+
await writeProject(dir);
|
|
256
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
257
|
+
|
|
258
|
+
await expect(deployStack(harness(nativeApply, { applyTargets: {} }))).rejects.toThrow(/nothing to deploy/);
|
|
259
|
+
expect(nativeApply).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
});
|