Files
ECC/tests/lib/terminal-spinner.test.js
28e53a0bc1 feat(install): add guided multi-harness installer (#2649)
* feat(install): add guided Claude plugin setup

* fix: support Claude command shims on Windows

* feat: support safe Claude plugin scope migration

* fix(install): preserve interactive setup terminal

* fix(install): auto-migrate setup scope changes

* feat(install): add guided multi-harness installer

* fix(install): sync Yarn binary metadata

* fix(install): handle wizard EOF on Node 18

* ci: allow installer matrix tests to finish

* test(install): allow slower PowerShell delegation

* fix(install): harden guided provider reconciliation

* test(install): harden packaged and local compatibility

* chore: prepare guided installer release 2.2.0

* fix(install): report refreshed Codex marketplace state

* fix(install): verify managed content provenance

* test(install): allow empty Yarn smoke fixture

* test(install): invoke Windows package shims safely

* fix(install): close cross-platform release gaps

* fix(install): require trusted GitHub origins

* fix(install): preserve hook profile precedence

* refactor(install): centralize trusted GitHub origins

* ci: retrigger workflow run after merge of main

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:39:49 -04:00

175 lines
4.9 KiB
JavaScript

'use strict';
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const {
CLEAR_LINE,
FRAMES,
runAnimator,
startTerminalSpinner,
} = require('../../scripts/lib/terminal-spinner');
const spinnerModule = path.join(
__dirname,
'..',
'..',
'scripts',
'lib',
'terminal-spinner.js'
);
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
passed += 1;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
console.log('\n=== Terminal spinner tests ===\n');
test('animator advances frames and exits when its parent disconnects', () => {
const writes = [];
let tick;
let disconnect;
let cleared;
let exitCode;
const timer = Symbol('timer');
runAnimator('Applying ECC setup...', {
clearSchedule: value => { cleared = value; },
exit: code => { exitCode = code; },
onDisconnect: handler => { disconnect = handler; },
output: { write: value => writes.push(value) },
schedule: (callback, interval) => {
assert.strictEqual(interval, 80);
tick = callback;
return timer;
},
});
tick();
tick();
assert.deepStrictEqual(writes, [
`\r${FRAMES[1]} Applying ECC setup...`,
`\r${FRAMES[2]} Applying ECC setup...`,
]);
disconnect();
assert.strictEqual(cleared, timer);
assert.strictEqual(exitCode, 0);
});
test('spinner renders immediately and clears again after animator termination', () => {
const writes = [];
const spawnCalls = [];
const handlers = {};
const animatorErrors = [];
let killCount = 0;
const child = {
kill: () => { killCount += 1; },
on: (event, handler) => {
assert.strictEqual(event, 'error');
handlers[event] = handler;
},
once: (event, handler) => { handlers[event] = handler; },
};
const spinner = startTerminalSpinner('Applying ECC setup...', {
onAnimatorError: error => animatorErrors.push(error.message),
output: { write: value => writes.push(value) },
spawnProcess: (...args) => {
spawnCalls.push(args);
return child;
},
});
assert.strictEqual(writes[0], `${FRAMES[0]} Applying ECC setup...`);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0][0], process.execPath);
assert.deepStrictEqual(spawnCalls[0][1].slice(1), [
'--animate',
'Applying ECC setup...',
]);
assert.strictEqual(typeof handlers.error, 'function');
handlers.error(new Error('animation unavailable'));
assert.deepStrictEqual(animatorErrors, ['animation unavailable']);
spinner.stop();
writes.push(`\r${FRAMES[2]} late frame`);
handlers.close();
spinner.stop();
assert.strictEqual(killCount, 1);
assert.strictEqual(writes.at(-1), CLEAR_LINE);
assert.deepStrictEqual(writes.slice(-3), [
CLEAR_LINE,
`\r${FRAMES[2]} late frame`,
CLEAR_LINE,
]);
});
test('spinner keeps a visible first frame when the animator cannot launch', () => {
const writes = [];
const spinner = startTerminalSpinner('Applying ECC setup...', {
output: { write: value => writes.push(value) },
spawnProcess: () => { throw new Error('spawn unavailable'); },
});
spinner.stop();
assert.deepStrictEqual(writes, [
`${FRAMES[0]} Applying ECC setup...`,
CLEAR_LINE,
]);
});
test('real animator advances independently and cannot write after cleanup', () => {
const source = `
const { startTerminalSpinner } = require(${JSON.stringify(spinnerModule)});
const spinner = startTerminalSpinner('Applying ECC setup...');
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
spinner.stop();
`;
const result = spawnSync(process.execPath, ['-e', source], {
encoding: 'utf8',
timeout: 3000,
});
assert.ifError(result.error);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, new RegExp(`${FRAMES[0]} Applying ECC setup`));
assert.match(result.stdout, new RegExp(`${FRAMES[1]} Applying ECC setup`));
const clearIndex = result.stdout.lastIndexOf(CLEAR_LINE);
assert.ok(clearIndex > 0, 'real animator should clear its line');
assert.doesNotMatch(
result.stdout.slice(clearIndex + CLEAR_LINE.length),
/Applying ECC setup/,
'real animator should not render after cleanup'
);
});
test('real animator exits when its parent process disappears', () => {
const source = `
const { startTerminalSpinner } = require(${JSON.stringify(spinnerModule)});
startTerminalSpinner('Applying ECC setup...');
process.exit(0);
`;
const result = spawnSync(process.execPath, ['-e', source], {
encoding: 'utf8',
timeout: 3000,
});
assert.ifError(result.error);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, new RegExp(`${FRAMES[0]} Applying ECC setup`));
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);