@nanobpm/nano-workforce 0.128.0 → 0.129.1
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/.github/workflows/pr-title-lint.yml +5 -3
- package/.releaserc.json +36 -2
- package/AGENTS.md +10 -6
- package/CHANGELOG.md +13 -0
- package/app/abandon.test.ts +16 -2
- package/app/abandon.ts +39 -17
- package/app/conformance.test.ts +2 -1
- package/app/conformance.ts +9 -3
- package/app/featureDelivery.test.ts +2 -1
- package/app/instanceTracking.ts +97 -0
- package/app/lineage.test.ts +2 -1
- package/app/lineage.ts +15 -2
- package/app/mergeEscalationUserTask.test.ts +21 -55
- package/app/mergeLoopBehaviour.test.ts +446 -0
- package/app/promotionPoll.test.ts +2 -1
- package/app/retro.test.ts +2 -1
- package/app/retro.ts +9 -2
- package/app/service.test.ts +15 -14
- package/app/service.ts +17 -24
- package/e2e/convergence-loop.e2e.ts +41 -8
- package/operations/acknowledgeEpic.test.ts +2 -1
- package/operations/checkAbandon.test.ts +2 -1
- package/operations/getLineage.test.ts +2 -1
- package/package.json +4 -3
- package/resources/processes/merge-loop.bpmn +692 -395
- package/test/trackingViews.ts +50 -0
- package/test/worldDb.ts +2 -1
- package/workers/retro-gather/worker.test.ts +2 -1
- package/app/mergeCiReattempt.test.ts +0 -138
- package/app/mergeEscalationQuestion.test.ts +0 -190
- package/app/mergeRebaseArm.test.ts +0 -140
- package/app/mergeRetryArm.test.ts +0 -100
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Structural regression guard for the merge-loop transient-retry arm (issue #334).
|
|
2
|
-
//
|
|
3
|
-
// #334: a transient, GitHub-flagged-retryable merge race ("Base branch was modified. Review and
|
|
4
|
-
// try the merge again.") was mapped to `blocked` → a decision-required human escalation on a PR
|
|
5
|
-
// that was actually mergeable once the base settled. The fix adds a `retry` merge outcome that
|
|
6
|
-
// re-enters the merge loop on the settled base through a *bounded* budget gate — mirroring the
|
|
7
|
-
// `gw-ci-fix` within-budget / budget-exhausted pattern — WITHOUT any remediation agent, and
|
|
8
|
-
// escalates via the existing `merge-esc-attempt` only when the retry budget is exhausted (so a
|
|
9
|
-
// continuously-moving base still escalates promptly rather than spinning forever).
|
|
10
|
-
//
|
|
11
|
-
// This test asserts the arm's topology on the committed model so it cannot regress silently. It is
|
|
12
|
-
// a pure text assertion over the BPMN (no engine), matching the repo's lightweight model-guard style.
|
|
13
|
-
|
|
14
|
-
import { test } from "node:test";
|
|
15
|
-
import { assert, assertStringIncludes } from "#test-assert";
|
|
16
|
-
import { readFileSync } from "node:fs";
|
|
17
|
-
|
|
18
|
-
const bpmn = readFileSync("resources/processes/merge-loop.bpmn", "utf8");
|
|
19
|
-
const flat = bpmn.replace(/\s+/g, " ");
|
|
20
|
-
|
|
21
|
-
function hasFlow(source: string, target: string): boolean {
|
|
22
|
-
const re = new RegExp(
|
|
23
|
-
`<bpmn:sequenceFlow\\b[^>]*\\bsourceRef="${source}"[^>]*\\btargetRef="${target}"|` +
|
|
24
|
-
`<bpmn:sequenceFlow\\b[^>]*\\btargetRef="${target}"[^>]*\\bsourceRef="${source}"`,
|
|
25
|
-
);
|
|
26
|
-
return re.test(flat);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function flowHasId(id: string, source: string, target: string): boolean {
|
|
30
|
-
const m = flat.match(new RegExp(`<bpmn:sequenceFlow\\b[^>]*\\bid="${id}"[^>]*/?>`));
|
|
31
|
-
if (!m) return false;
|
|
32
|
-
const tag = m[0];
|
|
33
|
-
return tag.includes(`sourceRef="${source}"`) && tag.includes(`targetRef="${target}"`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function gatewayDefault(id: string, def: string): boolean {
|
|
37
|
-
const m = flat.match(new RegExp(`<bpmn:exclusiveGateway\\b[^>]*\\bid="${id}"[^>]*>`));
|
|
38
|
-
if (!m) return false;
|
|
39
|
-
return m[0].includes(`default="${def}"`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Assert the conditionExpression *of a specific sequenceFlow* contains `needle`, so the guard
|
|
43
|
-
// cannot be satisfied by the same substring appearing on an unrelated flow (e.g. the
|
|
44
|
-
// merge-esc-attempt question FEEL also mentions `mergeStatus = "retry"`).
|
|
45
|
-
function flowHasCondition(id: string, needle: string): boolean {
|
|
46
|
-
const m = flat.match(
|
|
47
|
-
new RegExp(`<bpmn:sequenceFlow\\b[^>]*\\bid="${id}"[^>]*>(.*?)</bpmn:sequenceFlow>`),
|
|
48
|
-
);
|
|
49
|
-
if (!m) return false;
|
|
50
|
-
const cond = m[1].match(/<bpmn:conditionExpression\b[^>]*>(.*?)<\/bpmn:conditionExpression>/);
|
|
51
|
-
return cond ? cond[1].includes(needle) : false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
test("gw-merge routes the retry outcome to a dedicated budget gate, not to a human", () => {
|
|
55
|
-
// A `retry` merge result must reach the retry-budget gate…
|
|
56
|
-
assert(hasFlow("gw-merge", "gw-merge-retry"), "gw-merge → gw-merge-retry (retry) missing");
|
|
57
|
-
// …guarded by the exact retry condition (mergeStatus = "retry").
|
|
58
|
-
assert(flowHasId("f_m_gRetry", "gw-merge", "gw-merge-retry"), "f_m_gRetry must be gw-merge → gw-merge-retry");
|
|
59
|
-
// Assert the retry condition on f_m_gRetry ITSELF — not merely anywhere in the model — so the
|
|
60
|
-
// guard cannot be satisfied by the identical substring in the merge-esc-attempt question FEEL.
|
|
61
|
-
assert(
|
|
62
|
-
flowHasCondition("f_m_gRetry", 'mergeStatus = "retry"'),
|
|
63
|
-
'f_m_gRetry conditionExpression must be mergeStatus = "retry"',
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("retry arm mirrors the fix-ci arm: budget gate → re-arm, exhausted → escalate", () => {
|
|
68
|
-
// Within budget → re-arm the merge poller (re-attempt on the settled base). No remediation agent.
|
|
69
|
-
assert(hasFlow("gw-merge-retry", "arm-merge"), "gw-merge-retry → arm-merge (within budget) missing");
|
|
70
|
-
assert(flowHasId("f_mr_go", "gw-merge-retry", "arm-merge"), "f_mr_go must be gw-merge-retry → arm-merge");
|
|
71
|
-
assertStringIncludes(flat, "mergeRetryRound <= mergeRetryMax");
|
|
72
|
-
|
|
73
|
-
// Budget exhausted → the EXISTING human escalation (merge-esc-attempt), and it is the gateway default
|
|
74
|
-
// so a continuously-moving base can never spin past the cap.
|
|
75
|
-
assert(
|
|
76
|
-
hasFlow("gw-merge-retry", "merge-esc-attempt"),
|
|
77
|
-
"gw-merge-retry → merge-esc-attempt (budget exhausted) missing",
|
|
78
|
-
);
|
|
79
|
-
assert(gatewayDefault("gw-merge-retry", "f_mr_giveup"), "gw-merge-retry must default to f_mr_giveup");
|
|
80
|
-
assert(
|
|
81
|
-
flowHasId("f_mr_giveup", "gw-merge-retry", "merge-esc-attempt"),
|
|
82
|
-
"f_mr_giveup must default gw-merge-retry → merge-esc-attempt",
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("the retry arm advances the attempt counter only on a transient retry outcome", () => {
|
|
87
|
-
// The counter advances ONLY when the merge attempt returned `retry` — mirroring how fix-ci/rebase
|
|
88
|
-
// advance their own rounds only on their own remediation — so unrelated merge attempts (initial,
|
|
89
|
-
// post-rebase, post-fix-ci, post-evict) can't consume the transient-retry budget. N consecutive
|
|
90
|
-
// transient races then trip the `mergeRetryRound <= mergeRetryMax` gate at exactly the cap.
|
|
91
|
-
assertStringIncludes(flat, "=if mergeStatus = "retry" then mergeRetryRound + 1 else mergeRetryRound");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("the retry arm has NO remediation agent (contrast the conflict/rebase arm)", () => {
|
|
95
|
-
// The base-moved race needs only a re-attempt on the settled base — no rebase/CI-fix agent. The
|
|
96
|
-
// within-budget flow goes straight back to arm-merge, never through a `senior:*` task.
|
|
97
|
-
assert(flowHasId("f_mr_go", "gw-merge-retry", "arm-merge"), "retry within-budget must go directly to arm-merge");
|
|
98
|
-
// Sanity: the untouched blocked path still escalates directly (a genuine refusal is unchanged).
|
|
99
|
-
assert(flowHasId("f_m_gBlocked", "gw-merge", "merge-esc-attempt"), "blocked → merge-esc-attempt must remain");
|
|
100
|
-
});
|