@specific.dev/spectest 0.54.0 → 0.54.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/dist/daemon.js +1 -0
- package/dist/recorder.d.ts +8 -0
- package/dist/terminal.js +29 -7
- package/package.json +1 -1
- package/src/daemon.ts +1 -0
- package/src/recorder.ts +8 -0
- package/src/terminal.ts +28 -7
package/dist/daemon.js
CHANGED
package/dist/recorder.d.ts
CHANGED
|
@@ -481,6 +481,14 @@ export interface TerminalEvent extends BaseEvent {
|
|
|
481
481
|
outputTruncated: boolean;
|
|
482
482
|
/** Set if spawning the PTY itself failed. */
|
|
483
483
|
error?: string;
|
|
484
|
+
/**
|
|
485
|
+
* Set on the open marker of an interactive session (`ctx.terminal.open`).
|
|
486
|
+
* A one-shot `ctx.terminal(...)` event covers a whole command run, so the
|
|
487
|
+
* UI seeks its replay to the end; the open marker is the state at open,
|
|
488
|
+
* so the UI seeks to 0 instead. Older recordings lack this flag — the UI
|
|
489
|
+
* falls back to the marker's sentinel shape (exitCode -1, durationMs 0).
|
|
490
|
+
*/
|
|
491
|
+
interactive?: boolean;
|
|
484
492
|
}
|
|
485
493
|
/** Action taken on an open interactive terminal session. */
|
|
486
494
|
export type TerminalStepAction = "send" | "sendLine" | "press" | "waitFor" | "exit" | "close";
|
package/dist/terminal.js
CHANGED
|
@@ -128,8 +128,23 @@ export async function openTerminal(args) {
|
|
|
128
128
|
// sendLine / press so the recorded `castTimeSec` lands on the echo's
|
|
129
129
|
// frame instead of microseconds before it.
|
|
130
130
|
const frameWaiters = [];
|
|
131
|
+
// Time of the most recently ingested frame. Two jobs: (1) frame
|
|
132
|
+
// timestamps are forced strictly increasing (Date.now() has ms
|
|
133
|
+
// resolution, so a fast PTY can land two frames — or two *ops* —
|
|
134
|
+
// in the same millisecond), and (2) it is what `recordOpEvent`
|
|
135
|
+
// stamps as the step's `castTimeSec`. Together they make the
|
|
136
|
+
// player's `seek(castTimeSec)` (which applies frames `<= target`)
|
|
137
|
+
// land on exactly the frames this op had seen: the step's own last
|
|
138
|
+
// frame is included, and the next op's first frame can never share
|
|
139
|
+
// the stamp. Before this, a `waitFor` that matched in the same
|
|
140
|
+
// millisecond as the following `sendLine`'s echo showed that echo
|
|
141
|
+
// — the replay ran one step ahead of the step list.
|
|
142
|
+
let lastFrameT = 0;
|
|
131
143
|
const ingest = (chunk) => {
|
|
132
|
-
|
|
144
|
+
let t = (Date.now() - start) / 1000;
|
|
145
|
+
if (t <= lastFrameT)
|
|
146
|
+
t = lastFrameT + 0.001;
|
|
147
|
+
lastFrameT = t;
|
|
133
148
|
sink.pushFrame(t, chunk);
|
|
134
149
|
emu.write(chunk);
|
|
135
150
|
if (output.length < OUTPUT_CAP_BYTES) {
|
|
@@ -223,8 +238,8 @@ export async function openTerminal(args) {
|
|
|
223
238
|
}
|
|
224
239
|
// Record cast time AFTER drain, so the `exit` step we record below
|
|
225
240
|
// points the player at the very last frame (the program's final
|
|
226
|
-
// output before EOF).
|
|
227
|
-
exitCastTimeSec =
|
|
241
|
+
// output before EOF). Same between-frames stamp as `recordOpEvent`.
|
|
242
|
+
exitCastTimeSec = lastFrameT + 0.0005;
|
|
228
243
|
sink.markClosed();
|
|
229
244
|
resolveExit(code);
|
|
230
245
|
})();
|
|
@@ -265,10 +280,17 @@ export async function openTerminal(args) {
|
|
|
265
280
|
if (!args.recordEvents)
|
|
266
281
|
return undefined;
|
|
267
282
|
const preview = truncateUtf8(renderScreen());
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
283
|
+
// Stamp the step half a millisecond past its last ingested frame:
|
|
284
|
+
// frame times are strictly increasing in 1ms steps (see `ingest`),
|
|
285
|
+
// so the stamp sits strictly BETWEEN the op's own last frame and
|
|
286
|
+
// any later op's first frame. That makes the boundary unambiguous
|
|
287
|
+
// in both directions — an inclusive seek (`<= stamp`) shows the
|
|
288
|
+
// op's own frame and can never pull in the next op's, and the
|
|
289
|
+
// viewer's exclusive `waitFor` seek (stamp − 0.0004, which
|
|
290
|
+
// disambiguates legacy wall-clock stamps that tied with the next
|
|
291
|
+
// op's echo) still lands past the op's own frame. Same clock as
|
|
292
|
+
// the frames (offset from `start`), so the UI seeks directly.
|
|
293
|
+
const castTimeSec = lastFrameT + 0.0005;
|
|
272
294
|
// Return the event seq so callers can `wrap()` their result value
|
|
273
295
|
// against it — e.g. `expect(await term.waitFor(...))` then links its
|
|
274
296
|
// assertion to this step (sourceSeq) and the UI nests it here.
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
package/src/recorder.ts
CHANGED
|
@@ -537,6 +537,14 @@ export interface TerminalEvent extends BaseEvent {
|
|
|
537
537
|
outputTruncated: boolean;
|
|
538
538
|
/** Set if spawning the PTY itself failed. */
|
|
539
539
|
error?: string;
|
|
540
|
+
/**
|
|
541
|
+
* Set on the open marker of an interactive session (`ctx.terminal.open`).
|
|
542
|
+
* A one-shot `ctx.terminal(...)` event covers a whole command run, so the
|
|
543
|
+
* UI seeks its replay to the end; the open marker is the state at open,
|
|
544
|
+
* so the UI seeks to 0 instead. Older recordings lack this flag — the UI
|
|
545
|
+
* falls back to the marker's sentinel shape (exitCode -1, durationMs 0).
|
|
546
|
+
*/
|
|
547
|
+
interactive?: boolean;
|
|
540
548
|
}
|
|
541
549
|
|
|
542
550
|
/** Action taken on an open interactive terminal session. */
|
package/src/terminal.ts
CHANGED
|
@@ -305,8 +305,22 @@ export async function openTerminal(args: OpenTerminalArgs): Promise<InternalTerm
|
|
|
305
305
|
// sendLine / press so the recorded `castTimeSec` lands on the echo's
|
|
306
306
|
// frame instead of microseconds before it.
|
|
307
307
|
const frameWaiters: Array<() => void> = [];
|
|
308
|
+
// Time of the most recently ingested frame. Two jobs: (1) frame
|
|
309
|
+
// timestamps are forced strictly increasing (Date.now() has ms
|
|
310
|
+
// resolution, so a fast PTY can land two frames — or two *ops* —
|
|
311
|
+
// in the same millisecond), and (2) it is what `recordOpEvent`
|
|
312
|
+
// stamps as the step's `castTimeSec`. Together they make the
|
|
313
|
+
// player's `seek(castTimeSec)` (which applies frames `<= target`)
|
|
314
|
+
// land on exactly the frames this op had seen: the step's own last
|
|
315
|
+
// frame is included, and the next op's first frame can never share
|
|
316
|
+
// the stamp. Before this, a `waitFor` that matched in the same
|
|
317
|
+
// millisecond as the following `sendLine`'s echo showed that echo
|
|
318
|
+
// — the replay ran one step ahead of the step list.
|
|
319
|
+
let lastFrameT = 0;
|
|
308
320
|
const ingest = (chunk: string): void => {
|
|
309
|
-
|
|
321
|
+
let t = (Date.now() - start) / 1000;
|
|
322
|
+
if (t <= lastFrameT) t = lastFrameT + 0.001;
|
|
323
|
+
lastFrameT = t;
|
|
310
324
|
sink.pushFrame(t, chunk);
|
|
311
325
|
emu.write(chunk);
|
|
312
326
|
if (output.length < OUTPUT_CAP_BYTES) {
|
|
@@ -397,8 +411,8 @@ export async function openTerminal(args: OpenTerminalArgs): Promise<InternalTerm
|
|
|
397
411
|
}
|
|
398
412
|
// Record cast time AFTER drain, so the `exit` step we record below
|
|
399
413
|
// points the player at the very last frame (the program's final
|
|
400
|
-
// output before EOF).
|
|
401
|
-
exitCastTimeSec =
|
|
414
|
+
// output before EOF). Same between-frames stamp as `recordOpEvent`.
|
|
415
|
+
exitCastTimeSec = lastFrameT + 0.0005;
|
|
402
416
|
sink.markClosed();
|
|
403
417
|
resolveExit(code);
|
|
404
418
|
})();
|
|
@@ -444,10 +458,17 @@ export async function openTerminal(args: OpenTerminalArgs): Promise<InternalTerm
|
|
|
444
458
|
): number | undefined => {
|
|
445
459
|
if (!args.recordEvents) return undefined;
|
|
446
460
|
const preview = truncateUtf8(renderScreen());
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
461
|
+
// Stamp the step half a millisecond past its last ingested frame:
|
|
462
|
+
// frame times are strictly increasing in 1ms steps (see `ingest`),
|
|
463
|
+
// so the stamp sits strictly BETWEEN the op's own last frame and
|
|
464
|
+
// any later op's first frame. That makes the boundary unambiguous
|
|
465
|
+
// in both directions — an inclusive seek (`<= stamp`) shows the
|
|
466
|
+
// op's own frame and can never pull in the next op's, and the
|
|
467
|
+
// viewer's exclusive `waitFor` seek (stamp − 0.0004, which
|
|
468
|
+
// disambiguates legacy wall-clock stamps that tied with the next
|
|
469
|
+
// op's echo) still lands past the op's own frame. Same clock as
|
|
470
|
+
// the frames (offset from `start`), so the UI seeks directly.
|
|
471
|
+
const castTimeSec = lastFrameT + 0.0005;
|
|
451
472
|
// Return the event seq so callers can `wrap()` their result value
|
|
452
473
|
// against it — e.g. `expect(await term.waitFor(...))` then links its
|
|
453
474
|
// assertion to this step (sourceSeq) and the UI nests it here.
|