gsd-pi 2.31.2-dev.2453512 → 2.31.2-dev.91f95cf
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.
|
@@ -833,9 +833,6 @@ export async function handleAgentEnd(
|
|
|
833
833
|
// permanently stalled with no unit running and no watchdog set.
|
|
834
834
|
if (s.pendingAgentEndRetry) {
|
|
835
835
|
s.pendingAgentEndRetry = false;
|
|
836
|
-
// Clear gap watchdog from the previous cycle to prevent concurrent
|
|
837
|
-
// dispatch when the deferred handleAgentEnd calls dispatchNextUnit (#1272).
|
|
838
|
-
clearDispatchGapWatchdog();
|
|
839
836
|
setImmediate(() => {
|
|
840
837
|
handleAgentEnd(ctx, pi).catch((err) => {
|
|
841
838
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -978,12 +975,8 @@ async function dispatchNextUnit(
|
|
|
978
975
|
return;
|
|
979
976
|
}
|
|
980
977
|
|
|
981
|
-
// Reentrancy guard
|
|
982
|
-
|
|
983
|
-
// Previously the guard was bypassed when skipDepth > 0, but the recursive
|
|
984
|
-
// skip chain's inner finally block resets s.dispatching = false before the
|
|
985
|
-
// outer call's finally runs, opening a window for concurrent entry.
|
|
986
|
-
if (s.dispatching) {
|
|
978
|
+
// Reentrancy guard
|
|
979
|
+
if (s.dispatching && s.skipDepth === 0) {
|
|
987
980
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
988
981
|
return;
|
|
989
982
|
}
|
|
@@ -1455,12 +1448,8 @@ async function dispatchNextUnit(
|
|
|
1455
1448
|
}
|
|
1456
1449
|
|
|
1457
1450
|
if (dispatchResult.action !== "dispatch") {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1461
|
-
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1462
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1463
|
-
}));
|
|
1451
|
+
await new Promise(r => setImmediate(r));
|
|
1452
|
+
await dispatchNextUnit(ctx, pi);
|
|
1464
1453
|
return;
|
|
1465
1454
|
}
|
|
1466
1455
|
|
|
@@ -1478,10 +1467,8 @@ async function dispatchNextUnit(
|
|
|
1478
1467
|
}
|
|
1479
1468
|
if (preDispatchResult.action === "skip") {
|
|
1480
1469
|
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1484
|
-
}));
|
|
1470
|
+
await new Promise(r => setImmediate(r));
|
|
1471
|
+
await dispatchNextUnit(ctx, pi);
|
|
1485
1472
|
return;
|
|
1486
1473
|
}
|
|
1487
1474
|
if (preDispatchResult.action === "replace") {
|
|
@@ -1512,16 +1499,9 @@ async function dispatchNextUnit(
|
|
|
1512
1499
|
if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
|
|
1513
1500
|
if (!s.active) return;
|
|
1514
1501
|
s.skipDepth++;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
dispatchNextUnit(ctx, pi).catch(err => {
|
|
1519
|
-
ctx.ui.notify(`Deferred skip-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1520
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1521
|
-
}).finally(() => {
|
|
1522
|
-
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1523
|
-
});
|
|
1524
|
-
}, skipDelay);
|
|
1502
|
+
await new Promise(r => setTimeout(r, idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150));
|
|
1503
|
+
await dispatchNextUnit(ctx, pi);
|
|
1504
|
+
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1525
1505
|
return;
|
|
1526
1506
|
}
|
|
1527
1507
|
} else if (idempotencyResult.action === "stop") {
|
|
@@ -1552,11 +1532,8 @@ async function dispatchNextUnit(
|
|
|
1552
1532
|
return;
|
|
1553
1533
|
}
|
|
1554
1534
|
if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
ctx.ui.notify(`Deferred recovery-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1558
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1559
|
-
}));
|
|
1535
|
+
await new Promise(r => setImmediate(r));
|
|
1536
|
+
await dispatchNextUnit(ctx, pi);
|
|
1560
1537
|
return;
|
|
1561
1538
|
}
|
|
1562
1539
|
|
|
@@ -1802,15 +1779,6 @@ export {
|
|
|
1802
1779
|
export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
|
|
1803
1780
|
export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
|
|
1804
1781
|
|
|
1805
|
-
/**
|
|
1806
|
-
* Test-only: expose dispatching / skipDepth state for reentrancy guard tests.
|
|
1807
|
-
* Not part of the public API.
|
|
1808
|
-
*/
|
|
1809
|
-
export function _getDispatching(): boolean { return s.dispatching; }
|
|
1810
|
-
export function _setDispatching(v: boolean): void { s.dispatching = v; }
|
|
1811
|
-
export function _getSkipDepth(): number { return s.skipDepth; }
|
|
1812
|
-
export function _setSkipDepth(v: number): void { s.skipDepth = v; }
|
|
1813
|
-
|
|
1814
1782
|
/**
|
|
1815
1783
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
1816
1784
|
* Used for manual hook triggers via /gsd run-hook.
|
package/package.json
CHANGED
|
@@ -833,9 +833,6 @@ export async function handleAgentEnd(
|
|
|
833
833
|
// permanently stalled with no unit running and no watchdog set.
|
|
834
834
|
if (s.pendingAgentEndRetry) {
|
|
835
835
|
s.pendingAgentEndRetry = false;
|
|
836
|
-
// Clear gap watchdog from the previous cycle to prevent concurrent
|
|
837
|
-
// dispatch when the deferred handleAgentEnd calls dispatchNextUnit (#1272).
|
|
838
|
-
clearDispatchGapWatchdog();
|
|
839
836
|
setImmediate(() => {
|
|
840
837
|
handleAgentEnd(ctx, pi).catch((err) => {
|
|
841
838
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -978,12 +975,8 @@ async function dispatchNextUnit(
|
|
|
978
975
|
return;
|
|
979
976
|
}
|
|
980
977
|
|
|
981
|
-
// Reentrancy guard
|
|
982
|
-
|
|
983
|
-
// Previously the guard was bypassed when skipDepth > 0, but the recursive
|
|
984
|
-
// skip chain's inner finally block resets s.dispatching = false before the
|
|
985
|
-
// outer call's finally runs, opening a window for concurrent entry.
|
|
986
|
-
if (s.dispatching) {
|
|
978
|
+
// Reentrancy guard
|
|
979
|
+
if (s.dispatching && s.skipDepth === 0) {
|
|
987
980
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
988
981
|
return;
|
|
989
982
|
}
|
|
@@ -1455,12 +1448,8 @@ async function dispatchNextUnit(
|
|
|
1455
1448
|
}
|
|
1456
1449
|
|
|
1457
1450
|
if (dispatchResult.action !== "dispatch") {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1461
|
-
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1462
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1463
|
-
}));
|
|
1451
|
+
await new Promise(r => setImmediate(r));
|
|
1452
|
+
await dispatchNextUnit(ctx, pi);
|
|
1464
1453
|
return;
|
|
1465
1454
|
}
|
|
1466
1455
|
|
|
@@ -1478,10 +1467,8 @@ async function dispatchNextUnit(
|
|
|
1478
1467
|
}
|
|
1479
1468
|
if (preDispatchResult.action === "skip") {
|
|
1480
1469
|
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1484
|
-
}));
|
|
1470
|
+
await new Promise(r => setImmediate(r));
|
|
1471
|
+
await dispatchNextUnit(ctx, pi);
|
|
1485
1472
|
return;
|
|
1486
1473
|
}
|
|
1487
1474
|
if (preDispatchResult.action === "replace") {
|
|
@@ -1512,16 +1499,9 @@ async function dispatchNextUnit(
|
|
|
1512
1499
|
if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
|
|
1513
1500
|
if (!s.active) return;
|
|
1514
1501
|
s.skipDepth++;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
dispatchNextUnit(ctx, pi).catch(err => {
|
|
1519
|
-
ctx.ui.notify(`Deferred skip-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1520
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1521
|
-
}).finally(() => {
|
|
1522
|
-
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1523
|
-
});
|
|
1524
|
-
}, skipDelay);
|
|
1502
|
+
await new Promise(r => setTimeout(r, idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150));
|
|
1503
|
+
await dispatchNextUnit(ctx, pi);
|
|
1504
|
+
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1525
1505
|
return;
|
|
1526
1506
|
}
|
|
1527
1507
|
} else if (idempotencyResult.action === "stop") {
|
|
@@ -1552,11 +1532,8 @@ async function dispatchNextUnit(
|
|
|
1552
1532
|
return;
|
|
1553
1533
|
}
|
|
1554
1534
|
if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
ctx.ui.notify(`Deferred recovery-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1558
|
-
pauseAuto(ctx, pi).catch(() => {});
|
|
1559
|
-
}));
|
|
1535
|
+
await new Promise(r => setImmediate(r));
|
|
1536
|
+
await dispatchNextUnit(ctx, pi);
|
|
1560
1537
|
return;
|
|
1561
1538
|
}
|
|
1562
1539
|
|
|
@@ -1802,15 +1779,6 @@ export {
|
|
|
1802
1779
|
export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
|
|
1803
1780
|
export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
|
|
1804
1781
|
|
|
1805
|
-
/**
|
|
1806
|
-
* Test-only: expose dispatching / skipDepth state for reentrancy guard tests.
|
|
1807
|
-
* Not part of the public API.
|
|
1808
|
-
*/
|
|
1809
|
-
export function _getDispatching(): boolean { return s.dispatching; }
|
|
1810
|
-
export function _setDispatching(v: boolean): void { s.dispatching = v; }
|
|
1811
|
-
export function _getSkipDepth(): number { return s.skipDepth; }
|
|
1812
|
-
export function _setSkipDepth(v: number): void { s.skipDepth = v; }
|
|
1813
|
-
|
|
1814
1782
|
/**
|
|
1815
1783
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
1816
1784
|
* Used for manual hook triggers via /gsd run-hook.
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auto-reentrancy-guard.test.ts — Tests for the unconditional reentrancy guard.
|
|
3
|
-
*
|
|
4
|
-
* Regression for #1272: auto-mode stuck-loop where gap watchdog or
|
|
5
|
-
* pendingAgentEndRetry could enter dispatchNextUnit concurrently during
|
|
6
|
-
* recursive skip chains because the reentrancy guard was bypassed when
|
|
7
|
-
* skipDepth > 0.
|
|
8
|
-
*
|
|
9
|
-
* The fix makes the guard unconditional (`if (s.dispatching)` without
|
|
10
|
-
* `&& s.skipDepth === 0`), and defers recursive re-dispatch via
|
|
11
|
-
* setImmediate/setTimeout so s.dispatching is released first.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
_getDispatching,
|
|
16
|
-
_setDispatching,
|
|
17
|
-
_getSkipDepth,
|
|
18
|
-
_setSkipDepth,
|
|
19
|
-
} from "../auto.ts";
|
|
20
|
-
import { createTestContext } from "./test-helpers.ts";
|
|
21
|
-
|
|
22
|
-
const { assertEq, assertTrue, report } = createTestContext();
|
|
23
|
-
|
|
24
|
-
async function main(): Promise<void> {
|
|
25
|
-
// ─── Test-only accessors work ───────────────────────────────────────────
|
|
26
|
-
console.log("\n=== reentrancy guard: test accessors round-trip ===");
|
|
27
|
-
{
|
|
28
|
-
_setDispatching(false);
|
|
29
|
-
assertEq(_getDispatching(), false, "dispatching starts false");
|
|
30
|
-
|
|
31
|
-
_setDispatching(true);
|
|
32
|
-
assertEq(_getDispatching(), true, "dispatching set to true");
|
|
33
|
-
|
|
34
|
-
_setDispatching(false);
|
|
35
|
-
assertEq(_getDispatching(), false, "dispatching reset to false");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ─── skipDepth accessors ────────────────────────────────────────────────
|
|
39
|
-
console.log("\n=== reentrancy guard: skipDepth accessors round-trip ===");
|
|
40
|
-
{
|
|
41
|
-
_setSkipDepth(0);
|
|
42
|
-
assertEq(_getSkipDepth(), 0, "skipDepth starts at 0");
|
|
43
|
-
|
|
44
|
-
_setSkipDepth(3);
|
|
45
|
-
assertEq(_getSkipDepth(), 3, "skipDepth set to 3");
|
|
46
|
-
|
|
47
|
-
_setSkipDepth(0);
|
|
48
|
-
assertEq(_getSkipDepth(), 0, "skipDepth reset to 0");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Guard blocks even when skipDepth > 0 (#1272 regression) ───────────
|
|
52
|
-
console.log("\n=== reentrancy guard: blocks when dispatching=true regardless of skipDepth ===");
|
|
53
|
-
{
|
|
54
|
-
// Simulate the scenario from #1272: dispatching=true + skipDepth>0
|
|
55
|
-
// The old guard (`if (s.dispatching && s.skipDepth === 0)`) would allow
|
|
56
|
-
// concurrent entry when skipDepth > 0. The fix makes the check
|
|
57
|
-
// unconditional on skipDepth.
|
|
58
|
-
_setDispatching(true);
|
|
59
|
-
_setSkipDepth(2);
|
|
60
|
-
|
|
61
|
-
// Verify dispatching is true — guard should block regardless of skipDepth
|
|
62
|
-
assertTrue(
|
|
63
|
-
_getDispatching() === true,
|
|
64
|
-
"dispatching flag is true during skip chain"
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// The actual reentrancy guard in dispatchNextUnit checks:
|
|
68
|
-
// if (s.dispatching) { return; }
|
|
69
|
-
// We verify the state that would trigger the guard:
|
|
70
|
-
const wouldBlock = _getDispatching(); // unconditional check
|
|
71
|
-
const wouldBlockOld = _getDispatching() && _getSkipDepth() === 0; // old check
|
|
72
|
-
|
|
73
|
-
assertTrue(wouldBlock === true, "new guard blocks when dispatching=true, skipDepth=2");
|
|
74
|
-
assertTrue(wouldBlockOld === false, "old guard WOULD NOT block when dispatching=true, skipDepth=2 (the bug)");
|
|
75
|
-
|
|
76
|
-
// Clean up
|
|
77
|
-
_setDispatching(false);
|
|
78
|
-
_setSkipDepth(0);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Guard allows entry when dispatching=false ──────────────────────────
|
|
82
|
-
console.log("\n=== reentrancy guard: allows entry when dispatching=false ===");
|
|
83
|
-
{
|
|
84
|
-
_setDispatching(false);
|
|
85
|
-
_setSkipDepth(0);
|
|
86
|
-
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=0");
|
|
87
|
-
|
|
88
|
-
_setDispatching(false);
|
|
89
|
-
_setSkipDepth(3);
|
|
90
|
-
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=3");
|
|
91
|
-
|
|
92
|
-
_setSkipDepth(0);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── skipDepth does not affect guard decision (the fix) ─────────────────
|
|
96
|
-
console.log("\n=== reentrancy guard: skipDepth is irrelevant to guard decision ===");
|
|
97
|
-
{
|
|
98
|
-
for (const depth of [0, 1, 2, 5]) {
|
|
99
|
-
_setDispatching(true);
|
|
100
|
-
_setSkipDepth(depth);
|
|
101
|
-
assertTrue(
|
|
102
|
-
_getDispatching() === true,
|
|
103
|
-
`guard blocks at skipDepth=${depth} when dispatching=true`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const depth of [0, 1, 2, 5]) {
|
|
108
|
-
_setDispatching(false);
|
|
109
|
-
_setSkipDepth(depth);
|
|
110
|
-
assertTrue(
|
|
111
|
-
_getDispatching() === false,
|
|
112
|
-
`guard allows at skipDepth=${depth} when dispatching=false`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Clean up
|
|
117
|
-
_setDispatching(false);
|
|
118
|
-
_setSkipDepth(0);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
report();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
main().catch((err) => {
|
|
125
|
-
console.error(err);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auto-reentrancy-guard.test.ts — Tests for the unconditional reentrancy guard.
|
|
3
|
-
*
|
|
4
|
-
* Regression for #1272: auto-mode stuck-loop where gap watchdog or
|
|
5
|
-
* pendingAgentEndRetry could enter dispatchNextUnit concurrently during
|
|
6
|
-
* recursive skip chains because the reentrancy guard was bypassed when
|
|
7
|
-
* skipDepth > 0.
|
|
8
|
-
*
|
|
9
|
-
* The fix makes the guard unconditional (`if (s.dispatching)` without
|
|
10
|
-
* `&& s.skipDepth === 0`), and defers recursive re-dispatch via
|
|
11
|
-
* setImmediate/setTimeout so s.dispatching is released first.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
_getDispatching,
|
|
16
|
-
_setDispatching,
|
|
17
|
-
_getSkipDepth,
|
|
18
|
-
_setSkipDepth,
|
|
19
|
-
} from "../auto.ts";
|
|
20
|
-
import { createTestContext } from "./test-helpers.ts";
|
|
21
|
-
|
|
22
|
-
const { assertEq, assertTrue, report } = createTestContext();
|
|
23
|
-
|
|
24
|
-
async function main(): Promise<void> {
|
|
25
|
-
// ─── Test-only accessors work ───────────────────────────────────────────
|
|
26
|
-
console.log("\n=== reentrancy guard: test accessors round-trip ===");
|
|
27
|
-
{
|
|
28
|
-
_setDispatching(false);
|
|
29
|
-
assertEq(_getDispatching(), false, "dispatching starts false");
|
|
30
|
-
|
|
31
|
-
_setDispatching(true);
|
|
32
|
-
assertEq(_getDispatching(), true, "dispatching set to true");
|
|
33
|
-
|
|
34
|
-
_setDispatching(false);
|
|
35
|
-
assertEq(_getDispatching(), false, "dispatching reset to false");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ─── skipDepth accessors ────────────────────────────────────────────────
|
|
39
|
-
console.log("\n=== reentrancy guard: skipDepth accessors round-trip ===");
|
|
40
|
-
{
|
|
41
|
-
_setSkipDepth(0);
|
|
42
|
-
assertEq(_getSkipDepth(), 0, "skipDepth starts at 0");
|
|
43
|
-
|
|
44
|
-
_setSkipDepth(3);
|
|
45
|
-
assertEq(_getSkipDepth(), 3, "skipDepth set to 3");
|
|
46
|
-
|
|
47
|
-
_setSkipDepth(0);
|
|
48
|
-
assertEq(_getSkipDepth(), 0, "skipDepth reset to 0");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Guard blocks even when skipDepth > 0 (#1272 regression) ───────────
|
|
52
|
-
console.log("\n=== reentrancy guard: blocks when dispatching=true regardless of skipDepth ===");
|
|
53
|
-
{
|
|
54
|
-
// Simulate the scenario from #1272: dispatching=true + skipDepth>0
|
|
55
|
-
// The old guard (`if (s.dispatching && s.skipDepth === 0)`) would allow
|
|
56
|
-
// concurrent entry when skipDepth > 0. The fix makes the check
|
|
57
|
-
// unconditional on skipDepth.
|
|
58
|
-
_setDispatching(true);
|
|
59
|
-
_setSkipDepth(2);
|
|
60
|
-
|
|
61
|
-
// Verify dispatching is true — guard should block regardless of skipDepth
|
|
62
|
-
assertTrue(
|
|
63
|
-
_getDispatching() === true,
|
|
64
|
-
"dispatching flag is true during skip chain"
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// The actual reentrancy guard in dispatchNextUnit checks:
|
|
68
|
-
// if (s.dispatching) { return; }
|
|
69
|
-
// We verify the state that would trigger the guard:
|
|
70
|
-
const wouldBlock = _getDispatching(); // unconditional check
|
|
71
|
-
const wouldBlockOld = _getDispatching() && _getSkipDepth() === 0; // old check
|
|
72
|
-
|
|
73
|
-
assertTrue(wouldBlock === true, "new guard blocks when dispatching=true, skipDepth=2");
|
|
74
|
-
assertTrue(wouldBlockOld === false, "old guard WOULD NOT block when dispatching=true, skipDepth=2 (the bug)");
|
|
75
|
-
|
|
76
|
-
// Clean up
|
|
77
|
-
_setDispatching(false);
|
|
78
|
-
_setSkipDepth(0);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Guard allows entry when dispatching=false ──────────────────────────
|
|
82
|
-
console.log("\n=== reentrancy guard: allows entry when dispatching=false ===");
|
|
83
|
-
{
|
|
84
|
-
_setDispatching(false);
|
|
85
|
-
_setSkipDepth(0);
|
|
86
|
-
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=0");
|
|
87
|
-
|
|
88
|
-
_setDispatching(false);
|
|
89
|
-
_setSkipDepth(3);
|
|
90
|
-
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=3");
|
|
91
|
-
|
|
92
|
-
_setSkipDepth(0);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── skipDepth does not affect guard decision (the fix) ─────────────────
|
|
96
|
-
console.log("\n=== reentrancy guard: skipDepth is irrelevant to guard decision ===");
|
|
97
|
-
{
|
|
98
|
-
for (const depth of [0, 1, 2, 5]) {
|
|
99
|
-
_setDispatching(true);
|
|
100
|
-
_setSkipDepth(depth);
|
|
101
|
-
assertTrue(
|
|
102
|
-
_getDispatching() === true,
|
|
103
|
-
`guard blocks at skipDepth=${depth} when dispatching=true`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const depth of [0, 1, 2, 5]) {
|
|
108
|
-
_setDispatching(false);
|
|
109
|
-
_setSkipDepth(depth);
|
|
110
|
-
assertTrue(
|
|
111
|
-
_getDispatching() === false,
|
|
112
|
-
`guard allows at skipDepth=${depth} when dispatching=false`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Clean up
|
|
117
|
-
_setDispatching(false);
|
|
118
|
-
_setSkipDepth(0);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
report();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
main().catch((err) => {
|
|
125
|
-
console.error(err);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
});
|