@intentius/chant 0.124.10 → 0.124.11
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/gate-resume.d.ts +85 -10
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/gate-resume-run.d.ts +87 -0
- package/dist/gate-resume-run.d.ts.map +1 -0
- package/dist/lifecycle/gate-applied.d.ts +44 -1
- package/dist/lifecycle/gate-applied.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-resume-plan.d.ts +113 -0
- package/dist/op/gate-resume-plan.d.ts.map +1 -0
- package/dist/op/gate-resume.d.ts +8 -64
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-run-locator.d.ts +1 -1
- package/dist/op/index.d.ts +2 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/point-decision.d.ts +11 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/gate-resume.test.ts +140 -2
- package/src/cli/handlers/gate-resume.ts +174 -65
- package/src/cli/main.ts +8 -3
- package/src/codegen/pinned-upgrade.test.ts +8 -1
- package/src/gate-decision-bundle.test.ts +1 -0
- package/src/gate-resume-run-bundle.test.ts +49 -0
- package/src/gate-resume-run.ts +171 -0
- package/src/lifecycle/gate-applied.ts +63 -1
- package/src/lifecycle/gate-ledger.test.ts +7 -1
- package/src/lifecycle/gate-ledger.ts +1 -1
- package/src/op/gate-resume-plan-bundle.test.ts +49 -0
- package/src/op/gate-resume-plan.test.ts +107 -0
- package/src/op/gate-resume-plan.ts +228 -0
- package/src/op/gate-resume.test.ts +18 -0
- package/src/op/gate-resume.ts +16 -150
- package/src/op/gate-run-locator.ts +1 -1
- package/src/op/gate.ts +1 -1
- package/src/op/index.ts +2 -0
- package/src/op/op-waves-point-mcp.e2e.test.ts +148 -0
- package/src/op/op-waves-point.test.ts +60 -2
- package/src/op/op-waves-run.ts +3 -0
- package/src/point-decision.ts +19 -2
|
@@ -13,7 +13,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { assertOpWavesSpec, type OpWavesSpec } from "./op-waves";
|
|
16
|
-
import { runOpWave, type OpWaveDecision, type OpWaveExec, type OpWaveShowAtBase } from "./op-waves-run";
|
|
16
|
+
import { runOpWave, type OpWaveDecision, type OpWaveExec, type OpWaveRunsFile, type OpWaveShowAtBase } from "./op-waves-run";
|
|
17
17
|
import { memoryGateLedgerPort } from "./gate";
|
|
18
18
|
import { waveSetDigest } from "../gated-waves";
|
|
19
19
|
import { pointGateResumable, type GatePointAsker, type GatePointQuestion, type GatePointRequest } from "../point-decision";
|
|
@@ -218,6 +218,62 @@ describe("a wave gate that asks a decision point (#3836)", () => {
|
|
|
218
218
|
h.done();
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
test("the plans file's pending fields and the spec's neverOverMcp land on the point gate's pending fact (#3863)", async () => {
|
|
222
|
+
const tg: OpWavesSpec = {
|
|
223
|
+
name: "tf-apply",
|
|
224
|
+
op: "tf-apply",
|
|
225
|
+
gateName: "wave-{k}",
|
|
226
|
+
planWave: ["tg", "plan", "{wave}", "{runs}", "{plans}"],
|
|
227
|
+
applyWave: ["tg", "apply", "{wave}", "{runs}", "{plans}"],
|
|
228
|
+
point: { name: "prod-apply" },
|
|
229
|
+
waves: [{ name: "net", runs: [{ target: "vpc" }, { target: "nat" }] }],
|
|
230
|
+
};
|
|
231
|
+
const members = [{ member: "vpc", planDigest: digest("1") }, { member: "nat", planDigest: digest("2") }];
|
|
232
|
+
const cwd = mkdtempSync(join(tmpdir(), "op-waves-point-3863-"));
|
|
233
|
+
const exec: OpWaveExec = (argv, dir) => {
|
|
234
|
+
const runs = JSON.parse(readFileSync(join(dir, argv[3]!), "utf-8")) as OpWaveRunsFile;
|
|
235
|
+
if (argv[1] === "plan") {
|
|
236
|
+
writeFileSync(join(dir, argv[4]!), JSON.stringify({
|
|
237
|
+
runs: runs.runs.map((r) => ({ target: r.target, planDigest: members.find((m) => m.member === r.target)!.planDigest })),
|
|
238
|
+
pending: { commit: "c".repeat(40), members, description: "wave 1 of 1: vpc, nat" },
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
};
|
|
243
|
+
// neverOverMcp at base counts even when the change drops it.
|
|
244
|
+
const show: OpWaveShowAtBase = () => ({ sha: "b".repeat(40), text: JSON.stringify({ ...tg, neverOverMcp: true }) });
|
|
245
|
+
const gates = memoryGateLedgerPort();
|
|
246
|
+
const asker = stubAsker();
|
|
247
|
+
const waiting = await runOpWave({ spec: tg, specFile: "waves.json", wave: 1, cwd, exec, show, gates, points: asker, now: NOW, log: () => {} });
|
|
248
|
+
expect(waiting.exitCode).toBe(3);
|
|
249
|
+
expect(gates.appended).toHaveLength(1);
|
|
250
|
+
expect(gates.appended[0]).toMatchObject({
|
|
251
|
+
op: "tf-apply",
|
|
252
|
+
gate: "wave-1",
|
|
253
|
+
commit: "c".repeat(40),
|
|
254
|
+
members,
|
|
255
|
+
description: "wave 1 of 1: vpc, nat",
|
|
256
|
+
neverOverMcp: true,
|
|
257
|
+
answer: { point: "prod-apply" },
|
|
258
|
+
});
|
|
259
|
+
expect(waiting.decision?.pendingFact).toMatchObject({ commit: "c".repeat(40), neverOverMcp: true });
|
|
260
|
+
|
|
261
|
+
// A standing fact for the same question that does not carry neverOverMcp does not stand: the run records one that does.
|
|
262
|
+
const old = { ...gates.appended[0]! };
|
|
263
|
+
delete (old as { neverOverMcp?: true }).neverOverMcp;
|
|
264
|
+
const stale = memoryGateLedgerPort({ pending: [old] });
|
|
265
|
+
await runOpWave({ spec: tg, specFile: "waves.json", wave: 1, cwd, exec, show, gates: stale, points: asker, now: NOW, log: () => {} });
|
|
266
|
+
expect(stale.appended).toHaveLength(1);
|
|
267
|
+
expect(stale.appended[0]).toMatchObject({ neverOverMcp: true });
|
|
268
|
+
|
|
269
|
+
// Without neverOverMcp at base, the fact carries the fields and no neverOverMcp.
|
|
270
|
+
const plain = memoryGateLedgerPort();
|
|
271
|
+
await runOpWave({ spec: tg, specFile: "waves.json", wave: 1, cwd, exec, show: () => ({ sha: "b".repeat(40), text: JSON.stringify(tg) }), gates: plain, points: asker, now: NOW, log: () => {} });
|
|
272
|
+
expect(plain.appended[0]).toMatchObject({ commit: "c".repeat(40), members });
|
|
273
|
+
expect(plain.appended[0]).not.toHaveProperty("neverOverMcp");
|
|
274
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
275
|
+
});
|
|
276
|
+
|
|
221
277
|
test("resume picks up the waiting wave once its question is answered (#3835)", async () => {
|
|
222
278
|
vi.stubEnv("GITLAB_CI", "");
|
|
223
279
|
vi.stubEnv("GITHUB_ACTIONS", "true");
|
|
@@ -245,7 +301,9 @@ describe("a wave gate that asks a decision point (#3836)", () => {
|
|
|
245
301
|
expect(await resumeStanding(fact, [], { questions: new Map([[id, yes]]), dryRun: true })).toMatchObject({
|
|
246
302
|
op: "tf-apply",
|
|
247
303
|
gate: "tf-apply-wave-1",
|
|
248
|
-
status: "
|
|
304
|
+
status: "would-resume",
|
|
305
|
+
resumable: true,
|
|
306
|
+
reason: "answered",
|
|
249
307
|
approvedBy: "alice",
|
|
250
308
|
message: expect.stringContaining("would resume github run 4242"),
|
|
251
309
|
});
|
package/src/op/op-waves-run.ts
CHANGED
|
@@ -1696,6 +1696,9 @@ async function settlePointGate(options: RunOpWaveOptions, ctx: WaveContext, deci
|
|
|
1696
1696
|
...(point.timeout ? { timeout: point.timeout } : {}),
|
|
1697
1697
|
planDigest: digest,
|
|
1698
1698
|
...(options.now ? { now: options.now } : {}),
|
|
1699
|
+
// #3863: the plans file's pending fields and the spec's neverOverMcp, as settleGate passes them.
|
|
1700
|
+
...(decision.pending ? { fields: decision.pending } : {}),
|
|
1701
|
+
...(decision.neverOverMcp ? { neverOverMcp: true } : {}),
|
|
1699
1702
|
});
|
|
1700
1703
|
if (check.satisfied) {
|
|
1701
1704
|
decision.status = "approved";
|
package/src/point-decision.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { execFile } from "node:child_process";
|
|
|
31
31
|
import { isPendingGateExpired, latestPendingGate } from "./lifecycle/gate-ledger-select";
|
|
32
32
|
import { DEFAULT_GATE_EXPIRY, resolveApprovalUrl } from "./lifecycle/gate-pending";
|
|
33
33
|
import { samePlanDigest } from "./lifecycle/plan-digest";
|
|
34
|
-
import type { GateAnswerRef, GateResolutionRecord, PendingGateRecord } from "./lifecycle/gate-ledger";
|
|
34
|
+
import type { GateAnswerRef, GateResolutionRecord, PendingGateInput, PendingGateRecord } from "./lifecycle/gate-ledger";
|
|
35
35
|
import { parseDuration } from "./op/duration";
|
|
36
36
|
import type { GateLedgerPort } from "./op/gate";
|
|
37
37
|
import { DEFAULT_GATE_POINT_PASS, defaultGatePointStore, gatePointInputs, gateSubject, type GatePoint, type GatePointFacts, type GatePointStore } from "./op/gate-point";
|
|
@@ -121,6 +121,17 @@ export interface PointGateInput {
|
|
|
121
121
|
* ({@link resolveGateRunLocator}); false records none.
|
|
122
122
|
*/
|
|
123
123
|
resume?: GateRunLocator | false;
|
|
124
|
+
/**
|
|
125
|
+
* Fields a caller adds to the pending fact it records (#3863), such as an
|
|
126
|
+
* Op waves product's commit and members, as `evaluateGate` takes them.
|
|
127
|
+
* Every field this function writes itself wins over one of the same name.
|
|
128
|
+
*/
|
|
129
|
+
fields?: Record<string, unknown>;
|
|
130
|
+
/**
|
|
131
|
+
* The pending fact it records carries `neverOverMcp` (#3863): an answer to
|
|
132
|
+
* the question over MCP or ACP is refused (`answer-never-over-mcp`, #3817).
|
|
133
|
+
*/
|
|
134
|
+
neverOverMcp?: boolean;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
/** What reaching a gate that asks a point decided. */
|
|
@@ -198,12 +209,17 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
|
|
|
198
209
|
const waiting: WaitingPoint = { id: q.id, point: q.point, state: q.state === "proposed" ? "proposed" : "escalated", path: q.path, subject: q.subject, steward: q.steward };
|
|
199
210
|
const ledger = await port.read(input.op);
|
|
200
211
|
const standing = latestPendingGate(ledger.pending.filter((p) => p.environment === undefined), input.gate);
|
|
201
|
-
|
|
212
|
+
// #3863: a standing fact that does not carry the gate's neverOverMcp does not stand, so the answer path reads the rule.
|
|
213
|
+
if (
|
|
214
|
+
standing && !isPendingGateExpired(standing, now) && samePlanDigest(standing.planDigest, input.planDigest) && standing.answer?.id === q.id &&
|
|
215
|
+
(!input.neverOverMcp || standing.neverOverMcp === true)
|
|
216
|
+
) {
|
|
202
217
|
return { satisfied: false, waiting, pending: standing, recorded: false };
|
|
203
218
|
}
|
|
204
219
|
const url = resolveApprovalUrl();
|
|
205
220
|
const resume = input.resume === false ? undefined : (input.resume ?? resolveGateRunLocator());
|
|
206
221
|
const { record, pushed, pushWarning } = await port.appendPending({
|
|
222
|
+
...(input.fields as Partial<PendingGateInput> | undefined),
|
|
207
223
|
op: input.op,
|
|
208
224
|
gate: input.gate,
|
|
209
225
|
timestamp: now,
|
|
@@ -214,6 +230,7 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
|
|
|
214
230
|
...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
|
|
215
231
|
...(resume ? { resume } : {}),
|
|
216
232
|
answer: { ...ref, ...(input.point.pass ? { pass: input.point.pass } : {}) },
|
|
233
|
+
...(input.neverOverMcp ? { neverOverMcp: true as const } : {}),
|
|
217
234
|
});
|
|
218
235
|
return { satisfied: false, waiting, pending: record, recorded: true, pushed, ...(pushWarning ? { pushWarning } : {}) };
|
|
219
236
|
}
|