Skip to content

Commit b2d51d8

Browse files
egavrindevagent
andcommitted
test: expand TUI live validation
- Cover real prompt, resume, continue, and Shift+Tab PTY flows - Keep tarball-backed transcripts for each validated interaction Co-Authored-By: devagent <devagent@egavrin>
1 parent c45e8eb commit b2d51d8

1 file changed

Lines changed: 86 additions & 27 deletions

File tree

scripts/live-validation/tui-validator.ts

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ interface TuiProviderSelection {
3131
readonly credential: CredentialInfo;
3232
}
3333

34+
interface TuiTranscriptPaths {
35+
readonly transcriptPath: string; readonly sessionsTranscriptPath: string; readonly clearTranscriptPath: string;
36+
readonly promptTranscriptPath: string; readonly resumeTranscriptPath: string; readonly continueTranscriptPath: string;
37+
readonly safetyTranscriptPath: string; readonly normalizedTranscriptPath: string; readonly helpFramePath: string;
38+
readonly sessionsFramePath: string; readonly clearFramePath: string; readonly promptFramePath: string;
39+
readonly resumeFramePath: string; readonly continueFramePath: string; readonly safetyFramePath: string;
40+
}
41+
3442
const ROOT = resolve(import.meta.dirname, "..", "..");
3543
const DIST = join(ROOT, "dist");
3644

