@termwright/conformance 0.2.0 → 0.3.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/README.md +39 -30
- package/dist/index.d.ts +9 -31
- package/dist/index.js +650 -282
- package/dist/index.js.map +1 -1
- package/package.json +15 -13
- package/src/fixtures/adversarial-peer.mjs +76 -114
- package/src/fixtures/generic-app.mjs +5 -2
- package/src/fixtures/ink-probe-app.mjs +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@termwright/conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "generic/semantic/adversarial fixtures + adapter contract tests",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": "
|
|
13
|
+
"node": "^22.0.0 || ^24.0.0"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
@@ -24,22 +24,24 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@xterm/headless": "^6.0.0",
|
|
27
|
-
"@termwright/
|
|
28
|
-
"@termwright/
|
|
27
|
+
"@termwright/protocol": "0.3.1",
|
|
28
|
+
"@termwright/resource-broker": "0.3.1",
|
|
29
|
+
"@termwright/driver": "0.3.1"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
33
|
"@types/react": "^19.2.18",
|
|
33
34
|
"ink": "^7.1.1",
|
|
34
35
|
"react": "^19.2.8",
|
|
35
|
-
"@termwright/
|
|
36
|
-
"@termwright/
|
|
37
|
-
"@termwright/
|
|
38
|
-
"@termwright/
|
|
39
|
-
"@termwright/probe-
|
|
36
|
+
"@termwright/ink": "0.3.1",
|
|
37
|
+
"@termwright/logs": "0.3.1",
|
|
38
|
+
"@termwright/mcp": "0.3.1",
|
|
39
|
+
"@termwright/probe-ink": "0.3.1",
|
|
40
|
+
"@termwright/probe-tview": "0.3.1",
|
|
41
|
+
"@termwright/test": "0.3.1"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
|
-
"vitest": "
|
|
44
|
+
"vitest": "4.1.11"
|
|
43
45
|
},
|
|
44
46
|
"peerDependenciesMeta": {
|
|
45
47
|
"vitest": {
|
|
@@ -47,10 +49,10 @@
|
|
|
47
49
|
}
|
|
48
50
|
},
|
|
49
51
|
"scripts": {
|
|
50
|
-
"build": "tsup src/index.ts --format esm --dts --sourcemap",
|
|
52
|
+
"build": "tsup src/index.ts --format esm --dts --sourcemap --clean",
|
|
51
53
|
"typecheck": "tsc --noEmit",
|
|
52
|
-
"test": "
|
|
53
|
-
"test:hostile": "
|
|
54
|
+
"test": "pnpm --dir ../.. test -- -- --run packages/conformance",
|
|
55
|
+
"test:hostile": "pnpm --dir ../.. test -- -- --run packages/conformance/src/suites/adversarial.test.ts packages/conformance/src/suites/mcp-sessions.test.ts",
|
|
54
56
|
"conformance": "node scripts/conformance.mjs"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -86,16 +86,6 @@ const MAX_LOG_RECORD_BYTES = 32 * 1024;
|
|
|
86
86
|
/** Scenarios that need the log channel negotiated in the handshake. */
|
|
87
87
|
const NEEDS_LOGS = new Set(['log-seq-duplicate', 'log-seq-gap', 'log-oversized', 'log-flood']);
|
|
88
88
|
|
|
89
|
-
/** Scenarios that announce `tree-diffs` and push deltas instead of whole trees. */
|
|
90
|
-
const NEEDS_DELTAS = new Set([
|
|
91
|
-
'delta-sequence',
|
|
92
|
-
'delta-bad-base',
|
|
93
|
-
'delta-cursor-clear',
|
|
94
|
-
'delta-flood',
|
|
95
|
-
'delta-removed-missing',
|
|
96
|
-
'delta-before-snapshot',
|
|
97
|
-
]);
|
|
98
|
-
|
|
99
89
|
let logSeq = 0;
|
|
100
90
|
let logBudget = null;
|
|
101
91
|
|
|
@@ -148,27 +138,62 @@ function marker(revision, forSession = sessionId, terminator = '\x07') {
|
|
|
148
138
|
|
|
149
139
|
function tree(revision, nodes, overrides = {}) {
|
|
150
140
|
return {
|
|
151
|
-
v:
|
|
141
|
+
v: 2,
|
|
152
142
|
sessionId,
|
|
153
143
|
revision,
|
|
154
144
|
columns: 80,
|
|
155
145
|
rows: 24,
|
|
156
146
|
rootIds: ['n1'],
|
|
157
147
|
nodes,
|
|
148
|
+
coordinateSpace: {
|
|
149
|
+
status: 'known',
|
|
150
|
+
value: 'viewport-cells',
|
|
151
|
+
evidence: evidence('adversarial-peer'),
|
|
152
|
+
},
|
|
153
|
+
hitGrid: {
|
|
154
|
+
status: 'unsupported',
|
|
155
|
+
capability: 'pointer-hit-grid',
|
|
156
|
+
reason: 'framework-unobservable',
|
|
157
|
+
},
|
|
158
158
|
...overrides,
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function evidence(providerId) {
|
|
163
|
+
return { source: 'framework', method: 'native', strength: 'authoritative', providerId };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function geometry(rect) {
|
|
167
|
+
return {
|
|
168
|
+
displayed: { status: 'known', value: true, evidence: evidence('adversarial-peer') },
|
|
169
|
+
intendedRect: { status: 'known', value: { ...rect }, evidence: evidence('adversarial-peer') },
|
|
170
|
+
visibleRect: { status: 'known', value: { ...rect }, evidence: evidence('adversarial-peer') },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function unknownGeometry() {
|
|
175
|
+
return {
|
|
176
|
+
displayed: { status: 'unknown', reason: 'awaiting-revision-pair' },
|
|
177
|
+
intendedRect: { status: 'unknown', reason: 'awaiting-revision-pair' },
|
|
178
|
+
visibleRect: { status: 'unknown', reason: 'awaiting-revision-pair' },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
162
182
|
function validNodes(label) {
|
|
163
183
|
return [
|
|
164
|
-
{
|
|
184
|
+
{
|
|
185
|
+
id: 'n1',
|
|
186
|
+
role: 'region',
|
|
187
|
+
name: 'Peer',
|
|
188
|
+
geometry: geometry({ row: 0, column: 0, width: 20, height: 2 }),
|
|
189
|
+
},
|
|
165
190
|
{
|
|
166
191
|
id: 'n2',
|
|
167
192
|
parentId: 'n1',
|
|
168
193
|
role: 'button',
|
|
169
194
|
name: label,
|
|
170
195
|
testId: 'peer-button',
|
|
171
|
-
|
|
196
|
+
geometry: geometry({ row: 1, column: 0, width: 10, height: 1 }),
|
|
172
197
|
actions: ['activate', 'focus'],
|
|
173
198
|
},
|
|
174
199
|
];
|
|
@@ -217,28 +242,34 @@ const SCENARIOS = {
|
|
|
217
242
|
cycle: () => {
|
|
218
243
|
send(
|
|
219
244
|
tree(2, [
|
|
220
|
-
{ id: 'n1', role: 'region', name: 'Peer' },
|
|
221
|
-
{ id: 'n2', parentId: 'n3', role: 'button', name: 'A' },
|
|
222
|
-
{ id: 'n3', parentId: 'n2', role: 'button', name: 'B' },
|
|
245
|
+
{ id: 'n1', role: 'region', name: 'Peer', geometry: unknownGeometry() },
|
|
246
|
+
{ id: 'n2', parentId: 'n3', role: 'button', name: 'A', geometry: unknownGeometry() },
|
|
247
|
+
{ id: 'n3', parentId: 'n2', role: 'button', name: 'B', geometry: unknownGeometry() },
|
|
223
248
|
]),
|
|
224
249
|
);
|
|
225
250
|
},
|
|
226
251
|
'missing-parent': () => {
|
|
227
252
|
send(
|
|
228
253
|
tree(2, [
|
|
229
|
-
{ id: 'n1', role: 'region', name: 'Peer' },
|
|
230
|
-
{
|
|
254
|
+
{ id: 'n1', role: 'region', name: 'Peer', geometry: unknownGeometry() },
|
|
255
|
+
{
|
|
256
|
+
id: 'n2',
|
|
257
|
+
parentId: 'ghost',
|
|
258
|
+
role: 'button',
|
|
259
|
+
name: 'Orphan',
|
|
260
|
+
geometry: unknownGeometry(),
|
|
261
|
+
},
|
|
231
262
|
]),
|
|
232
263
|
);
|
|
233
264
|
},
|
|
234
|
-
'impossible-
|
|
265
|
+
'impossible-geometry': () => {
|
|
235
266
|
send(
|
|
236
267
|
tree(2, [
|
|
237
268
|
{
|
|
238
269
|
id: 'n1',
|
|
239
270
|
role: 'region',
|
|
240
271
|
name: 'Peer',
|
|
241
|
-
|
|
272
|
+
geometry: geometry({ row: 9_000, column: 9_000, width: 5, height: 5 }),
|
|
242
273
|
},
|
|
243
274
|
]),
|
|
244
275
|
);
|
|
@@ -260,18 +291,24 @@ const SCENARIOS = {
|
|
|
260
291
|
flood: () => {
|
|
261
292
|
const noise = `${'x'.repeat(4096)}\r\n`;
|
|
262
293
|
for (let chunk = 0; chunk < 512; chunk += 1) process.stdout.write(noise);
|
|
294
|
+
say('PEER FLOOD OUTPUT COMPLETE');
|
|
263
295
|
for (let revision = 2; revision <= 100; revision += 1) {
|
|
264
|
-
socket.write(
|
|
296
|
+
socket.write(
|
|
297
|
+
frame({ type: 'snapshot', snapshot: tree(revision, validNodes(`Flood${revision}`)) }),
|
|
298
|
+
);
|
|
265
299
|
}
|
|
266
300
|
},
|
|
267
301
|
'disconnect-mid-render': () => {
|
|
268
|
-
socket.
|
|
269
|
-
socket.destroy();
|
|
302
|
+
socket.end(frame({ type: 'snapshot', snapshot: tree(2, validNodes('Torn')) }));
|
|
270
303
|
},
|
|
271
304
|
'hostile-unicode': () => {
|
|
272
305
|
// A lone high surrogate survives JSON.stringify as an escape, so the bytes
|
|
273
306
|
// on the wire are ASCII and the receiver is the one that has to fail closed.
|
|
274
|
-
send(
|
|
307
|
+
send(
|
|
308
|
+
tree(2, [
|
|
309
|
+
{ id: 'n1', role: 'region', name: 'lone \ud800 surrogate', geometry: unknownGeometry() },
|
|
310
|
+
]),
|
|
311
|
+
);
|
|
275
312
|
},
|
|
276
313
|
'foreign-session': () => {
|
|
277
314
|
send(tree(2, validNodes('Foreign'), { sessionId: 'someone-elses-session' }));
|
|
@@ -291,9 +328,15 @@ const SCENARIOS = {
|
|
|
291
328
|
socket.write(frame({ type: 'snapshot', snapshot: value }));
|
|
292
329
|
},
|
|
293
330
|
'too-many-nodes': () => {
|
|
294
|
-
const nodes = [{ id: 'n1', role: 'region', name: 'Peer' }];
|
|
331
|
+
const nodes = [{ id: 'n1', role: 'region', name: 'Peer', geometry: unknownGeometry() }];
|
|
295
332
|
for (let index = 2; index <= MAX_NODES + 1_000; index += 1) {
|
|
296
|
-
nodes.push({
|
|
333
|
+
nodes.push({
|
|
334
|
+
id: `n${index}`,
|
|
335
|
+
parentId: 'n1',
|
|
336
|
+
role: 'text',
|
|
337
|
+
name: `t${index}`,
|
|
338
|
+
geometry: unknownGeometry(),
|
|
339
|
+
});
|
|
297
340
|
}
|
|
298
341
|
send(tree(2, nodes));
|
|
299
342
|
},
|
|
@@ -345,32 +388,6 @@ const SCENARIOS = {
|
|
|
345
388
|
// Far past any sane per-second budget, sent in one turn.
|
|
346
389
|
for (let seq = 1; seq <= 500; seq += 1) sendLog(seq, `flood ${seq}`);
|
|
347
390
|
},
|
|
348
|
-
'delta-sequence': () => {
|
|
349
|
-
sendDelta(renameDelta(1, 2, 'Second'));
|
|
350
|
-
sendDelta(renameDelta(2, 3, 'Third'));
|
|
351
|
-
sendDelta(renameDelta(3, 4, 'Fourth'));
|
|
352
|
-
},
|
|
353
|
-
'delta-bad-base': () => {
|
|
354
|
-
// Base 999 was never held, so this cannot be patched onto anything.
|
|
355
|
-
sendDelta(renameDelta(999, 1000, 'Impossible'));
|
|
356
|
-
},
|
|
357
|
-
'delta-cursor-clear': () => {
|
|
358
|
-
// A delta can set a cursor but never clear it. `c` sends the full tree that
|
|
359
|
-
// clears it, so the two halves are separate steps rather than a race.
|
|
360
|
-
sendDelta(renameDelta(1, 2, 'Cursor', { cursor: { row: 3, column: 7, visible: true } }));
|
|
361
|
-
},
|
|
362
|
-
'delta-flood': () => {
|
|
363
|
-
for (let revision = 2; revision <= 200; revision += 1) {
|
|
364
|
-
sendDelta(renameDelta(revision - 1, revision, `Rev${revision}`));
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
'delta-removed-missing': () => {
|
|
368
|
-
sendDelta({ baseRevision: 1, revision: 2, changed: [], removed: ['ghost'] });
|
|
369
|
-
},
|
|
370
|
-
'delta-before-snapshot': () => {
|
|
371
|
-
// Handled specially at handshake time: nothing was published first.
|
|
372
|
-
sendDelta(renameDelta(1, 2, 'Premature'));
|
|
373
|
-
},
|
|
374
391
|
'marker-st-terminator': () => {
|
|
375
392
|
// ST rather than BEL. The tree must pair exactly as it does with BEL.
|
|
376
393
|
socket.write(frame({ type: 'snapshot', snapshot: tree(2, validNodes('Terminated')) }));
|
|
@@ -390,14 +407,13 @@ const SCENARIOS = {
|
|
|
390
407
|
};
|
|
391
408
|
|
|
392
409
|
function hello(overrides = {}) {
|
|
393
|
-
const capabilities = ['tree', '
|
|
410
|
+
const capabilities = ['tree', 'states', 'actions', 'render-revisions'];
|
|
394
411
|
// `log-no-negotiation` deliberately does NOT announce it: the point of that
|
|
395
412
|
// scenario is sending records the driver never invited.
|
|
396
413
|
if (NEEDS_LOGS.has(scenario)) capabilities.push('logs');
|
|
397
|
-
if (NEEDS_DELTAS.has(scenario)) capabilities.push('tree-diffs');
|
|
398
414
|
return {
|
|
399
415
|
type: 'hello',
|
|
400
|
-
protocol: 'termwright/
|
|
416
|
+
protocol: 'termwright/2',
|
|
401
417
|
token,
|
|
402
418
|
adapter: { name: 'adversarial-peer', version: '0.1.0' },
|
|
403
419
|
capabilities,
|
|
@@ -415,41 +431,7 @@ function sendLog(seq, message) {
|
|
|
415
431
|
logSeq = Math.max(logSeq, seq);
|
|
416
432
|
}
|
|
417
433
|
|
|
418
|
-
/**
|
|
419
|
-
* Sends a delta and its marker. The peer keeps no model of its own beyond the
|
|
420
|
-
* revision counter: composing is the receiver's job, and a producer that also
|
|
421
|
-
* composed would only prove it agrees with itself.
|
|
422
|
-
*/
|
|
423
|
-
function sendDelta(delta) {
|
|
424
|
-
// The delta is the message: the body sits beside the discriminator rather
|
|
425
|
-
// than nested under it.
|
|
426
|
-
socket.write(frame({ type: 'tree-delta', ...delta }));
|
|
427
|
-
process.stdout.write(REPAINT + marker(delta.revision));
|
|
428
|
-
published = Math.max(published, delta.revision);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/** A delta that renames the button, so a composed tree is observable on screen. */
|
|
432
|
-
function renameDelta(baseRevision, revision, label, overrides = {}) {
|
|
433
|
-
return {
|
|
434
|
-
baseRevision,
|
|
435
|
-
revision,
|
|
436
|
-
changed: [
|
|
437
|
-
{
|
|
438
|
-
id: 'n2',
|
|
439
|
-
parentId: 'n1',
|
|
440
|
-
role: 'button',
|
|
441
|
-
name: label,
|
|
442
|
-
testId: 'peer-button',
|
|
443
|
-
bounds: { row: 1, column: 0, width: 10, height: 1 },
|
|
444
|
-
actions: ['activate', 'focus'],
|
|
445
|
-
},
|
|
446
|
-
],
|
|
447
|
-
removed: [],
|
|
448
|
-
...overrides,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/** Sends a snapshot together with its marker, so only the tree can be at fault. */
|
|
434
|
+
/** Sends a deliberately chosen snapshot together with its marker. */
|
|
453
435
|
function send(snapshot) {
|
|
454
436
|
socket.write(frame({ type: 'snapshot', snapshot }));
|
|
455
437
|
process.stdout.write(marker(snapshot.revision));
|
|
@@ -467,8 +449,6 @@ function fire() {
|
|
|
467
449
|
attack();
|
|
468
450
|
say('PEER FIRED');
|
|
469
451
|
} catch (error) {
|
|
470
|
-
// A peer that cannot even build its attack must say so rather than die
|
|
471
|
-
// silently and leave the suite waiting on a timeout.
|
|
472
452
|
say(`PEER FAILED ${String(error && error.message ? error.message : error)}`);
|
|
473
453
|
}
|
|
474
454
|
}
|
|
@@ -483,9 +463,6 @@ process.stdin.on('data', (chunk) => {
|
|
|
483
463
|
}
|
|
484
464
|
if (text.includes('g')) fire();
|
|
485
465
|
if (text.includes('p')) publish(published + 1, `Manual${published + 1}`);
|
|
486
|
-
// The cursor-clearing half of `delta-cursor-clear`: only a full tree can do
|
|
487
|
-
// it, and `tree()` builds one without a cursor.
|
|
488
|
-
if (text.includes('c')) publish(published + 1, 'NoCursor');
|
|
489
466
|
});
|
|
490
467
|
|
|
491
468
|
say(`PEER START ${scenario}`);
|
|
@@ -540,26 +517,11 @@ if (endpoint === undefined || token === undefined) {
|
|
|
540
517
|
if (message.type === 'hello-ack') {
|
|
541
518
|
sessionId = message.sessionId;
|
|
542
519
|
logBudget = message.logs ?? null;
|
|
543
|
-
say(
|
|
544
|
-
|
|
545
|
-
// whole point is a delta with nothing to compose onto.
|
|
546
|
-
if (scenario !== 'delta-before-snapshot') publish(1);
|
|
547
|
-
say(`PEER READY ${scenario}`);
|
|
548
|
-
}
|
|
549
|
-
if (message.type === 'get-tree') {
|
|
550
|
-
// Answering is what makes a resync observable end to end: the driver
|
|
551
|
-
// asks, the peer supplies, the session returns to a known tree.
|
|
552
|
-
const revision = published + 1;
|
|
553
|
-
socket.write(
|
|
554
|
-
frame({
|
|
555
|
-
type: 'get-tree-result',
|
|
556
|
-
requestId: message.requestId,
|
|
557
|
-
snapshot: tree(revision, validNodes('Resynced')),
|
|
558
|
-
}),
|
|
520
|
+
say(
|
|
521
|
+
`PEER LOGS ${logBudget === null ? 'denied' : `enabled ${logBudget.maxRecordsPerSecond}/s`}`,
|
|
559
522
|
);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
say(`PEER SENT FULL TREE ${revision}`);
|
|
523
|
+
publish(1);
|
|
524
|
+
say(`PEER READY ${scenario}`);
|
|
563
525
|
}
|
|
564
526
|
if (message.type === 'error') say(`PEER GOT ERROR ${message.code}`);
|
|
565
527
|
}
|
|
@@ -65,7 +65,9 @@ function draw() {
|
|
|
65
65
|
out(index === selected ? `\x1b[1;32m> ${item}\x1b[0m\r\n` : ` ${item}\r\n`);
|
|
66
66
|
}
|
|
67
67
|
out('\x1b[31mRED\x1b[0m \x1b[4mUNDER\x1b[0m \x1b[44mONBLUE\x1b[0m\r\n');
|
|
68
|
-
out(
|
|
68
|
+
out(
|
|
69
|
+
`modes: mouse=${mouse} paste=${bracketed ? 'on' : 'off'} focus=${focusReporting ? 'on' : 'off'}`,
|
|
70
|
+
);
|
|
69
71
|
out(` alt=${alternate ? 'on' : 'off'}\r\n`);
|
|
70
72
|
out(`size: ${process.stdout.columns}x${process.stdout.rows}\r\n`);
|
|
71
73
|
out(`activated: ${activated}\r\n`);
|
|
@@ -88,7 +90,8 @@ function draw() {
|
|
|
88
90
|
.map((name) => `${name}=${process.env[name] === undefined ? 'no' : 'yes'}`)
|
|
89
91
|
.join(' ');
|
|
90
92
|
out(`allow: ${allow}\r\n`);
|
|
91
|
-
if (unicode)
|
|
93
|
+
if (unicode)
|
|
94
|
+
out('U: \u{1F600} \u{1F469}\u200D\u{1F469}\u200D\u{1F467} e\u0301 \u65E5\u672C\u8A9E ok\r\n');
|
|
92
95
|
if (wide) out(`W: ${'0123456789'.repeat(12)} END\r\n`);
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/** Small normal-render Ink process used only by cross-package readiness tests. */
|
|
2
2
|
|
|
3
|
-
import {createElement, useEffect, useRef} from 'react';
|
|
4
|
-
import {Box, Text, render, useApp} from 'ink';
|
|
5
|
-
import {useSemantic} from '@termwright/ink';
|
|
3
|
+
import { createElement, useEffect, useRef } from 'react';
|
|
4
|
+
import { Box, Text, render, useApp } from 'ink';
|
|
5
|
+
import { useSemantic } from '@termwright/ink';
|
|
6
6
|
|
|
7
7
|
function App() {
|
|
8
|
-
const {exit} = useApp();
|
|
8
|
+
const { exit } = useApp();
|
|
9
9
|
const status = useRef(null);
|
|
10
|
-
useSemantic(status, {role: 'status', name: 'Ready', testId: 'status'});
|
|
10
|
+
useSemantic(status, { role: 'status', name: 'Ready', testId: 'status' });
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
const timer = setTimeout(exit, 2_000);
|
|
13
13
|
return () => clearTimeout(timer);
|
|
14
14
|
}, [exit]);
|
|
15
15
|
return createElement(
|
|
16
16
|
Box,
|
|
17
|
-
{ref: status, flexDirection: 'column'},
|
|
17
|
+
{ ref: status, flexDirection: 'column' },
|
|
18
18
|
createElement(Text, null, 'Termwright Conformance'),
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const app = render(createElement(App), {alternateScreen: true, interactive: true});
|
|
22
|
+
const app = render(createElement(App), { alternateScreen: true, interactive: true });
|
|
23
23
|
await app.waitUntilExit();
|