@intentius/chant 0.124.40 → 0.124.41
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/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -1
- package/dist/cli/handlers/fan-out.js +16 -3
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +74 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +503 -53
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +41 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -2
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/fan-out-run.d.ts +28 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +27 -3
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +72 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +96 -5
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +5 -3
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +62 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +190 -11
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +36 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +82 -0
- package/dist/pr-loop.d.ts +90 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +77 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +242 -242
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +64 -3
- package/dist/workspace/declaration.d.ts +63 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +147 -0
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +73 -2
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +4 -1
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/fan-out.ts +17 -4
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +496 -59
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/main.ts +41 -2
- package/src/cli/parse-args.ts +23 -2
- package/src/cli/registry.ts +12 -2
- package/src/components/fan-out-run.ts +39 -2
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +130 -5
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +5 -2
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +233 -11
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +114 -0
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +146 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +107 -8
- package/src/workspace/ci-run.ts +72 -4
- package/src/workspace/declaration.schema.json +147 -0
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +134 -2
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +73 -2
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/src/op/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Op, phase, activity, gate, effect, build, kubectlApply, helmInstall, helmInstallPinned, waitForStack, waitForReady,
|
|
2
|
-
gitlabPipeline, lifecycleSnapshot, shell, sourceArchive, releasePlan, releaseRecord, composeChangeSet, lifecyclePlanChangeSet, readChangeSetPart, releaseRollbackPlan, releaseRollbackRecord, decide, ensureSecret, teardown, envTeardown, k3dUp, k3dDown,
|
|
2
|
+
gitlabPipeline, lifecycleSnapshot, shell, sourceArchive, releasePlan, releaseRecord, composeChangeSet, lifecyclePlanChangeSet, readChangeSetPart, reviewAgent, releaseRollbackPlan, releaseRollbackRecord, decide, ensureSecret, teardown, envTeardown, k3dUp, k3dDown,
|
|
3
3
|
k3sInstall, k3sUninstall, flociUp, flociDown,
|
|
4
4
|
flociAzUp, flociAzDown, flociGcpUp, flociGcpDown, httpCheck,
|
|
5
5
|
azGroupEnsure, azGroupDelete, azApply, azDelete, awsApply, awsDelete, gcpApply, gcpDelete, policyGate,
|
|
@@ -57,10 +57,13 @@ export {
|
|
|
57
57
|
OP_WAVE_DEFAULT_GATE_NAME,
|
|
58
58
|
OP_WAVE_GATE_POLICIES,
|
|
59
59
|
OP_WAVE_HOOK_MOMENTS,
|
|
60
|
+
PR_LOOP_HOOK_MOMENTS,
|
|
60
61
|
assertOpWaveHooks,
|
|
61
62
|
assertOpWavesSpec,
|
|
63
|
+
assertPrLoopHooks,
|
|
62
64
|
opWaveApplyHooksRunBy,
|
|
63
65
|
opWaveGateName,
|
|
66
|
+
opWaveHookRuns,
|
|
64
67
|
opWaveHooksAt,
|
|
65
68
|
opWaveJobs,
|
|
66
69
|
opWaveRecordPath,
|
|
@@ -71,6 +74,7 @@ export {
|
|
|
71
74
|
OP_WAVE_APPLY_OUTCOMES,
|
|
72
75
|
OP_WAVE_MAX_REPLANS,
|
|
73
76
|
opWaveHooksAtBase,
|
|
77
|
+
runStageHooks,
|
|
74
78
|
opWaveNeedsApproval,
|
|
75
79
|
opWavePlansPath,
|
|
76
80
|
opWaveRunsPath,
|
|
@@ -102,6 +106,8 @@ export type {
|
|
|
102
106
|
OpWaveShowAtBase,
|
|
103
107
|
RecordHeadPlansOptions,
|
|
104
108
|
RunOpWaveApplyHooksOptions,
|
|
109
|
+
RunStageHooksOptions,
|
|
110
|
+
StageHooksRun,
|
|
105
111
|
RunOpWaveOptions,
|
|
106
112
|
RunOpWaveResult,
|
|
107
113
|
} from "./op-waves-run";
|
|
@@ -130,6 +136,12 @@ export type {
|
|
|
130
136
|
GateApproval, GateApprovalMode, GateApprover, GateContextValue, GatePolicyAnswer, GatePolicyDecision,
|
|
131
137
|
GatePolicyEvaluator, GatePolicyRef, GatePolicyRequest, GateQuorum, ResolvedGateApproval,
|
|
132
138
|
} from "./gate-approval";
|
|
139
|
+
export {
|
|
140
|
+
quorumRules, quorumRulesFor, quorumNeed, quorumProblems, quorumRuleProgress, quorumRuleLine, quorumWaiting, rolesOf, trustQuorumJudge, UNPROVEN_JUDGE,
|
|
141
|
+
} from "./gate-quorum";
|
|
142
|
+
export type {
|
|
143
|
+
GateQuorumRule, GateQuorumSpec, GateQuorumJudge, GateQuorumRuleProgress, QuorumJudgement, QuorumApprover, TrustQuorumInput,
|
|
144
|
+
} from "./gate-quorum";
|
|
133
145
|
export {
|
|
134
146
|
gatePlanSummary, gatePlanSummaryOfResult, changeSetOfResult, GATE_PLAN_CONTEXT_KEY, GATE_PLAN_ADDRESS_LIMIT,
|
|
135
147
|
} from "./gate-plan-context";
|
package/src/op/local-executor.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { resolveGateAliases } from "./gate-alias";
|
|
|
41
41
|
import { gatePointOf } from "./gate-point";
|
|
42
42
|
import { evaluatePointGate, workspaceGatePointAsker, type GatePointAsker } from "./gate-point-run";
|
|
43
43
|
import type { ResolvedGateApproval } from "./gate-approval";
|
|
44
|
+
import { quorumRules, quorumRulesFor } from "./gate-quorum";
|
|
44
45
|
import { GATE_PLAN_CONTEXT_KEY, gatePlanSummaryOfResult } from "./gate-plan-context";
|
|
45
46
|
import { withOpRunContext, type OpRunContext, type PassedGate } from "./run-context";
|
|
46
47
|
import type { GateAnswerRef, PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
@@ -732,6 +733,7 @@ function gateFn(step: GateStep): string {
|
|
|
732
733
|
function resolveGateApproval(
|
|
733
734
|
step: GateStep,
|
|
734
735
|
resultsById: ReadonlyMap<string, unknown>,
|
|
736
|
+
env?: string,
|
|
735
737
|
): ResolvedGateApproval | undefined {
|
|
736
738
|
const authored = step.approval;
|
|
737
739
|
if (!authored) return undefined;
|
|
@@ -750,8 +752,13 @@ function resolveGateApproval(
|
|
|
750
752
|
const summary = gatePlanSummaryOfResult(resultsById.get(step.plan.step));
|
|
751
753
|
if (summary) context = { ...(context ?? {}), [GATE_PLAN_CONTEXT_KEY]: summary };
|
|
752
754
|
}
|
|
755
|
+
// #4290: the rules this run's gate takes, by its environment. A rule with
|
|
756
|
+
// `targets` never covers a run with no environment. One rule with no
|
|
757
|
+
// `targets` keeps its own shape, so a standing pending fact still matches.
|
|
758
|
+
const single = authored.quorum !== undefined && !Array.isArray(authored.quorum) && (authored.quorum as { targets?: unknown }).targets === undefined;
|
|
759
|
+
const rules = authored.quorum ? quorumRulesFor(quorumRules(authored.quorum), env !== undefined ? [env] : []) : [];
|
|
753
760
|
return {
|
|
754
|
-
...(
|
|
761
|
+
...(single ? { quorum: authored.quorum } : rules.length > 0 ? { quorum: rules } : {}),
|
|
755
762
|
...(authored.policy ? { policy: authored.policy } : {}),
|
|
756
763
|
mode: authored.mode ?? "log-only",
|
|
757
764
|
...(context ? { context } : {}),
|
|
@@ -779,7 +786,7 @@ async function runGateStep(
|
|
|
779
786
|
// a run over a missing digest.
|
|
780
787
|
const resolvedPlan = resolveStepOutputRefs(step.plan, resultsById);
|
|
781
788
|
const planDigest = typeof resolvedPlan === "string" && resolvedPlan !== "" ? resolvedPlan : undefined;
|
|
782
|
-
const approval = resolveGateApproval(step, resultsById);
|
|
789
|
+
const approval = resolveGateApproval(step, resultsById, gates.env);
|
|
783
790
|
// #3947: a gate that resumes stores the outputs the steps after it read.
|
|
784
791
|
const resumeAt = gates.resume?.gates.get(step);
|
|
785
792
|
let checkpoint: GateCheckpoint | undefined;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook `targets` and the `before.observe` moment (#4288), in an Op waves
|
|
3
|
+
* job and through `runStageHooks`.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { assertOpWaveHooks, assertPrLoopHooks, opWaveHookRuns, opWaveHooksAt, OP_WAVE_HOOK_MOMENTS, type OpWavesSpec } from "./op-waves";
|
|
10
|
+
import { runOpWave, runStageHooks, type OpWaveExec, type OpWaveShowAtBase } from "./op-waves-run";
|
|
11
|
+
import { memoryGateLedgerPort } from "./gate";
|
|
12
|
+
|
|
13
|
+
const digest = (n: string) => `sha256:${n.repeat(64)}`;
|
|
14
|
+
|
|
15
|
+
describe("hook targets and before.observe in the spec (#4288)", () => {
|
|
16
|
+
test("before.observe is the first moment, and opWaveHooksAt reads it", () => {
|
|
17
|
+
expect(OP_WAVE_HOOK_MOMENTS[0]).toBe("before-observe");
|
|
18
|
+
const hooks = opWaveHooksAt({ hooks: { before: { observe: [{ command: ["init"] }] } }, waves: [{ name: "prod", runs: [] }] }, "prod");
|
|
19
|
+
expect(hooks["before-observe"]).toEqual([{ command: ["init"] }]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("the spec refuses after.observe, {plan} before the observe and targets that are not a list of names", () => {
|
|
23
|
+
expect(() => assertOpWaveHooks("w", { after: { observe: [{ command: ["x"] }] } } as never)).toThrow(/hooks.after.observe is not a moment/);
|
|
24
|
+
expect(() => assertOpWaveHooks("w", { before: { observe: [{ command: ["x", "{plan}"], perRun: true }] } })).toThrow(/no \{plan\} yet/);
|
|
25
|
+
expect(() => assertOpWaveHooks("w", { before: { plan: [{ command: ["x"], targets: [] }] } })).toThrow(/targets must be a non-empty list/);
|
|
26
|
+
expect(() => assertOpWaveHooks("w", { before: { plan: [{ command: ["x"], targets: ["prod/*", 3] as never }] } })).toThrow(/targets must be a non-empty list/);
|
|
27
|
+
expect(() => assertOpWaveHooks("w", { before: { observe: [{ command: ["x"], targets: ["prod/**"], onFailure: "approve" }] } })).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a pull-request job refuses after.gate and apply.runBy", () => {
|
|
31
|
+
expect(() => assertPrLoopHooks("ci.hooks", { after: { gate: [{ command: ["x"] }] } })).toThrow(/after.gate runs in an Op waves job only/);
|
|
32
|
+
expect(() => assertPrLoopHooks("ci.hooks", { apply: { runBy: "apply" } })).toThrow(/runBy is for an Op waves spec/);
|
|
33
|
+
expect(() => assertPrLoopHooks("ci.hooks", { before: { observe: [{ command: ["x"] }], apply: [{ command: ["y"] }] } })).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("targets match a run by name or by a pattern, as a job group's runs do", () => {
|
|
37
|
+
const runs = [{ target: "prod/eu" }, { target: "prod/us/east" }, { target: "dev" }];
|
|
38
|
+
expect(opWaveHookRuns({}, runs).map((r) => r.target)).toEqual(["prod/eu", "prod/us/east", "dev"]);
|
|
39
|
+
expect(opWaveHookRuns({ targets: ["prod/*"] }, runs).map((r) => r.target)).toEqual(["prod/eu"]);
|
|
40
|
+
expect(opWaveHookRuns({ targets: ["prod/**", "dev"] }, runs).map((r) => r.target)).toEqual(["prod/eu", "prod/us/east", "dev"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("runOpWave with targets and before.observe (#4288)", () => {
|
|
45
|
+
const spec: OpWavesSpec = {
|
|
46
|
+
name: "roots",
|
|
47
|
+
op: "tf",
|
|
48
|
+
plan: ["tool", "plan", "{target}", "{plan}"],
|
|
49
|
+
apply: ["tool", "apply", "{target}", "{plan}"],
|
|
50
|
+
hooks: {
|
|
51
|
+
before: {
|
|
52
|
+
observe: [{ name: "init", command: ["hook", "init", "{target}"], perRun: true, targets: ["eu/*"] }],
|
|
53
|
+
plan: [{ name: "us-only", command: ["hook", "us", "{runs}"], targets: ["us/*"] }],
|
|
54
|
+
},
|
|
55
|
+
after: { plan: [{ name: "eu-check", command: ["hook", "eu-check", "{runs}"], targets: ["eu/*"] }] },
|
|
56
|
+
},
|
|
57
|
+
waves: [{ name: "prod", runs: [{ target: "eu/a" }, { target: "eu/b" }, { target: "asia/c" }], gate: "never" }],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function harness(fails: string[] = []) {
|
|
61
|
+
const cwd = mkdtempSync(join(tmpdir(), "op-waves-targets-"));
|
|
62
|
+
const calls: Array<{ argv: string; env?: Record<string, string> }> = [];
|
|
63
|
+
const exec: OpWaveExec = (argv, dir, env) => {
|
|
64
|
+
calls.push({ argv: argv.join(" "), ...(env ? { env } : {}) });
|
|
65
|
+
if (argv[1] === "plan") writeFileSync(join(dir, argv[3]!), JSON.stringify({ planDigest: digest(argv[2]!.endsWith("a") ? "1" : "2") }));
|
|
66
|
+
return fails.some((f) => argv.join(" ").startsWith(f)) ? 1 : 0;
|
|
67
|
+
};
|
|
68
|
+
const show: OpWaveShowAtBase = () => ({ sha: "b".repeat(40), text: JSON.stringify(spec) });
|
|
69
|
+
return { cwd, calls, exec, show, argv: () => calls.map((c) => c.argv), done: () => rmSync(cwd, { recursive: true, force: true }) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("before-observe runs ahead of before-plan, each hook only for the runs its targets match", async () => {
|
|
73
|
+
const h = harness();
|
|
74
|
+
const logs: string[] = [];
|
|
75
|
+
const result = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort(), log: (l) => logs.push(l) });
|
|
76
|
+
expect(result.exitCode).toBe(0);
|
|
77
|
+
expect(h.argv().filter((c) => c.startsWith("hook") || c.startsWith("tool plan"))).toEqual([
|
|
78
|
+
"hook init eu/a",
|
|
79
|
+
"hook init eu/b",
|
|
80
|
+
"tool plan eu/a .chant/op-waves/roots/wave-1/eu_a.plan.json",
|
|
81
|
+
"tool plan eu/b .chant/op-waves/roots/wave-1/eu_b.plan.json",
|
|
82
|
+
"tool plan asia/c .chant/op-waves/roots/wave-1/asia_c.plan.json",
|
|
83
|
+
"hook eu-check .chant/op-waves/roots/wave-1/runs.json",
|
|
84
|
+
]);
|
|
85
|
+
expect(h.calls[0]!.env).toMatchObject({ CHANT_HOOK: "before-observe", CHANT_TARGET: "eu/a" });
|
|
86
|
+
expect(logs).toContain("before-plan hook us-only: no run matches its targets (us/*), so it did not run");
|
|
87
|
+
expect(result.hooks.map((o) => `${o.moment} ${o.name}${o.target ? ` ${o.target}` : ""} ${o.outcome}`)).toEqual([
|
|
88
|
+
"before-observe init eu/a passed",
|
|
89
|
+
"before-observe init eu/b passed",
|
|
90
|
+
"after-plan eu-check passed",
|
|
91
|
+
]);
|
|
92
|
+
h.done();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a failed before-observe hook stops the wave before anything plans", async () => {
|
|
96
|
+
const h = harness(["hook init eu/b"]);
|
|
97
|
+
const result = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort(), log: () => {} });
|
|
98
|
+
expect(result.exitCode).toBe(1);
|
|
99
|
+
expect(h.argv().some((c) => c.startsWith("tool"))).toBe(false);
|
|
100
|
+
h.done();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("runStageHooks (#4288)", () => {
|
|
105
|
+
test("runs one moment's hooks for the runs given, with targets, and counts an approve failure as a hold", () => {
|
|
106
|
+
const cwd = mkdtempSync(join(tmpdir(), "stage-hooks-"));
|
|
107
|
+
const calls: string[] = [];
|
|
108
|
+
const exec: OpWaveExec = (argv) => {
|
|
109
|
+
calls.push(argv.join(" "));
|
|
110
|
+
return argv[1] === "gate" ? 1 : 0;
|
|
111
|
+
};
|
|
112
|
+
const ran = runStageHooks(
|
|
113
|
+
"before-plan",
|
|
114
|
+
[
|
|
115
|
+
{ name: "each", command: ["hook", "each", "{target}", "{op}"], perRun: true, targets: ["api"] },
|
|
116
|
+
{ name: "gate", command: ["hook", "gate", "{wave}"], onFailure: "approve" },
|
|
117
|
+
],
|
|
118
|
+
{ name: "pr-7", op: "pr-7", wave: "prod", runs: [{ target: "api", op: "pr-7" }, { target: "web", op: "pr-7" }], runsFile: "runs.json", holdFile: "hold.json", cwd, exec, log: () => {} },
|
|
119
|
+
);
|
|
120
|
+
expect(calls).toEqual(["hook each api pr-7", "hook gate prod"]);
|
|
121
|
+
expect(ran.failed).toBeUndefined();
|
|
122
|
+
expect(ran.holds).toEqual(["before-plan hook gate"]);
|
|
123
|
+
expect(ran.outcomes.map((o) => o.outcome)).toEqual(["passed", "held"]);
|
|
124
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plan the product's policy holds (#4294): the wave waits for a person's
|
|
3
|
+
* approval of its set digest whatever its gate policy says, and applies
|
|
4
|
+
* once one stands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import type { OpWavesSpec } from "./op-waves";
|
|
12
|
+
import { runOpWave, type OpWaveExec, type OpWaveShowAtBase } from "./op-waves-run";
|
|
13
|
+
import { memoryGateLedgerPort } from "./gate";
|
|
14
|
+
import { waveSetDigest } from "../gated-waves";
|
|
15
|
+
|
|
16
|
+
const spec: OpWavesSpec = {
|
|
17
|
+
name: "roots",
|
|
18
|
+
op: "tf",
|
|
19
|
+
plan: ["tool", "plan", "{target}", "--out", "{plan}"],
|
|
20
|
+
apply: ["tool", "apply", "{target}", "--plan", "{plan}"],
|
|
21
|
+
waves: [{ name: "dev", runs: [{ target: "vpc" }], gate: "never" }],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const digest = (n: string) => `sha256:${n.repeat(64)}`;
|
|
25
|
+
|
|
26
|
+
function harness(plan: Record<string, unknown>) {
|
|
27
|
+
const cwd = mkdtempSync(join(tmpdir(), "op-waves-hold-"));
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
const exec: OpWaveExec = (argv, dir) => {
|
|
30
|
+
calls.push(argv.join(" "));
|
|
31
|
+
if (argv[1] === "plan") writeFileSync(join(dir, argv[4]!), JSON.stringify(plan));
|
|
32
|
+
return 0;
|
|
33
|
+
};
|
|
34
|
+
const show: OpWaveShowAtBase = () => ({ sha: "b".repeat(40), text: JSON.stringify(spec) });
|
|
35
|
+
return { cwd, calls, exec, show, done: () => rmSync(cwd, { recursive: true, force: true }) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("a policy hold on an Op waves plan (#4294)", () => {
|
|
39
|
+
test("a held run makes a gate: never wave wait, and an approval of its digest lets it apply", async () => {
|
|
40
|
+
const h = harness({ planDigest: digest("1"), held: { rules: ["main.hold_iam", "main.hold_iam"], reason: "an IAM change" } });
|
|
41
|
+
const waiting = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort() });
|
|
42
|
+
expect(waiting.exitCode).toBe(3);
|
|
43
|
+
expect(waiting.decision).toMatchObject({ status: "waiting", policy: "never", heldBy: ["policy on vpc (main.hold_iam): an IAM change"] });
|
|
44
|
+
expect(waiting.decision!.members[0]).toMatchObject({ member: "vpc", held: { rules: ["main.hold_iam"] } });
|
|
45
|
+
expect(h.calls.some((c) => c.startsWith("tool apply"))).toBe(false);
|
|
46
|
+
|
|
47
|
+
const set = waveSetDigest([{ member: "vpc", planDigest: digest("1") }]);
|
|
48
|
+
const gates = memoryGateLedgerPort({ resolutions: [{ version: 1, op: "roots", gate: "roots-wave-1", resolvedBy: "alice", timestamp: "2026-10-10T00:00:00Z", planDigest: set }] });
|
|
49
|
+
const approved = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates });
|
|
50
|
+
expect(approved).toMatchObject({ exitCode: 0, applied: ["vpc"], decision: { status: "approved", approvedBy: "alice" } });
|
|
51
|
+
h.done();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a run with no hold under gate: never needs no approval", async () => {
|
|
55
|
+
const h = harness({ planDigest: digest("2") });
|
|
56
|
+
const free = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort() });
|
|
57
|
+
expect(free).toMatchObject({ exitCode: 0, applied: ["vpc"], decision: { status: "not-required" } });
|
|
58
|
+
h.done();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a hold that names no rule is refused", async () => {
|
|
62
|
+
const h = harness({ planDigest: digest("3"), held: { rules: [] } });
|
|
63
|
+
await expect(runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort() })).rejects.toThrow(/a hold is \{ "rules"/);
|
|
64
|
+
h.done();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { assertOpWavesSpec, type OpWavesSpec } from "./op-waves";
|
|
6
|
+
import { runOpWave, type OpWaveExec, type OpWaveShowAtBase } from "./op-waves-run";
|
|
7
|
+
import { memoryGateLedgerPort } from "./gate";
|
|
8
|
+
import { waveSetDigest } from "../gated-waves";
|
|
9
|
+
import type { TrustPolicy } from "../workspace/trust/policy";
|
|
10
|
+
import type { MergedReview, ReviewSource } from "./gate-review";
|
|
11
|
+
|
|
12
|
+
const digest = (n: string) => `sha256:${n.repeat(64)}`;
|
|
13
|
+
|
|
14
|
+
const spec: OpWavesSpec = {
|
|
15
|
+
name: "migrations",
|
|
16
|
+
op: "migrate",
|
|
17
|
+
plan: ["tool", "plan", "{target}", "--out", "{plan}"],
|
|
18
|
+
apply: ["tool", "apply", "{target}", "--plan", "{plan}"],
|
|
19
|
+
waves: [{ name: "prod", runs: [{ target: "prod-eu" }, { target: "prod-us" }] }],
|
|
20
|
+
};
|
|
21
|
+
const set = waveSetDigest([
|
|
22
|
+
{ member: "prod-eu", planDigest: digest("e") },
|
|
23
|
+
{ member: "prod-us", planDigest: digest("u") },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function harness(atBase: OpWavesSpec) {
|
|
27
|
+
const cwd = mkdtempSync(join(tmpdir(), "op-waves-quorum-"));
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
const exec: OpWaveExec = (argv, dir) => {
|
|
30
|
+
calls.push(argv.join(" "));
|
|
31
|
+
if (argv[1] === "plan") writeFileSync(join(dir, argv[4]!), JSON.stringify({ planDigest: digest(argv[2] === "prod-eu" ? "e" : "u") }));
|
|
32
|
+
return 0;
|
|
33
|
+
};
|
|
34
|
+
const show: OpWaveShowAtBase = () => ({ sha: "b".repeat(40), text: JSON.stringify(atBase) });
|
|
35
|
+
return { cwd, exec, show, applies: () => calls.filter((c) => c.startsWith("tool apply")).length, done: () => rmSync(cwd, { recursive: true, force: true }) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const trust = (roles: Record<string, string[]>): (() => TrustPolicy) => () =>
|
|
39
|
+
({ base: "b".repeat(40), signersPath: ".chant/allowed_signers", active: false, signers: [], excluded: [], roles, adopted: [], runners: [], excludedRunners: [], admitted: {}, problems: [] }) as TrustPolicy;
|
|
40
|
+
|
|
41
|
+
const reviews = (merged: MergedReview | null): ReviewSource => ({ kind: "github", mergedReview: async () => merged });
|
|
42
|
+
const approval = (who: string, at: string) => ({ version: 1 as const, op: "migrations", gate: "migrations-wave-1", resolvedBy: who, timestamp: at, planDigest: set });
|
|
43
|
+
|
|
44
|
+
describe("Op waves approval rules (#4290)", () => {
|
|
45
|
+
test("the spec checks approvals.quorum, and refuses it beside a decision point", () => {
|
|
46
|
+
expect(() => assertOpWavesSpec({ ...spec, approvals: { quorum: [{ targets: ["prod-*"], count: 2, roles: ["platform"], notAuthor: true }] } })).not.toThrow();
|
|
47
|
+
expect(() => assertOpWavesSpec({ ...spec, approvals: { quorum: [{ count: 0 }] } })).toThrow(/approvals.quorum\[0\].count/);
|
|
48
|
+
expect(() => assertOpWavesSpec({ ...spec, approvals: { quorum: { count: 1 } } } as unknown as OpWavesSpec)).toThrow(/must be a list of rules/);
|
|
49
|
+
expect(() => assertOpWavesSpec({ ...spec, point: "ship", approvals: { quorum: [{ count: 1 }] } })).toThrow(/both set/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a rule at base whose glob matches a run holds the wave until two people approve", async () => {
|
|
53
|
+
const atBase: OpWavesSpec = { ...spec, approvals: { quorum: [{ targets: ["prod-*"], count: 2 }, { targets: ["dev"], count: 5 }] } };
|
|
54
|
+
const h = harness(atBase);
|
|
55
|
+
const gates = memoryGateLedgerPort({ resolutions: [approval("alice", "2026-10-22T01:00:00Z")] });
|
|
56
|
+
const logs: string[] = [];
|
|
57
|
+
// The change drops the rule; the one at base holds.
|
|
58
|
+
const one = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates, now: "2026-10-22T02:00:00Z", log: (l) => logs.push(l) });
|
|
59
|
+
expect(one.exitCode).toBe(3);
|
|
60
|
+
expect(one.decision?.quorum).toEqual([{ targets: ["prod-*"], count: 2 }]);
|
|
61
|
+
expect(one.decision?.quorumProgress?.[0]).toMatchObject({ approvers: ["alice"], met: false });
|
|
62
|
+
expect(logs.some((l) => /the ones at base hold/.test(l))).toBe(true);
|
|
63
|
+
// Approvals made while the wave waits count, each person once.
|
|
64
|
+
const two = memoryGateLedgerPort({ resolutions: [approval("alice", "2026-10-22T02:30:00Z"), approval("alice", "2026-10-22T02:45:00Z"), approval("bob", "2026-10-22T03:00:00Z")], pending: gates.appended });
|
|
65
|
+
const done = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: two, now: "2026-10-22T04:00:00Z" });
|
|
66
|
+
expect(done.exitCode).toBe(0);
|
|
67
|
+
expect(done.decision?.approvedBy).toBe("alice, bob");
|
|
68
|
+
expect(h.applies()).toBe(2);
|
|
69
|
+
h.done();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a claimed role never counts; a role from trust.json at base counts for a reviewer of the merged pull request", async () => {
|
|
73
|
+
const atBase: OpWavesSpec = { ...spec, waves: [{ ...spec.waves[0]!, approval: "pr-review" }], approvals: { quorum: [{ count: 1, roles: ["platform"], notAuthor: true }] } };
|
|
74
|
+
const h = harness(atBase);
|
|
75
|
+
const claimed = memoryGateLedgerPort({ resolutions: [{ ...approval("github:carol", "2026-10-22T01:00:00Z"), approver: { kind: "human", roles: ["platform"] } }] });
|
|
76
|
+
const merged: MergedReview = { pr: 7, head: "h".repeat(40), author: "github:dana", approvers: ["github:alice"], refused: [] };
|
|
77
|
+
const headPlans = async () => ({ version: 1 as const, name: "migrations", head: merged.head, timestamp: "2026-10-22T00:00:00Z", waves: [{ wave: 1, name: "prod", digest: set }] });
|
|
78
|
+
const base = { spec: atBase, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, head: "m".repeat(40), headPlans, now: "2026-10-22T02:00:00Z" };
|
|
79
|
+
// carol claims the role and is not proven; nobody reviewed.
|
|
80
|
+
const waiting = await runOpWave({ ...base, gates: claimed, reviews: reviews({ ...merged, approvers: [] }), trust: trust({ platform: ["github:alice", "github:carol"] }) });
|
|
81
|
+
expect(waiting.exitCode).toBe(3);
|
|
82
|
+
// alice reviewed and holds the role at base; dana wrote the change.
|
|
83
|
+
const gates = memoryGateLedgerPort();
|
|
84
|
+
const passed = await runOpWave({ ...base, gates, reviews: reviews(merged), trust: trust({ platform: ["github:alice"] }) });
|
|
85
|
+
expect(passed.exitCode).toBe(0);
|
|
86
|
+
expect(passed.decision).toMatchObject({ via: "pr-review", approvedBy: "github:alice" });
|
|
87
|
+
expect(gates.resolved.map((r) => r.resolvedBy)).toEqual(["github:alice"]);
|
|
88
|
+
// The author reviewing their own change never meets notAuthor.
|
|
89
|
+
const own = await runOpWave({ ...base, gates: memoryGateLedgerPort(), reviews: reviews({ ...merged, author: "github:alice" }), trust: trust({ platform: ["github:alice"] }) });
|
|
90
|
+
expect(own.exitCode).toBe(3);
|
|
91
|
+
h.done();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("rules at base that cannot be read fail the wave", async () => {
|
|
95
|
+
const h = harness({ ...spec, approvals: { quorum: [{ count: 0 }] } });
|
|
96
|
+
const result = await runOpWave({ spec, specFile: "waves.json", wave: 1, cwd: h.cwd, exec: h.exec, show: h.show, gates: memoryGateLedgerPort() });
|
|
97
|
+
expect(result.exitCode).toBe(1);
|
|
98
|
+
expect(h.applies()).toBe(0);
|
|
99
|
+
h.done();
|
|
100
|
+
});
|
|
101
|
+
});
|