wstp-node 0.6.2 → 0.6.3
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/build/Release/wstp.node +0 -0
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/src/addon.cc +48 -10
- package/test.js +113 -24
package/build/Release/wstp.node
CHANGED
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -429,3 +429,12 @@ export class WstpReader {
|
|
|
429
429
|
* or `null` / `undefined` to clear a previously-set handler.
|
|
430
430
|
*/
|
|
431
431
|
export function setDiagHandler(fn: ((msg: string) => void) | null | undefined): void;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Addon version string — mirrors the `version` field in `package.json`.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* const { version } = require('./build/Release/wstp.node');
|
|
438
|
+
* console.log(version); // "0.6.2"
|
|
439
|
+
*/
|
|
440
|
+
export declare const version: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wstp-node",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Native Node.js addon for Wolfram/Mathematica WSTP — kernel sessions with evaluation queue, streaming Print/messages, Dialog subsessions, and side-channel WstpReader",
|
|
5
5
|
"main": "build/Release/wstp.node",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/addon.cc
CHANGED
|
@@ -882,13 +882,18 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
882
882
|
// Drain any stale packets (e.g. late BEGINDLGPKT from a concurrent
|
|
883
883
|
// interrupt that arrived just as this cell's RETURNPKT was sent).
|
|
884
884
|
// This prevents Pattern D: stale BEGINDLGPKT corrupting next eval.
|
|
885
|
-
|
|
886
|
-
// In interactive mode the kernel
|
|
887
|
-
// trailing INPUTNAMEPKT (
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
//
|
|
885
|
+
//
|
|
886
|
+
// IMPORTANT: In interactive mode (EnterExpressionPacket), the kernel
|
|
887
|
+
// follows RETURNEXPRPKT with a trailing INPUTNAMEPKT ("In[n+1]:=").
|
|
888
|
+
// drainStalePackets must NOT run here because it would consume that
|
|
889
|
+
// INPUTNAMEPKT and discard it, causing the outer loop to block forever
|
|
890
|
+
// waiting for a packet that was already eaten. Drain is deferred to
|
|
891
|
+
// the INPUTNAMEPKT handler below.
|
|
892
|
+
if (!opts || !opts->interactive) {
|
|
893
|
+
if (!r.aborted) drainStalePackets(lp, opts);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
// Interactive mode: fall through to consume trailing INPUTNAMEPKT.
|
|
892
897
|
}
|
|
893
898
|
else if (pkt == INPUTNAMEPKT) {
|
|
894
899
|
const char* s = nullptr; WSGetString(lp, &s);
|
|
@@ -906,7 +911,9 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
906
911
|
break;
|
|
907
912
|
}
|
|
908
913
|
// Trailing INPUTNAMEPKT after RETURNEXPRPKT — consume and exit.
|
|
909
|
-
//
|
|
914
|
+
// Now safe to drain stale packets (Pattern D prevention) —
|
|
915
|
+
// the trailing INPUTNAMEPKT has already been consumed above.
|
|
916
|
+
if (!r.aborted) drainStalePackets(lp, opts);
|
|
910
917
|
break;
|
|
911
918
|
}
|
|
912
919
|
r.cellIndex = parseCellIndex(nameStr);
|
|
@@ -1351,9 +1358,23 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
1351
1358
|
// Legacy dialog path: when dynAutoMode is false and the eval has
|
|
1352
1359
|
// dialog callbacks, respond 'i' (inspect) so the kernel enters
|
|
1353
1360
|
// inspect mode and the Internal`AddHandler fires Dialog[].
|
|
1354
|
-
//
|
|
1361
|
+
// For type-1 (interrupt menu) without dialog callbacks, respond
|
|
1362
|
+
// 'a' (abort) rather than 'c' (continue). On ARM64/Wolfram 3,
|
|
1363
|
+
// 'c' can leave the kernel in a stuck state when the interrupt
|
|
1364
|
+
// handler (Internal`AddHandler["Interrupt", Function[.., Dialog[]]])
|
|
1365
|
+
// is installed — Dialog[] interferes with the continuation and
|
|
1366
|
+
// the kernel never sends RETURNPKT. Aborting cleanly produces
|
|
1367
|
+
// $Aborted and keeps the session alive.
|
|
1368
|
+
// For non-interrupt menus (type != 1), 'c' is still safe.
|
|
1355
1369
|
bool wantInspect = opts && !opts->dynAutoMode && opts->hasOnDialogBegin;
|
|
1356
|
-
const char* resp
|
|
1370
|
+
const char* resp;
|
|
1371
|
+
if (wantInspect) {
|
|
1372
|
+
resp = "i";
|
|
1373
|
+
} else if (menuType_ == 1) {
|
|
1374
|
+
resp = "a";
|
|
1375
|
+
} else {
|
|
1376
|
+
resp = "c";
|
|
1377
|
+
}
|
|
1357
1378
|
DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding '" + resp + "'");
|
|
1358
1379
|
WSPutString(lp, resp);
|
|
1359
1380
|
WSEndPacket(lp);
|
|
@@ -1467,6 +1488,21 @@ public:
|
|
|
1467
1488
|
|
|
1468
1489
|
// ---- thread-pool thread: send packet; block until response ----
|
|
1469
1490
|
void Execute() override {
|
|
1491
|
+
// ---- Pre-eval drain: consume stale packets on the link --------
|
|
1492
|
+
// If an interrupt was sent just as the previous eval completed,
|
|
1493
|
+
// the kernel may have opened a Dialog[] (via the interrupt handler)
|
|
1494
|
+
// while idle. The resulting BEGINDLGPKT sits unread on the link.
|
|
1495
|
+
// Without draining it first, our EvaluatePacket is processed inside
|
|
1496
|
+
// the stale Dialog context and its RETURNPKT is consumed during the
|
|
1497
|
+
// BEGINDLGPKT handler's drain — leaving the outer DrainToEvalResult
|
|
1498
|
+
// loop waiting forever for a RETURNPKT that was already eaten.
|
|
1499
|
+
// Only check if data is already buffered (WSReady) to avoid adding
|
|
1500
|
+
// latency in the normal case (no stale packets).
|
|
1501
|
+
if (WSReady(lp_)) {
|
|
1502
|
+
DiagLog("[Eval] pre-eval: stale data on link — draining");
|
|
1503
|
+
drainStalePackets(lp_, nullptr);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1470
1506
|
// ---- Phase 2: ScheduledTask[Dialog[], interval] management ----
|
|
1471
1507
|
// Install a kernel-side ScheduledTask that calls Dialog[] periodically.
|
|
1472
1508
|
// Only install when:
|
|
@@ -3083,6 +3119,8 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
|
|
|
3083
3119
|
WstpReader::Init(env, exports);
|
|
3084
3120
|
exports.Set("setDiagHandler",
|
|
3085
3121
|
Napi::Function::New(env, SetDiagHandler, "setDiagHandler"));
|
|
3122
|
+
// version — mirrors package.json "version"; read-only string constant.
|
|
3123
|
+
exports.Set("version", Napi::String::New(env, "0.6.3"));
|
|
3086
3124
|
return exports;
|
|
3087
3125
|
}
|
|
3088
3126
|
|
package/test.js
CHANGED
|
@@ -731,14 +731,14 @@ async function main() {
|
|
|
731
731
|
// 2500ms because Pause[] ignores WSInterruptMessage during a sleep.
|
|
732
732
|
// This test documents the fundamental limitation: Dynamic cannot read a
|
|
733
733
|
// live variable while Pause[N] is running.
|
|
734
|
-
await run('P1: Pause[
|
|
734
|
+
await run('P1: Pause[4] ignores interrupt within 1500ms', async () => {
|
|
735
735
|
const s = mkSession();
|
|
736
736
|
try {
|
|
737
737
|
await installHandler(s);
|
|
738
738
|
|
|
739
739
|
let evalDone = false;
|
|
740
740
|
const mainProm = s.evaluate(
|
|
741
|
-
'pP1 = 0; Pause[
|
|
741
|
+
'pP1 = 0; Pause[4]; pP1 = 1; "p1-done"'
|
|
742
742
|
).then(() => { evalDone = true; });
|
|
743
743
|
|
|
744
744
|
await sleep(300);
|
|
@@ -747,12 +747,12 @@ async function main() {
|
|
|
747
747
|
assert(sent === true, 'interrupt() should return true mid-eval');
|
|
748
748
|
|
|
749
749
|
const t0 = Date.now();
|
|
750
|
-
while (!s.isDialogOpen && Date.now() - t0 <
|
|
750
|
+
while (!s.isDialogOpen && Date.now() - t0 < 1500) await sleep(25);
|
|
751
751
|
|
|
752
752
|
const dlgOpened = s.isDialogOpen;
|
|
753
|
-
assert(!dlgOpened, 'Pause[
|
|
753
|
+
assert(!dlgOpened, 'Pause[4] should NOT open a dialog within 1500ms');
|
|
754
754
|
|
|
755
|
-
// After the
|
|
755
|
+
// After the 1500ms window the interrupt may still be queued — the kernel
|
|
756
756
|
// will fire Dialog[] once Pause[] releases. Abort to unstick the eval
|
|
757
757
|
// rather than waiting for it to return on its own (which could take forever
|
|
758
758
|
// if Dialog[] opens and nobody services it).
|
|
@@ -761,7 +761,7 @@ async function main() {
|
|
|
761
761
|
} finally {
|
|
762
762
|
s.close();
|
|
763
763
|
}
|
|
764
|
-
},
|
|
764
|
+
}, 12_000);
|
|
765
765
|
|
|
766
766
|
// ── P2: Pause[0.3] loop — interrupt opens Dialog and dialogEval works ──
|
|
767
767
|
// Expected: interrupt during a short Pause[] loop opens a Dialog[],
|
|
@@ -773,7 +773,7 @@ async function main() {
|
|
|
773
773
|
|
|
774
774
|
let evalDone = false;
|
|
775
775
|
const mainProm = s.evaluate(
|
|
776
|
-
'Do[nP2 = k; Pause[0.
|
|
776
|
+
'Do[nP2 = k; Pause[0.1], {k, 1, 15}]; "p2-done"',
|
|
777
777
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
778
778
|
).then(() => { evalDone = true; });
|
|
779
779
|
|
|
@@ -781,19 +781,19 @@ async function main() {
|
|
|
781
781
|
|
|
782
782
|
s.interrupt();
|
|
783
783
|
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
784
|
-
catch (_) { throw new Error('Dialog never opened — interrupt not working with Pause[0.
|
|
784
|
+
catch (_) { throw new Error('Dialog never opened — interrupt not working with Pause[0.1]'); }
|
|
785
785
|
|
|
786
786
|
const val = await withTimeout(s.dialogEval('nP2'), 5000, 'dialogEval nP2');
|
|
787
787
|
assert(val && typeof val.value === 'number' && val.value >= 1,
|
|
788
788
|
`expected nP2 >= 1, got ${JSON.stringify(val)}`);
|
|
789
789
|
|
|
790
790
|
await s.exitDialog();
|
|
791
|
-
await withTimeout(mainProm,
|
|
791
|
+
await withTimeout(mainProm, 8_000, 'P2 main eval');
|
|
792
792
|
} finally {
|
|
793
793
|
try { s.abort(); } catch (_) {}
|
|
794
794
|
s.close();
|
|
795
795
|
}
|
|
796
|
-
},
|
|
796
|
+
}, 15_000);
|
|
797
797
|
|
|
798
798
|
// ── P3: dialogEval timeout — kernel state after (diagnostic) ───────────
|
|
799
799
|
// Simulates the extension failure: dialogEval times out without exitDialog.
|
|
@@ -806,7 +806,7 @@ async function main() {
|
|
|
806
806
|
|
|
807
807
|
let evalDone = false;
|
|
808
808
|
const mainProm = s.evaluate(
|
|
809
|
-
'Do[nP3 = k; Pause[0.
|
|
809
|
+
'Do[nP3 = k; Pause[0.1], {k, 1, 15}]; "p3-done"',
|
|
810
810
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
811
811
|
).then(() => { evalDone = true; });
|
|
812
812
|
|
|
@@ -841,7 +841,7 @@ async function main() {
|
|
|
841
841
|
await s.exitDialog().catch(() => {});
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
if (!evalDone) { try { await withTimeout(mainProm,
|
|
844
|
+
if (!evalDone) { try { await withTimeout(mainProm, 6_000, 'P3 loop'); } catch (_) {} }
|
|
845
845
|
|
|
846
846
|
// Diagnostic: document observed outcome but do not hard-fail on dlg2
|
|
847
847
|
if (!exitOk) {
|
|
@@ -853,7 +853,7 @@ async function main() {
|
|
|
853
853
|
try { s.abort(); } catch (_) {}
|
|
854
854
|
s.close();
|
|
855
855
|
}
|
|
856
|
-
},
|
|
856
|
+
}, 20_000);
|
|
857
857
|
|
|
858
858
|
// ── P4: abort() after stuck dialog — session stays alive ────────────────
|
|
859
859
|
// Note: abort() sends WSAbortMessage which resets the Wolfram kernel's
|
|
@@ -867,7 +867,7 @@ async function main() {
|
|
|
867
867
|
await installHandler(s);
|
|
868
868
|
|
|
869
869
|
const mainProm = s.evaluate(
|
|
870
|
-
'Do[nP4=k; Pause[0.
|
|
870
|
+
'Do[nP4=k; Pause[0.1], {k,1,15}]; "p4-done"',
|
|
871
871
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
872
872
|
).catch(() => {});
|
|
873
873
|
|
|
@@ -899,7 +899,7 @@ async function main() {
|
|
|
899
899
|
try { s.abort(); } catch (_) {}
|
|
900
900
|
s.close();
|
|
901
901
|
}
|
|
902
|
-
},
|
|
902
|
+
}, 15_000);
|
|
903
903
|
|
|
904
904
|
// ── P5: closeAllDialogs()+abort() recovery → reinstall handler → new dialog works
|
|
905
905
|
// closeAllDialogs() is designed to be paired with abort(). It rejects all
|
|
@@ -912,7 +912,7 @@ async function main() {
|
|
|
912
912
|
await installHandler(s);
|
|
913
913
|
|
|
914
914
|
const mainProm = s.evaluate(
|
|
915
|
-
'Do[nP5 = k; Pause[0.
|
|
915
|
+
'Do[nP5 = k; Pause[0.1], {k, 1, 15}]; "p5-done"',
|
|
916
916
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
917
917
|
).catch(() => {});
|
|
918
918
|
|
|
@@ -938,7 +938,7 @@ async function main() {
|
|
|
938
938
|
|
|
939
939
|
let dlg2 = false;
|
|
940
940
|
const mainProm2 = s.evaluate(
|
|
941
|
-
'Do[nP5b = k; Pause[0.
|
|
941
|
+
'Do[nP5b = k; Pause[0.1], {k, 1, 15}]; "p5b-done"',
|
|
942
942
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
943
943
|
).catch(() => {});
|
|
944
944
|
|
|
@@ -960,7 +960,7 @@ async function main() {
|
|
|
960
960
|
try { s.abort(); } catch (_) {}
|
|
961
961
|
s.close();
|
|
962
962
|
}
|
|
963
|
-
},
|
|
963
|
+
}, 25_000);
|
|
964
964
|
|
|
965
965
|
// ── P6: Simulate Dynamic + Pause[5] full scenario (diagnostic) ─────────
|
|
966
966
|
// Reproduces: n=RandomInteger[100]; Pause[5] with interrupt every 2.5s.
|
|
@@ -973,18 +973,18 @@ async function main() {
|
|
|
973
973
|
|
|
974
974
|
let evalDone = false;
|
|
975
975
|
const mainProm = s.evaluate(
|
|
976
|
-
'n = RandomInteger[100]; Pause[
|
|
976
|
+
'n = RandomInteger[100]; Pause[3]; "p6-done"',
|
|
977
977
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
978
978
|
).then(() => { evalDone = true; });
|
|
979
979
|
|
|
980
980
|
await sleep(200);
|
|
981
981
|
|
|
982
|
-
const INTERRUPT_WAIT_MS =
|
|
982
|
+
const INTERRUPT_WAIT_MS = 1500;
|
|
983
983
|
const DIALOG_EVAL_TIMEOUT_MS = 8000;
|
|
984
984
|
let dialogReadSucceeded = false;
|
|
985
985
|
let pauseIgnoredInterrupt = false;
|
|
986
986
|
|
|
987
|
-
for (let cycle = 1; cycle <=
|
|
987
|
+
for (let cycle = 1; cycle <= 2 && !evalDone; cycle++) {
|
|
988
988
|
const t0 = Date.now();
|
|
989
989
|
s.interrupt();
|
|
990
990
|
while (!s.isDialogOpen && Date.now() - t0 < INTERRUPT_WAIT_MS) await sleep(25);
|
|
@@ -1012,11 +1012,11 @@ async function main() {
|
|
|
1012
1012
|
}
|
|
1013
1013
|
}
|
|
1014
1014
|
|
|
1015
|
-
try { await withTimeout(mainProm,
|
|
1015
|
+
try { await withTimeout(mainProm, 7_000, 'P6 main eval'); } catch (_) {}
|
|
1016
1016
|
|
|
1017
1017
|
// Diagnostic: always passes — log observed outcome
|
|
1018
1018
|
if (pauseIgnoredInterrupt && !dialogReadSucceeded) {
|
|
1019
|
-
console.log(' P6 note: Pause[
|
|
1019
|
+
console.log(' P6 note: Pause[3] blocked all interrupts — expected behaviour');
|
|
1020
1020
|
} else if (dialogReadSucceeded) {
|
|
1021
1021
|
console.log(' P6 note: at least one read succeeded despite Pause');
|
|
1022
1022
|
}
|
|
@@ -1024,7 +1024,7 @@ async function main() {
|
|
|
1024
1024
|
try { s.abort(); } catch (_) {}
|
|
1025
1025
|
s.close();
|
|
1026
1026
|
}
|
|
1027
|
-
},
|
|
1027
|
+
}, 20_000);
|
|
1028
1028
|
|
|
1029
1029
|
// ── 25. abort() while dialog is open ──────────────────────────────────
|
|
1030
1030
|
// Must run AFTER all other dialog tests — abort() sends WSAbortMessage
|
|
@@ -1542,6 +1542,94 @@ async function main() {
|
|
|
1542
1542
|
}
|
|
1543
1543
|
}, 30000);
|
|
1544
1544
|
|
|
1545
|
+
// ── 55. Interactive mode with non-Null result (no trailing semicolon) ──
|
|
1546
|
+
// In interactive mode (EnterExpressionPacket), a non-Null result produces:
|
|
1547
|
+
// RETURNEXPRPKT → INPUTNAMEPKT.
|
|
1548
|
+
// Bug: drainStalePackets() consumed the trailing INPUTNAMEPKT, causing the
|
|
1549
|
+
// outer DrainToEvalResult loop to hang forever waiting for a packet already
|
|
1550
|
+
// consumed. This test verifies that interactive evals with visible results
|
|
1551
|
+
// (no trailing semicolon) complete without hanging.
|
|
1552
|
+
await run('55. interactive eval with non-Null result (drainStalePackets fix)', async () => {
|
|
1553
|
+
const s = mkSession();
|
|
1554
|
+
try {
|
|
1555
|
+
// Simple assignment without semicolon — returns non-Null.
|
|
1556
|
+
const r1 = await withTimeout(
|
|
1557
|
+
s.evaluate('n = 1', { interactive: true }),
|
|
1558
|
+
10000, '55 n=1 interactive — would hang without drainStalePackets fix'
|
|
1559
|
+
);
|
|
1560
|
+
assert(!r1.aborted, 'n=1 must not be aborted');
|
|
1561
|
+
assert(r1.result.value === 1, `n=1 expected 1, got ${r1.result.value}`);
|
|
1562
|
+
|
|
1563
|
+
// Follow-up: another non-Null interactive eval.
|
|
1564
|
+
const r2 = await withTimeout(
|
|
1565
|
+
s.evaluate('n + 41', { interactive: true }),
|
|
1566
|
+
10000, '55 n+41 interactive'
|
|
1567
|
+
);
|
|
1568
|
+
assert(!r2.aborted, 'n+41 must not be aborted');
|
|
1569
|
+
assert(r2.result.value === 42, `n+41 expected 42, got ${r2.result.value}`);
|
|
1570
|
+
|
|
1571
|
+
// Follow-up with semicolon (Null result) — should also work.
|
|
1572
|
+
const r3 = await withTimeout(
|
|
1573
|
+
s.evaluate('m = 99;', { interactive: true }),
|
|
1574
|
+
10000, '55 m=99; interactive'
|
|
1575
|
+
);
|
|
1576
|
+
assert(!r3.aborted, 'm=99; must not be aborted');
|
|
1577
|
+
|
|
1578
|
+
// Follow-up: non-interactive eval still works after interactive ones.
|
|
1579
|
+
const r4 = await withTimeout(
|
|
1580
|
+
s.evaluate('m + 1'),
|
|
1581
|
+
10000, '55 m+1 non-interactive follow-up'
|
|
1582
|
+
);
|
|
1583
|
+
assert(r4.result.value === 100, `m+1 expected 100, got ${r4.result.value}`);
|
|
1584
|
+
} finally {
|
|
1585
|
+
s.close();
|
|
1586
|
+
}
|
|
1587
|
+
}, 45000);
|
|
1588
|
+
|
|
1589
|
+
// ── 56. Stale interrupt aborts eval cleanly (no hang) ──────────────────
|
|
1590
|
+
// When live-watch sends an interrupt just as a cell completes, the kernel
|
|
1591
|
+
// may fire the queued interrupt during the NEXT evaluation. Without
|
|
1592
|
+
// dialog callbacks, the C++ MENUPKT handler responds 'a' (abort) so the
|
|
1593
|
+
// eval returns $Aborted rather than hanging. This is safe: the caller
|
|
1594
|
+
// can retry. A follow-up eval (without stale interrupt) must succeed.
|
|
1595
|
+
await run('56. stale interrupt aborts eval — no hang', async () => {
|
|
1596
|
+
const s = mkSession();
|
|
1597
|
+
try {
|
|
1598
|
+
await installHandler(s);
|
|
1599
|
+
|
|
1600
|
+
// Warm up
|
|
1601
|
+
const r0 = await withTimeout(s.evaluate('1 + 1'), 5000, '56 warmup');
|
|
1602
|
+
assert(r0.result.value === 2);
|
|
1603
|
+
|
|
1604
|
+
// Send interrupt to idle kernel — may fire during next eval
|
|
1605
|
+
s.interrupt();
|
|
1606
|
+
await sleep(500); // give kernel time to queue interrupt
|
|
1607
|
+
|
|
1608
|
+
// Evaluate — stale interrupt fires → MENUPKT → 'a' → $Aborted
|
|
1609
|
+
const r1 = await withTimeout(
|
|
1610
|
+
s.evaluate('42'),
|
|
1611
|
+
10000, '56 eval — would hang without abort-on-MENUPKT fix'
|
|
1612
|
+
);
|
|
1613
|
+
// The eval may return $Aborted (interrupt fired) or 42 (interrupt
|
|
1614
|
+
// was ignored by idle kernel). Either is acceptable — the critical
|
|
1615
|
+
// thing is that it does NOT hang.
|
|
1616
|
+
if (r1.aborted) {
|
|
1617
|
+
assert(r1.result.value === '$Aborted',
|
|
1618
|
+
`expected $Aborted, got ${JSON.stringify(r1.result)}`);
|
|
1619
|
+
} else {
|
|
1620
|
+
assert(r1.result.value === 42,
|
|
1621
|
+
`expected 42, got ${JSON.stringify(r1.result)}`);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Follow-up eval must work regardless
|
|
1625
|
+
const r2 = await withTimeout(s.evaluate('2 + 2'), 5000, '56 follow-up');
|
|
1626
|
+
assert(!r2.aborted, '56 follow-up should not be aborted');
|
|
1627
|
+
assert(r2.result.value === 4);
|
|
1628
|
+
} finally {
|
|
1629
|
+
s.close();
|
|
1630
|
+
}
|
|
1631
|
+
}, 30000);
|
|
1632
|
+
|
|
1545
1633
|
// ── Teardown ──────────────────────────────────────────────────────────
|
|
1546
1634
|
session.close();
|
|
1547
1635
|
assert(!session.isOpen, 'main session not closed');
|
|
@@ -1555,6 +1643,7 @@ async function main() {
|
|
|
1555
1643
|
process.exit(0);
|
|
1556
1644
|
} else {
|
|
1557
1645
|
console.log(`${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ''}.`);
|
|
1646
|
+
process.exit(1);
|
|
1558
1647
|
}
|
|
1559
1648
|
}
|
|
1560
1649
|
|