Skip to content

Commit 74213f4

Browse files
Add flush-queue consistency test (#247) + scope-throw behavioral probe (#246) (#12)
#247 (conformance): After an effect throws during flush, writing to an unrelated signal must not trigger effects that were skipped from the halted flush. Tests that the flush queue state is properly reset after an error, per the scenario in alien-signals#97. #246 (behavioral): Records whether each framework disposes child effects when a run/scope body throws before returning. This is a design choice (not a correctness requirement) — most frameworks keep children alive.
1 parent 571acdf commit 74213f4

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

src/behaviorDifferences.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,35 @@ export const cases: Record<string, (fw: ReactiveFramework) => any> = {
458458
return runs <= 1 ? "batched" : `unbatched (${runs} runs)`;
459459
},
460460

461+
/**
462+
* run{ E(child ─→ S(source)); throw }
463+
*
464+
* A scope/root body creates a child effect then throws before
465+
* returning. Checks whether the framework disposes child
466+
* effects created before the throw or leaves them alive.
467+
* Returns "disposes children" or "children survive".
468+
*/
469+
"#246 throwing run body child effect cleanup"(fw: ReactiveFramework) {
470+
const source = fw.signal(0);
471+
let childRuns = 0;
472+
473+
try {
474+
fw.run(() => {
475+
fw.effect(() => {
476+
childRuns++;
477+
source.read();
478+
});
479+
throw new Error("scope setup failed");
480+
});
481+
} catch {}
482+
483+
childRuns = 0;
484+
try {
485+
source.write(1);
486+
} catch {}
487+
return childRuns === 0 ? "disposes children" : "children survive";
488+
},
489+
461490
/**
462491
* E_outer{ E_inner1, E_inner2, E_inner3 } → dispose
463492
*

src/errorHandling.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,52 @@ export const cases: Record<string, (fw: ReactiveFramework) => any> = {
321321
expect(c.read()).toBe(4);
322322
},
323323

324+
/**
325+
* S(a) ← E(e1)
326+
* S(a) ← E(e2 ⚡ throws when a===1)
327+
* S(a) ← E(e3)
328+
* S(b) ← E(e4)
329+
*
330+
* e2 throws during propagation of a(1), which may halt the
331+
* flush. On a subsequent write to unrelated signal b, only
332+
* b-dependent effects must run — a-dependent effects skipped
333+
* by the earlier halt must NOT leak into the new flush.
334+
*/
335+
"#247 flush queue consistent after effect throw"(
336+
fw: ReactiveFramework
337+
) {
338+
const a = fw.signal(0);
339+
const b = fw.signal(0);
340+
let e3Runs = 0;
341+
342+
fw.effect(() => {
343+
a.read();
344+
});
345+
try {
346+
fw.effect(() => {
347+
if (a.read() === 1) throw new Error("e2 error");
348+
});
349+
} catch {}
350+
fw.effect(() => {
351+
a.read();
352+
e3Runs++;
353+
});
354+
fw.effect(() => {
355+
b.read();
356+
});
357+
358+
e3Runs = 0;
359+
try {
360+
a.write(1);
361+
} catch {}
362+
e3Runs = 0;
363+
364+
try {
365+
b.write(1);
366+
} catch {}
367+
expect(e3Runs).toBe(0);
368+
},
369+
324370
/**
325371
* S(a) → C(b) ⚡ throws when a is true
326372
*

0 commit comments

Comments
 (0)