@intentius/chant 0.50.0 → 0.51.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/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/op-progress.d.ts +57 -0
- package/dist/cli/handlers/op-progress.d.ts.map +1 -0
- package/dist/cli/handlers/run-client.d.ts +21 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run-report.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +22 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +33 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +7 -5
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/lexicon.d.ts +41 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts +77 -0
- package/dist/lifecycle/assert-live.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +17 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/disruption.d.ts +96 -0
- package/dist/lifecycle/disruption.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts +2 -0
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +7 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/testing.d.ts +23 -2
- package/dist/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/lifecycle.test.ts +90 -0
- package/src/cli/handlers/lifecycle.ts +18 -1
- package/src/cli/handlers/op-progress.test.ts +202 -0
- package/src/cli/handlers/op-progress.ts +192 -0
- package/src/cli/handlers/run-client.test.ts +82 -0
- package/src/cli/handlers/run-client.ts +85 -2
- package/src/cli/handlers/run-report.test.ts +62 -0
- package/src/cli/handlers/run-report.ts +20 -58
- package/src/cli/handlers/run.test.ts +144 -0
- package/src/cli/handlers/run.ts +40 -18
- package/src/cli/handlers/search-drift.test.ts +263 -0
- package/src/cli/handlers/search.ts +150 -1
- package/src/cli/main.ts +9 -0
- package/src/cli/mcp/op-tools.ts +17 -6
- package/src/cli/mcp/resource-handlers.ts +13 -5
- package/src/cli/registry.ts +33 -1
- package/src/components/run-progress.ts +9 -5
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/assert-live.test.ts +125 -0
- package/src/lifecycle/assert-live.ts +154 -0
- package/src/lifecycle/change-set.ts +35 -3
- package/src/lifecycle/disruption.test.ts +186 -0
- package/src/lifecycle/disruption.ts +224 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/replay.test.ts +25 -0
- package/src/lifecycle/replay.ts +11 -3
- package/src/op/local-executor.ts +7 -1
- package/src/op/local-output.ts +1 -1
- package/src/testing.test.ts +89 -2
- package/src/testing.ts +63 -3
package/src/lifecycle/replay.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { readEnvironmentSnapshots } from "./git";
|
|
18
18
|
import { unqualifiedKey } from "./identity";
|
|
19
19
|
import type { LiveObservation } from "../graph-ir";
|
|
20
|
-
import type { LifecycleSnapshot } from "./types";
|
|
20
|
+
import type { LifecycleSnapshot, ObservationDepth } from "./types";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Rebuild observations from a recorded snapshot (#1266).
|
|
@@ -52,7 +52,10 @@ export async function replaySnapshots(
|
|
|
52
52
|
environment: string,
|
|
53
53
|
ref: string,
|
|
54
54
|
scopedStacks: Set<string>,
|
|
55
|
-
): Promise<
|
|
55
|
+
): Promise<
|
|
56
|
+
| { observations: LiveObservation[]; commit: string; timestamp: string; depth: ObservationDepth }
|
|
57
|
+
| { error: string; hint?: string }
|
|
58
|
+
> {
|
|
56
59
|
if (ref !== "latest" && ref !== "true") {
|
|
57
60
|
return {
|
|
58
61
|
error: `chant search --at only accepts "latest" for now, got "${ref}"`,
|
|
@@ -69,6 +72,10 @@ export async function replaySnapshots(
|
|
|
69
72
|
const observations: LiveObservation[] = [];
|
|
70
73
|
let commit = "";
|
|
71
74
|
let timestamp = "";
|
|
75
|
+
// "deep" when ANY recorded lexicon read that deep — a consumer comparing
|
|
76
|
+
// against this replay (#1268) needs to know a richer record exists, even
|
|
77
|
+
// though a replay itself only ever turns `resources` back into identity.
|
|
78
|
+
let depth: ObservationDepth = "identity";
|
|
72
79
|
// Ambient and dependency resources are keyed by physical id and are
|
|
73
80
|
// account-level: the default security group three stacks each recorded is one
|
|
74
81
|
// group, not three. Managed resources are stack-qualified below and cannot
|
|
@@ -187,6 +194,7 @@ export async function replaySnapshots(
|
|
|
187
194
|
if (!timestamp || (snapshot.timestamp && snapshot.timestamp < timestamp)) {
|
|
188
195
|
timestamp = snapshot.timestamp ?? timestamp;
|
|
189
196
|
}
|
|
197
|
+
if (snapshot.depth === "deep") depth = "deep";
|
|
190
198
|
}
|
|
191
|
-
return { observations, commit, timestamp };
|
|
199
|
+
return { observations, commit, timestamp, depth };
|
|
192
200
|
}
|
package/src/op/local-executor.ts
CHANGED
|
@@ -21,7 +21,13 @@ import { isStepOutputRef } from "./step-output-ref";
|
|
|
21
21
|
export interface StepRecord {
|
|
22
22
|
phase: string;
|
|
23
23
|
fn: string;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Present for a local-executor record. Absent for one reconstructed from
|
|
26
|
+
* Temporal workflow history (op-progress.ts) — an activity's scheduled
|
|
27
|
+
* input isn't decoded there, so the field is simply omitted rather than
|
|
28
|
+
* populated with a guess.
|
|
29
|
+
*/
|
|
30
|
+
args?: Record<string, unknown>;
|
|
25
31
|
status: "ok" | "fail" | "skipped";
|
|
26
32
|
durationMs: number;
|
|
27
33
|
outcome?: { name: string; value: unknown };
|
package/src/op/local-output.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
|
|
|
33
33
|
write(`[phase] ${currentPhase}`);
|
|
34
34
|
}
|
|
35
35
|
const mark = record.status === "ok" ? "✓" : record.status === "fail" ? "✗" : "•";
|
|
36
|
-
const call = `${record.fn}(${formatArgs(record.args)})`;
|
|
36
|
+
const call = `${record.fn}(${formatArgs(record.args ?? {})})`;
|
|
37
37
|
if (record.status === "skipped") {
|
|
38
38
|
write(` ${mark} ${call} skipped`);
|
|
39
39
|
} else {
|
package/src/testing.test.ts
CHANGED
|
@@ -3,8 +3,14 @@ import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { createMockPlugin } from "@intentius/chant-test-utils";
|
|
7
|
-
import {
|
|
6
|
+
import { createMockPlugin, staticObservation } from "@intentius/chant-test-utils";
|
|
7
|
+
import {
|
|
8
|
+
deployStack,
|
|
9
|
+
testEnvName,
|
|
10
|
+
TeardownIncompleteError,
|
|
11
|
+
LiveAssertionError,
|
|
12
|
+
UnobservedAssertionError,
|
|
13
|
+
} from "./testing";
|
|
8
14
|
import type { ActivityFn } from "./op/activity-registry";
|
|
9
15
|
import type { TeardownExecution } from "./lexicon";
|
|
10
16
|
|
|
@@ -258,4 +264,85 @@ describe("deployStack", () => {
|
|
|
258
264
|
await expect(deployStack(harness(nativeApply, { applyTargets: {} }))).rejects.toThrow(/nothing to deploy/);
|
|
259
265
|
expect(nativeApply).not.toHaveBeenCalled();
|
|
260
266
|
});
|
|
267
|
+
|
|
268
|
+
describe("assertLive", () => {
|
|
269
|
+
const nativeApply = vi.fn(async (_args: Record<string, unknown>) => ({ applied: 1, pruned: 0, notAttempted: 0 }));
|
|
270
|
+
|
|
271
|
+
test("resolves the metadata for an entity observed present and marker-matched", async () => {
|
|
272
|
+
await writeProject(dir);
|
|
273
|
+
const plugin = createMockPlugin({
|
|
274
|
+
name: "mock",
|
|
275
|
+
describeResources: (opts) =>
|
|
276
|
+
Promise.resolve({
|
|
277
|
+
bucket: { type: "Mock::Bucket", status: "READY", marker: { stack: "harness-fixture", env: opts.environment } },
|
|
278
|
+
}),
|
|
279
|
+
});
|
|
280
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
281
|
+
|
|
282
|
+
const meta = await stack.assertLive("bucket");
|
|
283
|
+
expect(meta.status).toBe("READY");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("checks status when given", async () => {
|
|
287
|
+
await writeProject(dir);
|
|
288
|
+
const plugin = createMockPlugin({
|
|
289
|
+
name: "mock",
|
|
290
|
+
describeResources: (opts) =>
|
|
291
|
+
Promise.resolve({
|
|
292
|
+
bucket: { type: "Mock::Bucket", status: "READY", marker: { stack: "harness-fixture", env: opts.environment } },
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
296
|
+
|
|
297
|
+
await expect(stack.assertLive("bucket", { status: "READY" })).resolves.toBeDefined();
|
|
298
|
+
await expect(stack.assertLive("bucket", { status: "GONE" })).rejects.toThrow(LiveAssertionError);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("throws naming the entity when observed absent", async () => {
|
|
302
|
+
await writeProject(dir);
|
|
303
|
+
const plugin = createMockPlugin({ name: "mock", describeResources: staticObservation({}) });
|
|
304
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
305
|
+
|
|
306
|
+
await expect(stack.assertLive("bucket")).rejects.toThrow(/bucket.*observed absent/s);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("throws UnobservedAssertionError, not a plain failure, when NOT-OBSERVED", async () => {
|
|
310
|
+
await writeProject(dir);
|
|
311
|
+
const plugin = createMockPlugin({
|
|
312
|
+
name: "mock",
|
|
313
|
+
describeResources: staticObservation({}, { bucket: { reason: "no-binding", type: "Mock::Bucket" } }),
|
|
314
|
+
});
|
|
315
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
316
|
+
|
|
317
|
+
const err = await stack.assertLive("bucket").then(
|
|
318
|
+
() => undefined,
|
|
319
|
+
(e: unknown) => e,
|
|
320
|
+
);
|
|
321
|
+
expect(err).toBeInstanceOf(UnobservedAssertionError);
|
|
322
|
+
expect(err).not.toBeInstanceOf(LiveAssertionError);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("throws when the observed resource carries another environment's marker — a same-named leftover cannot satisfy it", async () => {
|
|
326
|
+
await writeProject(dir);
|
|
327
|
+
const plugin = createMockPlugin({
|
|
328
|
+
name: "mock",
|
|
329
|
+
describeResources: staticObservation({
|
|
330
|
+
bucket: { type: "Mock::Bucket", status: "READY", marker: { stack: "harness-fixture", env: "test-someone-else-000000" } },
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
334
|
+
|
|
335
|
+
await expect(stack.assertLive("bucket")).rejects.toThrow(/not this deploy's/);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("an unknown entity name throws before any read is attempted", async () => {
|
|
339
|
+
await writeProject(dir);
|
|
340
|
+
const describeResources = vi.fn(staticObservation({}));
|
|
341
|
+
const plugin = createMockPlugin({ name: "mock", describeResources });
|
|
342
|
+
const stack = await deployStack(harness(nativeApply, { plugins: [plugin] }));
|
|
343
|
+
|
|
344
|
+
await expect(stack.assertLive("nope")).rejects.toThrow(/no such entity/);
|
|
345
|
+
expect(describeResources).not.toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
261
348
|
});
|
package/src/testing.ts
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* in-process for exactly this suite's environment. Stateless by design:
|
|
16
16
|
* a crashed suite's environment is recovered by calling destroy again (or
|
|
17
17
|
* `chant lifecycle teardown <env> --yes`).
|
|
18
|
+
* - **assertLive** (#1857) — `describeResources()` against exactly one
|
|
19
|
+
* declared entity, turned into a pass or a thrown failure. Preserves
|
|
20
|
+
* #1089's tri-state rather than collapsing it: NOT-OBSERVED throws {@link
|
|
21
|
+
* UnobservedAssertionError}, never a silent pass and never an ordinary
|
|
22
|
+
* failure, because chant could not read the entity is not the same claim
|
|
23
|
+
* as chant read it and it is gone.
|
|
18
24
|
*
|
|
19
25
|
* ## Isolation
|
|
20
26
|
*
|
|
@@ -44,7 +50,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
44
50
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
45
51
|
import { tmpdir } from "node:os";
|
|
46
52
|
import { build } from "./build";
|
|
47
|
-
import type
|
|
53
|
+
import { isResourceDeclarable, type Declarable } from "./declarable";
|
|
48
54
|
import type { SerializerResult } from "./serializer";
|
|
49
55
|
import {
|
|
50
56
|
loadChantConfigUpward,
|
|
@@ -55,12 +61,20 @@ import { resolveBuildParams, type BuildParamValue } from "./build-params";
|
|
|
55
61
|
import { ENV_VAR, unknownEnvError } from "./env";
|
|
56
62
|
import { applyLiveEndpoint } from "./live-endpoint";
|
|
57
63
|
import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "./cli/plugins";
|
|
58
|
-
import type { LexiconPlugin } from "./lexicon";
|
|
64
|
+
import type { LexiconPlugin, ResourceMetadata } from "./lexicon";
|
|
59
65
|
import type { OwnershipMarker } from "./ownership";
|
|
60
66
|
import { runOpLocally } from "./op/local-executor";
|
|
61
67
|
import { loadActivities, loadProfiles, type ActivityFn, type ActivityProfile } from "./op/activity-registry";
|
|
62
68
|
import type { OpConfig, ActivityStep } from "./op/types";
|
|
63
69
|
import { executeTeardown, type TeardownReport } from "./lifecycle/teardown";
|
|
70
|
+
import {
|
|
71
|
+
assertLiveEntity,
|
|
72
|
+
LiveAssertionError,
|
|
73
|
+
UnobservedAssertionError,
|
|
74
|
+
type AssertLiveOptions,
|
|
75
|
+
} from "./lifecycle/assert-live";
|
|
76
|
+
|
|
77
|
+
export { LiveAssertionError, UnobservedAssertionError, type AssertLiveOptions };
|
|
64
78
|
|
|
65
79
|
/**
|
|
66
80
|
* Which `nativeApply` target deploys each lexicon's built output. Only these
|
|
@@ -118,6 +132,19 @@ export interface DeployedStack {
|
|
|
118
132
|
entities: Map<string, Declarable>;
|
|
119
133
|
/** The environment this deploy targeted — the teardown key. */
|
|
120
134
|
env: string;
|
|
135
|
+
/**
|
|
136
|
+
* Assert that a declared entity is live: observed present in this
|
|
137
|
+
* deploy's environment, not confirmed as another stack/env's resource,
|
|
138
|
+
* and — when `status` is given — reporting that status.
|
|
139
|
+
*
|
|
140
|
+
* Rides the observation contract (#1089): an entity `describeResources`
|
|
141
|
+
* could not cover throws {@link UnobservedAssertionError}, never a pass
|
|
142
|
+
* and never a plain failure — NOT-OBSERVED is not the same claim as
|
|
143
|
+
* absent. Observed-absent, a confirmed-foreign identity, or a status
|
|
144
|
+
* mismatch throws {@link LiveAssertionError}. Resolves to the entity's
|
|
145
|
+
* `ResourceMetadata` on success.
|
|
146
|
+
*/
|
|
147
|
+
assertLive(name: string, options?: AssertLiveOptions): Promise<ResourceMetadata>;
|
|
121
148
|
/**
|
|
122
149
|
* Tear down everything carrying this suite's marker `{ stack, env }`.
|
|
123
150
|
* Throws {@link TeardownIncompleteError} when any candidate failed to
|
|
@@ -321,6 +348,39 @@ export async function deployStack(options: DeployStackOptions): Promise<Deployed
|
|
|
321
348
|
applied.restore();
|
|
322
349
|
}
|
|
323
350
|
|
|
351
|
+
const assertLive = async (name: string, assertOptions: AssertLiveOptions = {}): Promise<ResourceMetadata> => {
|
|
352
|
+
const entity = result.entities.get(name);
|
|
353
|
+
if (!entity) {
|
|
354
|
+
throw new LiveAssertionError(
|
|
355
|
+
`assertLive("${name}"): no such entity — this deploy built ${[...result.entities.keys()].join(", ") || "nothing"}.`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const entityPlugin = plugins.find((p) => p.name === entity.lexicon);
|
|
359
|
+
if (!entityPlugin) {
|
|
360
|
+
throw new LiveAssertionError(`assertLive("${name}"): no loaded lexicon named "${entity.lexicon}".`);
|
|
361
|
+
}
|
|
362
|
+
const entityOutput = result.outputs.get(entity.lexicon);
|
|
363
|
+
const entityBuildOutput =
|
|
364
|
+
entityOutput === undefined ? "" : typeof entityOutput === "string" ? entityOutput : entityOutput.primary;
|
|
365
|
+
const props = isResourceDeclarable(entity) ? ((entity.props ?? {}) as Record<string, unknown>) : {};
|
|
366
|
+
|
|
367
|
+
const endpointForAssert = applyLiveEndpoint(config.environments, env, plugins);
|
|
368
|
+
try {
|
|
369
|
+
return await assertLiveEntity({
|
|
370
|
+
plugin: entityPlugin,
|
|
371
|
+
name,
|
|
372
|
+
entityType: entity.entityType,
|
|
373
|
+
props,
|
|
374
|
+
buildOutput: entityBuildOutput,
|
|
375
|
+
environment: env,
|
|
376
|
+
marker,
|
|
377
|
+
...assertOptions,
|
|
378
|
+
});
|
|
379
|
+
} finally {
|
|
380
|
+
endpointForAssert.restore();
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
324
384
|
const destroy = async (): Promise<TeardownReport> => {
|
|
325
385
|
const endpointForTeardown = applyLiveEndpoint(config.environments, env, plugins);
|
|
326
386
|
let report: TeardownReport;
|
|
@@ -334,5 +394,5 @@ export async function deployStack(options: DeployStackOptions): Promise<Deployed
|
|
|
334
394
|
return report;
|
|
335
395
|
};
|
|
336
396
|
|
|
337
|
-
return { outputs: result.outputs, entities: result.entities, env, destroy };
|
|
397
|
+
return { outputs: result.outputs, entities: result.entities, env, assertLive, destroy };
|
|
338
398
|
}
|