wstp-node 0.4.5 → 0.6.0
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/README.md +32 -2
- package/build/Release/wstp.node +0 -0
- package/index.d.ts +134 -0
- package/package.json +1 -1
- package/scripts/diagnose-windows.ps1 +4 -2
- package/scripts/wstp_dir.js +72 -50
- package/src/addon.cc +906 -172
- package/test.js +492 -30
package/test.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// ── Test suite for wstp-backend
|
|
3
|
+
// ── Test suite for wstp-backend v0.6.0 ────────────────────────────────────
|
|
4
4
|
// Covers: evaluation queue, streaming callbacks, sub() priority, abort
|
|
5
5
|
// behaviour, WstpReader side-channel, edge cases, Dialog[] subsession
|
|
6
|
-
// (dialogEval, exitDialog, isDialogOpen, onDialogBegin/End/Print)
|
|
7
|
-
//
|
|
6
|
+
// (dialogEval, exitDialog, isDialogOpen, onDialogBegin/End/Print),
|
|
7
|
+
// subWhenIdle() (background queue, timeout, close rejection), kernelPid,
|
|
8
|
+
// Dynamic eval API (registerDynamic, getDynamicResults, setDynamicInterval,
|
|
9
|
+
// setDynAutoMode, dynamicActive, rejectDialog, abort deduplication).
|
|
8
10
|
|
|
9
11
|
const { WstpSession, WstpReader, setDiagHandler } = require('./build/Release/wstp.node');
|
|
10
12
|
|
|
@@ -70,17 +72,47 @@ const TEST_TIMEOUT_MS = 30_000;
|
|
|
70
72
|
// Hard suite-level watchdog: if the entire suite takes longer than this the
|
|
71
73
|
// process is force-killed. Covers cases where a test hangs AND the per-test
|
|
72
74
|
// timeout itself is somehow bypassed (e.g. a blocked native thread).
|
|
73
|
-
const SUITE_TIMEOUT_MS =
|
|
75
|
+
const SUITE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
74
76
|
const suiteWatchdog = setTimeout(() => {
|
|
75
77
|
console.error('\nFATAL: suite watchdog expired — process hung, force-exiting.');
|
|
76
78
|
process.exit(2);
|
|
77
79
|
}, SUITE_TIMEOUT_MS);
|
|
78
80
|
suiteWatchdog.unref(); // does not prevent normal exit
|
|
79
81
|
|
|
82
|
+
// ── Test filtering ─────────────────────────────────────────────────────────
|
|
83
|
+
// Usage: node test.js --only 38,39,40 or --only 38-52
|
|
84
|
+
// Omit flag to run all tests.
|
|
85
|
+
const ONLY_TESTS = (() => {
|
|
86
|
+
const idx = process.argv.indexOf('--only');
|
|
87
|
+
if (idx === -1) return null;
|
|
88
|
+
const spec = process.argv[idx + 1] || '';
|
|
89
|
+
const nums = new Set();
|
|
90
|
+
for (const part of spec.split(',')) {
|
|
91
|
+
const range = part.split('-');
|
|
92
|
+
if (range.length === 2) {
|
|
93
|
+
for (let i = parseInt(range[0]); i <= parseInt(range[1]); i++) nums.add(i);
|
|
94
|
+
} else {
|
|
95
|
+
nums.add(parseInt(part));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return nums;
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
// Extract leading integer from a test name like "38. foo bar" → 38.
|
|
102
|
+
function testNum(name) {
|
|
103
|
+
const m = name.match(/^(\d+)/);
|
|
104
|
+
return m ? parseInt(m[1]) : NaN;
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
let passed = 0;
|
|
81
108
|
let failed = 0;
|
|
109
|
+
let skipped = 0;
|
|
82
110
|
|
|
83
111
|
async function run(name, fn, timeoutMs = TEST_TIMEOUT_MS) {
|
|
112
|
+
if (ONLY_TESTS !== null && !ONLY_TESTS.has(testNum(name))) {
|
|
113
|
+
skipped++;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
84
116
|
// Race the test body against a per-test timeout.
|
|
85
117
|
const timeout = new Promise((_, reject) =>
|
|
86
118
|
setTimeout(() => reject(new Error(`TIMED OUT after ${timeoutMs} ms`)),
|
|
@@ -779,35 +811,40 @@ async function main() {
|
|
|
779
811
|
await sleep(500);
|
|
780
812
|
|
|
781
813
|
s.interrupt();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// Simulate a timed-out dialogEval (abandon it at 200ms)
|
|
786
|
-
try { await withTimeout(s.dialogEval('nP3'), 200, 'deliberate-timeout'); } catch (_) {}
|
|
787
|
-
|
|
788
|
-
// Attempt exitDialog — should succeed
|
|
789
|
-
let exitOk = false;
|
|
790
|
-
try { await withTimeout(s.exitDialog(), 2000, 'exitDialog after timeout'); exitOk = true; }
|
|
814
|
+
let dlg1 = false;
|
|
815
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); dlg1 = true; }
|
|
791
816
|
catch (_) {}
|
|
792
817
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
while (!s.isDialogOpen && Date.now() - t2 < 3000) await sleep(30);
|
|
799
|
-
dlg2 = s.isDialogOpen;
|
|
818
|
+
if (!dlg1) {
|
|
819
|
+
console.log(' P3 note: Dialog #1 never opened — interrupt may have been slow');
|
|
820
|
+
} else {
|
|
821
|
+
// Simulate a timed-out dialogEval (abandon it at 200ms)
|
|
822
|
+
try { await withTimeout(s.dialogEval('nP3'), 200, 'deliberate-timeout'); } catch (_) {}
|
|
800
823
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
await s.exitDialog()
|
|
804
|
-
|
|
824
|
+
// Attempt exitDialog — should succeed
|
|
825
|
+
let exitOk = false;
|
|
826
|
+
try { await withTimeout(s.exitDialog(), 2000, 'exitDialog after timeout'); exitOk = true; }
|
|
827
|
+
catch (_) {}
|
|
828
|
+
|
|
829
|
+
// Attempt a second interrupt to confirm kernel state
|
|
830
|
+
await sleep(400);
|
|
831
|
+
s.interrupt();
|
|
832
|
+
let dlg2 = false;
|
|
833
|
+
const t2 = Date.now();
|
|
834
|
+
while (!s.isDialogOpen && Date.now() - t2 < 3000) await sleep(30);
|
|
835
|
+
dlg2 = s.isDialogOpen;
|
|
836
|
+
|
|
837
|
+
if (dlg2) {
|
|
838
|
+
await withTimeout(s.dialogEval('nP3'), 4000, 'dialogEval #2').catch(() => {});
|
|
839
|
+
await s.exitDialog().catch(() => {});
|
|
840
|
+
}
|
|
805
841
|
|
|
806
|
-
|
|
842
|
+
if (!evalDone) { try { await withTimeout(mainProm, 20_000, 'P3 loop'); } catch (_) {} }
|
|
807
843
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
844
|
+
// Diagnostic: document observed outcome but do not hard-fail on dlg2
|
|
845
|
+
if (!exitOk) {
|
|
846
|
+
console.log(` P3 note: exitDialog failed, dlg2=${dlg2} (expected=false for unfixed build)`);
|
|
847
|
+
}
|
|
811
848
|
}
|
|
812
849
|
// Test passes unconditionally.
|
|
813
850
|
} finally {
|
|
@@ -1013,19 +1050,444 @@ async function main() {
|
|
|
1013
1050
|
assert(typeof ok === 'boolean', `interrupt() should return boolean, got ${typeof ok}`);
|
|
1014
1051
|
});
|
|
1015
1052
|
|
|
1053
|
+
// ── 30. kernelPid is a positive integer ───────────────────────────────
|
|
1054
|
+
await run('30. kernelPid is a positive integer', async () => {
|
|
1055
|
+
const s = mkSession();
|
|
1056
|
+
try {
|
|
1057
|
+
const pid = s.kernelPid;
|
|
1058
|
+
assert(typeof pid === 'number', `kernelPid type: ${typeof pid}`);
|
|
1059
|
+
assert(Number.isInteger(pid), `kernelPid not integer: ${pid}`);
|
|
1060
|
+
assert(pid > 0, `kernelPid not positive: ${pid}`);
|
|
1061
|
+
// Verify it matches $ProcessID reported by the kernel itself.
|
|
1062
|
+
const r = await s.sub('$ProcessID');
|
|
1063
|
+
assert(r.type === 'integer' && r.value === pid,
|
|
1064
|
+
`kernelPid ${pid} vs kernel $ProcessID ${JSON.stringify(r)}`);
|
|
1065
|
+
} finally {
|
|
1066
|
+
s.close();
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// ── 31. kernelPid is distinct for main + subsessions ──────────────────
|
|
1071
|
+
await run('31. kernelPid distinct across subsessions', async () => {
|
|
1072
|
+
const s = mkSession();
|
|
1073
|
+
try {
|
|
1074
|
+
const child1 = s.createSubsession();
|
|
1075
|
+
const child2 = s.createSubsession();
|
|
1076
|
+
try {
|
|
1077
|
+
const pids = [s.kernelPid, child1.kernelPid, child2.kernelPid];
|
|
1078
|
+
assert(pids.every(p => p > 0),
|
|
1079
|
+
`all PIDs must be positive: ${JSON.stringify(pids)}`);
|
|
1080
|
+
assert(new Set(pids).size === 3,
|
|
1081
|
+
`PIDs must be distinct: ${JSON.stringify(pids)}`);
|
|
1082
|
+
} finally {
|
|
1083
|
+
child1.close();
|
|
1084
|
+
child2.close();
|
|
1085
|
+
}
|
|
1086
|
+
} finally {
|
|
1087
|
+
s.close();
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// ── 32. kernelPid survives close() ────────────────────────────────────
|
|
1092
|
+
await run('32. kernelPid readable after close()', async () => {
|
|
1093
|
+
const s = mkSession();
|
|
1094
|
+
const pid = s.kernelPid;
|
|
1095
|
+
assert(pid > 0, `pid before close: ${pid}`);
|
|
1096
|
+
s.close();
|
|
1097
|
+
assert(s.kernelPid === pid,
|
|
1098
|
+
`kernelPid changed after close: ${s.kernelPid} vs ${pid}`);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// ── 33. subWhenIdle runs after all evaluate() calls ───────────────────
|
|
1102
|
+
await run('33. subWhenIdle runs after evaluate() queue drains', async () => {
|
|
1103
|
+
const s = mkSession();
|
|
1104
|
+
try {
|
|
1105
|
+
const order = [];
|
|
1106
|
+
const p1 = s.evaluate('Pause[0.3]; 1').then(() => order.push('eval1'));
|
|
1107
|
+
const p2 = s.evaluate('Pause[0.1]; 2').then(() => order.push('eval2'));
|
|
1108
|
+
const p3 = s.subWhenIdle('3').then(() => order.push('whenIdle'));
|
|
1109
|
+
await Promise.all([p1, p2, p3]);
|
|
1110
|
+
assert(order[0] === 'eval1', `order[0]: ${order[0]}`);
|
|
1111
|
+
assert(order[1] === 'eval2', `order[1]: ${order[1]}`);
|
|
1112
|
+
assert(order[2] === 'whenIdle', `order[2]: ${order[2]}`);
|
|
1113
|
+
} finally {
|
|
1114
|
+
s.close();
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// ── 34. subWhenIdle resolves correctly when idle ───────────────────────
|
|
1119
|
+
await run('34. subWhenIdle resolves with correct WExpr when idle', async () => {
|
|
1120
|
+
const s = mkSession();
|
|
1121
|
+
try {
|
|
1122
|
+
const r = await s.subWhenIdle('2 + 2');
|
|
1123
|
+
assert(r.type === 'integer' && r.value === 4,
|
|
1124
|
+
`expected {type:"integer",value:4}, got ${JSON.stringify(r)}`);
|
|
1125
|
+
} finally {
|
|
1126
|
+
s.close();
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// ── 35. subWhenIdle timeout rejects while kernel is busy ──────────────
|
|
1131
|
+
await run('35. subWhenIdle timeout rejects', async () => {
|
|
1132
|
+
const s = mkSession();
|
|
1133
|
+
try {
|
|
1134
|
+
s.evaluate('Pause[3]; 0'); // keep kernel busy for 3 s
|
|
1135
|
+
let threw = false;
|
|
1136
|
+
try {
|
|
1137
|
+
await s.subWhenIdle('1', { timeout: 400 });
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
threw = true;
|
|
1140
|
+
assert(e.message === 'subWhenIdle: timeout',
|
|
1141
|
+
`unexpected error: ${e.message}`);
|
|
1142
|
+
}
|
|
1143
|
+
assert(threw, 'subWhenIdle should have rejected with timeout');
|
|
1144
|
+
} finally {
|
|
1145
|
+
s.close();
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// ── 36. subWhenIdle rejects when session is closed while queued ────────
|
|
1150
|
+
await run('36. subWhenIdle rejects on session close', async () => {
|
|
1151
|
+
const s = mkSession();
|
|
1152
|
+
s.evaluate('Pause[5]; 0'); // keep busy
|
|
1153
|
+
const p = s.subWhenIdle('1');
|
|
1154
|
+
let threw = false;
|
|
1155
|
+
try {
|
|
1156
|
+
setTimeout(() => s.close(), 200);
|
|
1157
|
+
await p;
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
threw = true;
|
|
1160
|
+
assert(e.message === 'Session is closed',
|
|
1161
|
+
`unexpected error: ${e.message}`);
|
|
1162
|
+
}
|
|
1163
|
+
assert(threw, 'subWhenIdle should reject when session is closed');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// ── 37. sub() still prioritised over subWhenIdle() ────────────────────
|
|
1167
|
+
await run('37. sub() prioritised over subWhenIdle()', async () => {
|
|
1168
|
+
const s = mkSession();
|
|
1169
|
+
try {
|
|
1170
|
+
const order = [];
|
|
1171
|
+
s.evaluate('Pause[0.4]; 0'); // keep busy
|
|
1172
|
+
const wi = s.subWhenIdle('1').then(() => order.push('whenIdle'));
|
|
1173
|
+
const su = s.sub('2').then(() => order.push('sub'));
|
|
1174
|
+
await Promise.all([wi, su]);
|
|
1175
|
+
assert(order[0] === 'sub', `expected sub first, got: ${order[0]}`);
|
|
1176
|
+
assert(order[1] === 'whenIdle', `expected whenIdle second, got: ${order[1]}`);
|
|
1177
|
+
} finally {
|
|
1178
|
+
s.close();
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1183
|
+
// Dynamic eval API tests (v0.6.0)
|
|
1184
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1185
|
+
|
|
1186
|
+
// ── 38. registerDynamic + getDynamicResults basic ─────────────────────
|
|
1187
|
+
await run('38. registerDynamic / getDynamicResults basic', async () => {
|
|
1188
|
+
const s = mkSession();
|
|
1189
|
+
try {
|
|
1190
|
+
s.registerDynamic('sum', 'ToString[1+1]');
|
|
1191
|
+
s.setDynamicInterval(150);
|
|
1192
|
+
assert(s.dynamicActive, 'dynamicActive should be true after registration + interval');
|
|
1193
|
+
// Run a long-ish eval so the timer fires at least once
|
|
1194
|
+
await withTimeout(s.evaluate('Pause[0.8]; "done"'), 8000, 'test 38 eval');
|
|
1195
|
+
const results = s.getDynamicResults();
|
|
1196
|
+
assert(typeof results === 'object', 'getDynamicResults must return object');
|
|
1197
|
+
assert('sum' in results, 'result must have key "sum"');
|
|
1198
|
+
assert(results.sum.value === '2', `expected "2", got "${results.sum.value}"`);
|
|
1199
|
+
assert(typeof results.sum.timestamp === 'number' && results.sum.timestamp > 0,
|
|
1200
|
+
'timestamp must be positive number');
|
|
1201
|
+
} finally {
|
|
1202
|
+
s.close();
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// ── 39. multiple registered expressions ───────────────────────────────
|
|
1207
|
+
await run('39. multiple Dynamic registrations', async () => {
|
|
1208
|
+
const s = mkSession();
|
|
1209
|
+
try {
|
|
1210
|
+
await s.evaluate('a = 10; b = 20; c = 30;');
|
|
1211
|
+
s.registerDynamic('a', 'ToString[a]');
|
|
1212
|
+
s.registerDynamic('b', 'ToString[b]');
|
|
1213
|
+
s.registerDynamic('c', 'ToString[c]');
|
|
1214
|
+
s.setDynamicInterval(150);
|
|
1215
|
+
await withTimeout(s.evaluate('Pause[0.8]; "done"'), 8000, 'test 39 eval');
|
|
1216
|
+
const res = s.getDynamicResults();
|
|
1217
|
+
assert(res.a && res.a.value === '10', `a: expected "10", got "${res.a && res.a.value}"`);
|
|
1218
|
+
assert(res.b && res.b.value === '20', `b: expected "20", got "${res.b && res.b.value}"`);
|
|
1219
|
+
assert(res.c && res.c.value === '30', `c: expected "30", got "${res.c && res.c.value}"`);
|
|
1220
|
+
} finally {
|
|
1221
|
+
s.close();
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ── 40. getDynamicResults clears on each call ─────────────────────────
|
|
1226
|
+
await run('40. getDynamicResults clears buffer on each call', async () => {
|
|
1227
|
+
const s = mkSession();
|
|
1228
|
+
try {
|
|
1229
|
+
s.registerDynamic('x', 'ToString[2+2]');
|
|
1230
|
+
s.setDynamicInterval(150);
|
|
1231
|
+
await withTimeout(s.evaluate('Pause[0.8]; "done"'), 8000, 'test 40 eval');
|
|
1232
|
+
const first = s.getDynamicResults();
|
|
1233
|
+
const second = s.getDynamicResults();
|
|
1234
|
+
assert('x' in first, 'first call should contain results');
|
|
1235
|
+
assert(Object.keys(second).length === 0, 'second call must return empty object (buffer cleared)');
|
|
1236
|
+
} finally {
|
|
1237
|
+
s.close();
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// ── 41. rejectDialog:true prevents deadlock ───────────────────────────
|
|
1242
|
+
await run('41. rejectDialog:true option', async () => {
|
|
1243
|
+
const s = mkSession();
|
|
1244
|
+
try {
|
|
1245
|
+
// Install a handler so interrupt → Dialog[]
|
|
1246
|
+
await s.evaluate(
|
|
1247
|
+
'Quiet[Internal`AddHandler["Interrupt", Function[{}, Dialog[]]]]',
|
|
1248
|
+
{ rejectDialog: true }
|
|
1249
|
+
);
|
|
1250
|
+
// Evaluate with rejectDialog; any dialog gets silently closed
|
|
1251
|
+
const r = await withTimeout(
|
|
1252
|
+
s.evaluate('Pause[0.3]; "ok"', { rejectDialog: true }),
|
|
1253
|
+
5000, 'test 41 eval'
|
|
1254
|
+
);
|
|
1255
|
+
assert(r.result.value === 'ok', `expected "ok", got "${r.result.value}"`);
|
|
1256
|
+
} finally {
|
|
1257
|
+
s.close();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// ── 42. abort deduplication — multiple abort() calls don't corrupt ────
|
|
1262
|
+
await run('42. abort deduplication', async () => {
|
|
1263
|
+
const s = mkSession();
|
|
1264
|
+
try {
|
|
1265
|
+
const p = s.evaluate('Pause[5]; 42');
|
|
1266
|
+
await sleep(100);
|
|
1267
|
+
// Fire three rapid aborts — only first should take effect
|
|
1268
|
+
s.abort();
|
|
1269
|
+
s.abort();
|
|
1270
|
+
s.abort();
|
|
1271
|
+
const r = await withTimeout(p, 6000, 'test 42 abort settle');
|
|
1272
|
+
assert(r.aborted, 'evaluation should be aborted');
|
|
1273
|
+
// Kernel must still be usable
|
|
1274
|
+
const r2 = await withTimeout(s.evaluate('"alive"'), 5000, 'test 42 post-abort');
|
|
1275
|
+
assert(r2.result.value === 'alive', 'kernel must still work after multi-abort');
|
|
1276
|
+
} finally {
|
|
1277
|
+
s.close();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// ── 43. dynamicActive accessor ────────────────────────────────────────
|
|
1282
|
+
await run('43. dynamicActive accessor reflects state', async () => {
|
|
1283
|
+
const s = mkSession();
|
|
1284
|
+
try {
|
|
1285
|
+
assert(!s.dynamicActive, 'dynamicActive must be false initially');
|
|
1286
|
+
s.registerDynamic('v', 'ToString[1]');
|
|
1287
|
+
assert(!s.dynamicActive, 'dynamicActive must remain false until interval set');
|
|
1288
|
+
s.setDynamicInterval(200);
|
|
1289
|
+
assert(s.dynamicActive, 'dynamicActive must be true after registration + interval');
|
|
1290
|
+
s.clearDynamicRegistry();
|
|
1291
|
+
assert(!s.dynamicActive, 'dynamicActive must be false after clear');
|
|
1292
|
+
} finally {
|
|
1293
|
+
s.close();
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// ── 44. clearDynamicRegistry empties getDynamicResults ────────────────
|
|
1298
|
+
await run('44. clearDynamicRegistry', async () => {
|
|
1299
|
+
const s = mkSession();
|
|
1300
|
+
try {
|
|
1301
|
+
s.registerDynamic('p', 'ToString[Pi, 5]');
|
|
1302
|
+
s.setDynamicInterval(150);
|
|
1303
|
+
await withTimeout(s.evaluate('Pause[0.5]; "done"'), 6000, 'test 44 eval');
|
|
1304
|
+
s.clearDynamicRegistry();
|
|
1305
|
+
// After clearing, a new eval should produce no dyn results
|
|
1306
|
+
await withTimeout(s.evaluate('"x"'), 5000, 'test 44 second eval');
|
|
1307
|
+
const res = s.getDynamicResults();
|
|
1308
|
+
assert(Object.keys(res).length === 0, 'no results expected after clearDynamicRegistry');
|
|
1309
|
+
} finally {
|
|
1310
|
+
s.close();
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// ── 45. unregisterDynamic removes one entry ───────────────────────────
|
|
1315
|
+
await run('45. unregisterDynamic removes one entry', async () => {
|
|
1316
|
+
const s = mkSession();
|
|
1317
|
+
try {
|
|
1318
|
+
s.registerDynamic('keep', 'ToString[3+3]');
|
|
1319
|
+
s.registerDynamic('drop', 'ToString[4+4]');
|
|
1320
|
+
s.unregisterDynamic('drop');
|
|
1321
|
+
s.setDynamicInterval(150);
|
|
1322
|
+
await withTimeout(s.evaluate('Pause[0.8]; "done"'), 8000, 'test 45 eval');
|
|
1323
|
+
const res = s.getDynamicResults();
|
|
1324
|
+
assert('keep' in res, 'key "keep" must still be present');
|
|
1325
|
+
assert(!('drop' in res), 'key "drop" must be absent after unregister');
|
|
1326
|
+
} finally {
|
|
1327
|
+
s.close();
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// ── 46. setDynamicInterval(0) stops timer ────────────────────────────
|
|
1332
|
+
await run('46. setDynamicInterval(0) disables timer', async () => {
|
|
1333
|
+
const s = mkSession();
|
|
1334
|
+
try {
|
|
1335
|
+
s.registerDynamic('z', 'ToString[7+7]');
|
|
1336
|
+
s.setDynamicInterval(100);
|
|
1337
|
+
assert(s.dynamicActive, 'dynamicActive should be true');
|
|
1338
|
+
s.setDynamicInterval(0);
|
|
1339
|
+
assert(!s.dynamicActive, 'dynamicActive must be false after interval set to 0');
|
|
1340
|
+
// Even with a long eval, no dynamic results should accumulate
|
|
1341
|
+
await withTimeout(s.evaluate('Pause[0.5]; "ok"'), 6000, 'test 46 eval');
|
|
1342
|
+
const res = s.getDynamicResults();
|
|
1343
|
+
assert(Object.keys(res).length === 0, 'no Dynamic results expected when timer is disabled');
|
|
1344
|
+
} finally {
|
|
1345
|
+
s.close();
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// ── 47. setDynAutoMode(false) falls back to legacy JS dialog path ─────
|
|
1350
|
+
await run('47. setDynAutoMode(false) — legacy JS dialog path works', async () => {
|
|
1351
|
+
const s = mkSession();
|
|
1352
|
+
try {
|
|
1353
|
+
s.setDynAutoMode(false);
|
|
1354
|
+
let dialogOpened = false;
|
|
1355
|
+
// Install interrupt handler manually (required for legacy path)
|
|
1356
|
+
await installHandler(s);
|
|
1357
|
+
// Use a loop with short Pause so the interrupt fires between iterations
|
|
1358
|
+
// (single Pause[] absorbs interrupts until it completes).
|
|
1359
|
+
const p = s.evaluate('Do[Pause[0.15], {20}]; "done"', {
|
|
1360
|
+
onDialogBegin: async () => {
|
|
1361
|
+
dialogOpened = true;
|
|
1362
|
+
await s.exitDialog();
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
await sleep(400);
|
|
1366
|
+
s.interrupt();
|
|
1367
|
+
const r = await withTimeout(p, 10000, 'test 47 legacy dialog eval');
|
|
1368
|
+
assert(dialogOpened, 'onDialogBegin should have fired in legacy mode');
|
|
1369
|
+
assert(!r.aborted, 'should not be aborted');
|
|
1370
|
+
} finally {
|
|
1371
|
+
s.close();
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
// ── 48. Dynamic: abort followed by new dynAutoMode eval works ─────────
|
|
1376
|
+
await run('48. abort then Dynamic eval recovers', async () => {
|
|
1377
|
+
const s = mkSession();
|
|
1378
|
+
try {
|
|
1379
|
+
s.registerDynamic('n', 'ToString[42]');
|
|
1380
|
+
s.setDynamicInterval(200);
|
|
1381
|
+
// Start a long eval, abort it
|
|
1382
|
+
const p = s.evaluate('Pause[10]; 0');
|
|
1383
|
+
await sleep(150);
|
|
1384
|
+
s.abort();
|
|
1385
|
+
const r = await withTimeout(p, 5000, 'test 48 abort wait');
|
|
1386
|
+
assert(r.aborted, 'first eval should be aborted');
|
|
1387
|
+
// Now a new eval must work normally with dyn interrupts
|
|
1388
|
+
const r2 = await withTimeout(s.evaluate('Pause[0.8]; "alive"'), 8000, 'test 48 second eval');
|
|
1389
|
+
assert(r2.result.value === 'alive', 'kernel alive after abort');
|
|
1390
|
+
// Dynamic results should be populated
|
|
1391
|
+
const res = s.getDynamicResults();
|
|
1392
|
+
assert('n' in res, 'dynamic result "n" expected after abort recovery');
|
|
1393
|
+
assert(res.n.value === '42', `expected "42", got "${res.n.value}"`);
|
|
1394
|
+
} finally {
|
|
1395
|
+
s.close();
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
// ── 49. Rapid cell transitions with Dynamic — no deadlock ─────────────
|
|
1400
|
+
// Tests rapid cell transitions with Dialog[] firing frequently. The C++
|
|
1401
|
+
// layer handles the case where EvaluatePacket gets processed inside a
|
|
1402
|
+
// Dialog[] context (between-eval ScheduledTask fire) by capturing the
|
|
1403
|
+
// outer RETURNPKT and returning it directly.
|
|
1404
|
+
await run('49. rapid cell transitions with Dynamic active', async () => {
|
|
1405
|
+
const s = mkSession();
|
|
1406
|
+
try {
|
|
1407
|
+
s.registerDynamic('tick', 'ToString[$Line]');
|
|
1408
|
+
s.setDynamicInterval(80);
|
|
1409
|
+
for (let i = 0; i < 8; i++) {
|
|
1410
|
+
const r = await withTimeout(
|
|
1411
|
+
s.evaluate(`Pause[0.15]; ${i}`),
|
|
1412
|
+
6000, `test 49 cell ${i}`
|
|
1413
|
+
);
|
|
1414
|
+
assert(!r.aborted, `cell ${i} must not be aborted`);
|
|
1415
|
+
assert(String(r.result.value) === String(i),
|
|
1416
|
+
`cell ${i}: expected "${i}", got "${r.result.value}"`);
|
|
1417
|
+
}
|
|
1418
|
+
} finally {
|
|
1419
|
+
s.close();
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// ── 50. registerDynamic upsert — updates existing entry ───────────────
|
|
1424
|
+
await run('50. registerDynamic upsert updates existing entry', async () => {
|
|
1425
|
+
const s = mkSession();
|
|
1426
|
+
try {
|
|
1427
|
+
await s.evaluate('q = 5;');
|
|
1428
|
+
s.registerDynamic('q', 'ToString[q]'); // initial registration
|
|
1429
|
+
s.registerDynamic('q', 'ToString[q*2]'); // upsert — should override
|
|
1430
|
+
s.setDynamicInterval(150);
|
|
1431
|
+
await withTimeout(s.evaluate('Pause[0.8]; "done"'), 8000, 'test 50 eval');
|
|
1432
|
+
const res = s.getDynamicResults();
|
|
1433
|
+
assert('q' in res, 'key "q" must be present');
|
|
1434
|
+
assert(res.q.value === '10', `expected "10" (q*2), got "${res.q.value}"`);
|
|
1435
|
+
} finally {
|
|
1436
|
+
s.close();
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// ── 51. empty registry + interval — eval completes normally ──────────
|
|
1441
|
+
await run('51. empty registry with interval set — eval completes', async () => {
|
|
1442
|
+
const s = mkSession();
|
|
1443
|
+
try {
|
|
1444
|
+
s.setDynamicInterval(100);
|
|
1445
|
+
// No registrations — dynamicActive must be false
|
|
1446
|
+
assert(!s.dynamicActive, 'dynamicActive must be false with empty registry');
|
|
1447
|
+
const r = await withTimeout(s.evaluate('"noblock"'), 5000, 'test 51 eval');
|
|
1448
|
+
assert(r.result.value === 'noblock', 'eval should complete normally');
|
|
1449
|
+
} finally {
|
|
1450
|
+
s.close();
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// ── 52. drainStalePackets — eval after stale BEGINDLGPKT survives ─────
|
|
1455
|
+
await run('52. evaluate survives stale BEGINDLGPKT (Pattern D)', async () => {
|
|
1456
|
+
const s = mkSession();
|
|
1457
|
+
try {
|
|
1458
|
+
// Install handler so any interrupt produces Dialog[], then evalute with
|
|
1459
|
+
// rejectDialog so the stale packet is auto-drained in C++
|
|
1460
|
+
await s.evaluate(
|
|
1461
|
+
'Quiet[Internal`AddHandler["Interrupt", Function[{}, Dialog[]]]]',
|
|
1462
|
+
{ rejectDialog: true }
|
|
1463
|
+
);
|
|
1464
|
+
for (let i = 0; i < 5; i++) {
|
|
1465
|
+
const r = await withTimeout(
|
|
1466
|
+
s.evaluate(`Pause[0.2]; ${i}`, { rejectDialog: true }),
|
|
1467
|
+
6000, `test 52 iteration ${i}`
|
|
1468
|
+
);
|
|
1469
|
+
assert(!r.aborted, `iteration ${i} must not be aborted`);
|
|
1470
|
+
assert(String(r.result.value) === String(i),
|
|
1471
|
+
`iteration ${i}: expected "${i}", got "${r.result.value}"`);
|
|
1472
|
+
}
|
|
1473
|
+
} finally {
|
|
1474
|
+
s.close();
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1016
1478
|
// ── Teardown ──────────────────────────────────────────────────────────
|
|
1017
1479
|
session.close();
|
|
1018
1480
|
assert(!session.isOpen, 'main session not closed');
|
|
1019
1481
|
|
|
1020
1482
|
console.log();
|
|
1021
1483
|
if (failed === 0) {
|
|
1022
|
-
console.log(`All ${passed} tests passed.`);
|
|
1484
|
+
console.log(`All ${passed} tests passed${skipped ? ` (${skipped} skipped)` : ''}.`);
|
|
1023
1485
|
// Force-exit: WSTP library may keep libuv handles alive after WSClose,
|
|
1024
1486
|
// preventing the event loop from draining naturally. All assertions are
|
|
1025
1487
|
// done; a clean exit(0) is safe.
|
|
1026
1488
|
process.exit(0);
|
|
1027
1489
|
} else {
|
|
1028
|
-
console.log(`${passed} passed, ${failed} failed.`);
|
|
1490
|
+
console.log(`${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ''}.`);
|
|
1029
1491
|
}
|
|
1030
1492
|
}
|
|
1031
1493
|
|