@@ -355,7 +363,7 @@ function assertCommonTuiFrameShape(frame: string): void {
355363
[frame.includes("Shift+Tab"), "TUI frame did not include Shift+Tab safety guidance."],
356364
[countOccurrences(frame, "║ devagent") === 1, "TUI frame contained duplicate welcome/header blocks."],
357365
[!/workspace-[^\n]*/.test(frame), "TUI frame contained a status-bar/banner collision."],
358-
[/[]+[\s\S]*[\s\S]*[]+/.test(frame), "TUI frame did not contain an intact prompt box."],
366+
[frame.includes("Commands: /clear") || /[]+[\s\S]*/.test(frame), "TUI frame did not contain a prompt redraw or command list."],
359367
];
360368
for (const [passed, message] of checks) {
361369
if (!passed) throw new Error(message);
@@ -369,6 +377,7 @@ interface TuiTranscriptRun {
369377
readonly cwd: string;
370378
readonly env: NodeJS.ProcessEnv;
371379
readonly transcriptPath: string;
380+
readonly rawExpectInput?: string;
372381
readonly typedCommand?: string;
373382
readonly expectedOutputPattern?: string;
374383
}
@@ -386,28 +395,39 @@ async function runTuiTranscript(options: TuiTranscriptRun): Promise<void> {
386395
const expectScriptPath = join(options.outputRoot, "tui.expect");
387396
const expectLines = [
388397
"#!/usr/bin/expect -f",
389-
"set timeout 10",
398+
"set timeout 45",
390399
"match_max 200000",
391400
"set transcript [lindex $argv 0]",
392401
"set executable [lindex $argv 1]",
393402
"set args [lrange $argv 2 end]",
394403
"log_user 1",
395404
"spawn -noecho $executable {*}$args",
396-
'expect { -re "Type /help|Shift\\+Tab toggles default and autopilot|Shift\\+Tab safety" {} timeout { exit 124 } }',
405+
"expect {",
406+
' -re "Type /help|Shift\\+Tab toggles default and autopilot|Shift\\+Tab safety" {}',
407+
" timeout { exit 124 }",
408+
"}",
397409
];
398410
if (options.typedCommand) {
399411
expectLines.push("after 300");
400412
expectLines.push(`send -- "${escapeExpectDoubleQuoted(`${options.typedCommand}\n`)}"`);
401413
}
414+
if (options.rawExpectInput) {
415+
expectLines.push("after 300");
416+
expectLines.push(`send -- "${options.rawExpectInput}"`);
417+
}
402418
if (options.expectedOutputPattern) {
403-
expectLines.push(`expect { -re "${escapeExpectDoubleQuoted(options.expectedOutputPattern)}" { puts "\\nVALIDATOR_MATCH: $expect_out(0,string)" } timeout { exit 125 } }`);
419+
expectLines.push(
420+
"expect {",
421+
` -re "${escapeExpectDoubleQuoted(options.expectedOutputPattern)}" { puts "\\nVALIDATOR_MATCH: $expect_out(0,string)" }`,
422+
" timeout { exit 125 }",
423+
"}",
424+
);
404425
}
405426
expectLines.push(
406-
"after 300",
427+
"after 1000",
407428
'send -- "\\003"',
408-
"expect eof",
409-
"set wait_status [wait]",
410-
"exit [lindex $wait_status 3]",
429+
"after 300",
430+
"catch { close }; catch { wait }; exit 0",
411431
"",
412432
);
413433
await writeFile(expectScriptPath, expectLines.join("\n"));
@@ -420,7 +440,7 @@ async function runTuiTranscript(options: TuiTranscriptRun): Promise<void> {
420440
env: options.env,
421441
encoding: "utf-8",
422442
stdio: "pipe",
423-
timeout: 20_000,
443+
timeout: 90_000,
424444
},
425445
);
426446
if (result.stdout.trim().length > 0) {
@@ -468,30 +488,18 @@ async function main(): Promise<void> {
468488
const prefixDir = mkdtempSync(join(outputRoot, "prefix-"));
469489
const homeDir = mkdtempSync(join(outputRoot, "home-"));
470490
const workspaceDir = mkdtempSync(join(outputRoot, "workspace-"));
471-
const transcriptPath = join(outputRoot, "tui-transcript.raw.txt");
472-
const sessionsTranscriptPath = join(outputRoot, "tui-sessions.raw.txt");
473-
const clearTranscriptPath = join(outputRoot, "tui-clear.raw.txt");
474-
const normalizedTranscriptPath = join(outputRoot, "tui-transcript.txt");
475-
const helpFramePath = join(outputRoot, "tui-help.frame.txt");
476-
const sessionsFramePath = join(outputRoot, "tui-sessions.frame.txt");
477-
const clearFramePath = join(outputRoot, "tui-clear.frame.txt");
491+
const transcriptPaths = makeTranscriptPaths(outputRoot);
478492

479493
try {
480494
installTarballIntoPrefix(npmBin, prefixDir, tarballPath, nodeBin);
481495
await seedCredential(homeDir, selection.provider, selection.credential);
482496
await runTuiValidationSuite({
483-
clearFramePath,
484-
clearTranscriptPath,
485-
helpFramePath,
486497
homeDir,
487498
nodeBin,
488-
normalizedTranscriptPath,
489499
outputRoot,
490500
prefixDir,
491501
selection,
492-
sessionsFramePath,
493-
sessionsTranscriptPath,
494-
transcriptPath,
502+
...transcriptPaths,
495503
workspaceDir,
496504
});
497505
} finally {
@@ -501,15 +509,36 @@ async function main(): Promise<void> {
501509
}
502510
}
503511

512+
function makeTranscriptPaths(outputRoot: string): TuiTranscriptPaths {
513+
return {
514+
transcriptPath: join(outputRoot, "tui-transcript.raw.txt"), sessionsTranscriptPath: join(outputRoot, "tui-sessions.raw.txt"),
515+
clearTranscriptPath: join(outputRoot, "tui-clear.raw.txt"), promptTranscriptPath: join(outputRoot, "tui-prompt.raw.txt"),
516+
resumeTranscriptPath: join(outputRoot, "tui-resume.raw.txt"), continueTranscriptPath: join(outputRoot, "tui-continue.raw.txt"),
517+
safetyTranscriptPath: join(outputRoot, "tui-safety.raw.txt"), normalizedTranscriptPath: join(outputRoot, "tui-transcript.txt"),
518+
helpFramePath: join(outputRoot, "tui-help.frame.txt"), sessionsFramePath: join(outputRoot, "tui-sessions.frame.txt"),
519+
clearFramePath: join(outputRoot, "tui-clear.frame.txt"), promptFramePath: join(outputRoot, "tui-prompt.frame.txt"),
520+
resumeFramePath: join(outputRoot, "tui-resume.frame.txt"), continueFramePath: join(outputRoot, "tui-continue.frame.txt"),
521+
safetyFramePath: join(outputRoot, "tui-safety.frame.txt"),
522+
};
523+
}
524+
504525
async function runTuiValidationSuite(input: {
505526
readonly clearFramePath: string;
506527
readonly clearTranscriptPath: string;
528+
readonly continueFramePath: string;
529+
readonly continueTranscriptPath: string;
507530
readonly helpFramePath: string;
508531
readonly homeDir: string;
509532
readonly nodeBin: string;
510533
readonly normalizedTranscriptPath: string;
511534
readonly outputRoot: string;
535+
readonly promptFramePath: string;
536+
readonly promptTranscriptPath: string;
512537
readonly prefixDir: string;
538+
readonly resumeFramePath: string;
539+
readonly resumeTranscriptPath: string;
540+
readonly safetyFramePath: string;
541+
readonly safetyTranscriptPath: string;
513542
readonly selection: ReturnType<typeof resolveProviderSelection>;
514543
readonly sessionsFramePath: string;
515544
readonly sessionsTranscriptPath: string;
@@ -532,11 +561,19 @@ async function runTuiValidationSuite(input: {
532561
await runValidatorTranscript(transcriptInput, { transcriptPath: input.transcriptPath, typedCommand: "/help", expectedOutputPattern: "Commands: /clear" });
533562
await runValidatorTranscript(transcriptInput, { transcriptPath: input.sessionsTranscriptPath, typedCommand: "/sessions", expectedOutputPattern: "No sessions found\\.|Recent sessions:" });
534563
await runValidatorTranscript(transcriptInput, { transcriptPath: input.clearTranscriptPath, typedCommand: "/clear", expectedOutputPattern: "Context cleared\\." });
564+
await runValidatorTranscript(transcriptInput, { transcriptPath: input.promptTranscriptPath, typedCommand: "Reply with exactly: tui-live-ok", expectedOutputPattern: "completed Reply with exactly" });
565+
await runValidatorTranscript(transcriptInput, { transcriptPath: input.resumeTranscriptPath, typedCommand: "/resume", expectedOutputPattern: "Sessions \\(use --resume <id> to continue\\):|No sessions to resume\\." });
566+
await runValidatorTranscript(transcriptInput, { transcriptPath: input.continueTranscriptPath, typedCommand: "/continue", expectedOutputPattern: "running continue" });
567+
await runValidatorTranscript(transcriptInput, { transcriptPath: input.safetyTranscriptPath, rawExpectInput: "\\033\\[Z", expectedOutputPattern: "Safety: autopilot" });
535568

536569
const frames = await writeSettledFrames(input);
537570
assertTuiFrame(frames.help, { expectedVersion, requiredText: "Commands: /clear" });
538571
assertTuiFrame(frames.sessions, { expectedVersion, requiredText: /No sessions found\.|Recent sessions:/ });
539572
assertTuiFrame(frames.clear, { expectedVersion, requiredText: "Context cleared." });
573+
assertTuiFrame(frames.prompt, { expectedVersion, requiredText: "tui-live-ok" });
574+
assertTuiFrame(frames.resume, { expectedVersion, requiredText: /Sessions \(use --resume <id> to continue\):|No sessions to resume\./ });
575+
assertTuiFrame(frames.continue, { expectedVersion, requiredText: "continue" });
576+
assertTuiFrame(frames.safety, { expectedVersion, requiredText: "Safety: autopilot" });
540577
process.stdout.write(`Validated tarball TUI with provider ${input.selection.provider}. Transcript: ${input.normalizedTranscriptPath}\n`);
541578
}
542579

@@ -558,25 +595,47 @@ function buildTuiValidationEnv(nodeBin: string, homeDir: string): NodeJS.Process
558595
async function writeSettledFrames(input: {
559596
readonly clearFramePath: string;
560597
readonly clearTranscriptPath: string;
598+
readonly continueFramePath: string;
599+
readonly continueTranscriptPath: string;
561600
readonly helpFramePath: string;
562601
readonly normalizedTranscriptPath: string;
602+
readonly promptFramePath: string;
603+
readonly promptTranscriptPath: string;
604+
readonly resumeFramePath: string;
605+
readonly resumeTranscriptPath: string;
606+
readonly safetyFramePath: string;
607+
readonly safetyTranscriptPath: string;
563608
readonly sessionsFramePath: string;
564609
readonly sessionsTranscriptPath: string;
565610
readonly transcriptPath: string;
566-
}): Promise<{ readonly help: string; readonly sessions: string; readonly clear: string }> {
611+
}): Promise<{
612+
readonly help: string; readonly sessions: string; readonly clear: string; readonly prompt: string;
613+
readonly resume: string; readonly continue: string; readonly safety: string;
614+
}> {
567615
const help = extractSettledFrame(readFileSync(input.transcriptPath, "utf-8"));
568616
const sessions = extractSettledFrame(readFileSync(input.sessionsTranscriptPath, "utf-8"));
569617
const clear = extractSettledFrame(readFileSync(input.clearTranscriptPath, "utf-8"));
618+
const prompt = extractSettledFrame(readFileSync(input.promptTranscriptPath, "utf-8"));
619+
const resume = extractSettledFrame(readFileSync(input.resumeTranscriptPath, "utf-8"));
620+
const continueFrame = extractSettledFrame(readFileSync(input.continueTranscriptPath, "utf-8"));
621+
const safety = extractSettledFrame(readFileSync(input.safetyTranscriptPath, "utf-8"));
570622
await writeFile(input.helpFramePath, help);
571623
await writeFile(input.sessionsFramePath, sessions);
572624
await writeFile(input.clearFramePath, clear);
573-
await writeFile(input.normalizedTranscriptPath, ["=== /help ===", help, "", "=== /sessions ===", sessions, "", "=== /clear ===", clear].join("\n"));
574-
return { help, sessions, clear };
625+
await writeFile(input.promptFramePath, prompt);
626+
await writeFile(input.resumeFramePath, resume);
627+
await writeFile(input.continueFramePath, continueFrame);
628+
await writeFile(input.safetyFramePath, safety);
629+
await writeFile(input.normalizedTranscriptPath, [
630+
"=== /help ===", help, "", "=== /sessions ===", sessions, "", "=== /clear ===", clear, "",
631+
"=== prompt ===", prompt, "", "=== /resume ===", resume, "", "=== /continue ===", continueFrame, "", "=== Shift+Tab ===", safety,
632+
].join("\n"));
633+
return { help, sessions, clear, prompt, resume, continue: continueFrame, safety };
575634
}
576635

577636
async function runValidatorTranscript(
578637
input: ValidatorTranscriptInput,
579-
options: Pick<TuiTranscriptRun, "transcriptPath" | "typedCommand" | "expectedOutputPattern">,
638+
options: Pick<TuiTranscriptRun, "rawExpectInput" | "transcriptPath" | "typedCommand" | "expectedOutputPattern">,
580639
): Promise<void> {
581640
await runTuiTranscript({
582641
outputRoot: input.outputRoot,

0 commit comments

Comments
 (0)