Files
sprites/tools/deploy/test/progress.test.ts
Claude 93ce1f50f9 Draw a progress bar for deploy on a terminal
A deploy's last phase is a long silent wait: thousands of files packed
into an upload or copied into a staging tree, with nothing between the
line naming the command and the line after it finishes.

Draw a bar over it when stderr is a terminal, and nothing at all when it
isn't -- piped or under CI a redraw per file is noise in the log, and
the stdout lines the deploy already prints are the record either way.

The tar entries a pack hands out are counted by their entry callbacks,
which wait on the pack draining into whoever is reading it, so the bar
follows what the upload command has taken rather than what has been read
off disk. The staging and -o paths count files as they land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRBNA4JH27Eq3cgJpzCLV2
2026-09-12 12:04:48 +00:00

77 lines
2.4 KiB
TypeScript

import assert from 'node:assert/strict';
import {test} from 'node:test';
import {Progress, type Term} from '../progress.ts';
function term(overrides: Partial<Term> = {}): Term & {written: string[]} {
let written: string[] = [];
return {isTTY: true, columns: 80, write: (s: string) => written.push(s), written, ...overrides};
}
// Every write the bar makes, with the clear-and-return prefix stripped.
function lines(t: {written: string[]}): string[] {
return t.written.map(s => s.replace('\x1b[2K\r', ''));
}
test('a bar draws nothing when stderr is not a terminal', () => {
let t = term({isTTY: false});
let bar = new Progress('deploy: uploading', 4, t);
for (let i = 0; i < 4; i++) {
bar.tick();
}
bar.finish();
assert.deepEqual(t.written, []);
});
test('a bar draws its label, a filled share of the width, and the counts', () => {
let t = term();
let bar = new Progress('up', 100, t);
// The timer holds the redraws back, so drive it to the end and read the
// first draw, which the constructor always makes.
let [first] = lines(t);
assert.equal(first, `up [${'-'.repeat(66)}] 0/100`);
assert.equal(first!.length, 79);
for (let i = 0; i < 50; i++) {
bar.tick();
}
assert.equal(lines(t).length, 1);
});
test('a bar never writes past the terminal width', () => {
for (let columns of [20, 40, 80, 200]) {
let t = term({columns});
new Progress('deploying something', 1000, t);
for (let line of lines(t)) {
assert.ok(line.length < columns, `${line.length} >= ${columns} at ${columns} columns`);
}
}
});
test('a narrow terminal gets the counts without a bar', () => {
let t = term({columns: 24});
new Progress('up', 1000, t);
assert.deepEqual(lines(t), ['up 0/1000']);
});
test('a bar clears its line once it is full, leaving the rest of the wait alone', () => {
let t = term();
let bar = new Progress('up', 2, t);
bar.tick();
bar.tick();
assert.equal(t.written.at(-1), '\x1b[2K\r');
assert.equal(lines(t).at(-1), '');
// Already cleared: finishing again is not a second clear.
let count = t.written.length;
bar.finish();
assert.equal(t.written.length, count);
});
test('finishing a bar mid-run clears the line it drew', () => {
let t = term();
let bar = new Progress('up', 100, t);
bar.tick();
bar.finish();
assert.equal(t.written.at(-1), '\x1b[2K\r');
});