Compare commits

..
Author SHA1 Message Date
Abdul WahabandCursor 06b26a138f Fix: stream bundle downloads to disk instead of buffering
AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 11:00:47 +05:00
Abdul WahabandCursor fec5d4c89b Fix: safe temp staging and downloadFile error handling (#479)
AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 10:42:58 +05:00
5 changed files with 251 additions and 220 deletions
+56 -38
View File
@@ -9,11 +9,12 @@
*/
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, rmSync, rmdirSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync, cpSync } from 'node:fs';
import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, mkdtempSync, writeFileSync, rmSync, rmdirSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync, cpSync } from 'node:fs';
import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
import { createInterface, emitKeypressEvents } from 'node:readline';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
import { get } from 'node:https';
import { createHash } from 'node:crypto';
import { tmpdir, homedir } from 'node:os';
import { unzipSync } from 'fflate';
@@ -622,13 +623,17 @@ async function downloadAndExtractBundle() {
const localBundle = process.env.IMPECCABLE_BUNDLE_PATH;
if (localBundle) return copyOrExtractLocalBundle(localBundle);
const tmpZip = join(tmpdir(), `impeccable-update-${Date.now()}.zip`);
const tmpDir = join(tmpdir(), `impeccable-update-${Date.now()}`);
await downloadFile(`${API_BASE}/api/download/bundle/universal`, tmpZip);
mkdirSync(tmpDir, { recursive: true });
await extractZip(tmpZip, tmpDir);
rmSync(tmpZip, { force: true });
return tmpDir;
const staging = mkdtempSync(join(tmpdir(), 'impeccable-update-'));
const tmpZip = join(staging, 'bundle.zip');
try {
await downloadFile(`${API_BASE}/api/download/bundle/universal`, tmpZip);
await extractZip(tmpZip, staging);
rmSync(tmpZip, { force: true });
return staging;
} catch (e) {
rmSync(staging, { recursive: true, force: true });
throw e;
}
}
async function copyOrExtractLocalBundle(sourceValue) {
@@ -637,16 +642,18 @@ async function copyOrExtractLocalBundle(sourceValue) {
throw new Error(`Local bundle not found: ${source}`);
}
const tmpDir = join(tmpdir(), `impeccable-local-bundle-${process.pid}-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
if (statSync(source).isDirectory()) {
cpSync(source, tmpDir, { recursive: true });
return tmpDir;
const staging = mkdtempSync(join(tmpdir(), 'impeccable-local-bundle-'));
try {
if (statSync(source).isDirectory()) {
cpSync(source, staging, { recursive: true });
} else {
await extractZip(source, staging);
}
return staging;
} catch (e) {
rmSync(staging, { recursive: true, force: true });
throw e;
}
await extractZip(source, tmpDir);
return tmpDir;
}
/**
@@ -2163,26 +2170,35 @@ function getModifiedSkillFiles(root, providerDirs) {
return modified;
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
// Follow redirect
get(res.headers.location, (res2) => {
res2.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', reject);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}`));
return;
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', reject);
});
async function downloadFile(url, dest, { fetchImpl = globalThis.fetch } = {}) {
let current = url;
let hopsLeft = 5;
while (true) {
const parsed = new URL(current);
if (parsed.protocol !== 'https:') {
throw new Error('Refusing non-HTTPS URL');
}
const res = await fetchImpl(current, { redirect: 'manual' });
if (res.status >= 300 && res.status < 400) {
const location = res.headers.get('location');
if (!location) throw new Error(`HTTP ${res.status}`);
if (hopsLeft <= 0) throw new Error('Too many redirects');
hopsLeft -= 1;
current = new URL(location, current).href;
continue;
}
if (res.status !== 200) {
throw new Error(`HTTP ${res.status}`);
}
if (!res.body) throw new Error('Empty response body');
try {
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest, { flags: 'wx' }));
} catch (e) {
if (e.code !== 'EEXIST') rmSync(dest, { force: true });
throw e;
}
return;
}
}
async function update(flags = []) {
@@ -2332,6 +2348,8 @@ export {
copyProviderHooks,
copyProviderSkills,
decideHookInstall,
downloadAndExtractBundle,
downloadFile,
expectedHookDests,
extractZip,
formatInstallDetectionLines,
+10 -46
View File
@@ -1019,9 +1019,7 @@ ${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria
// exits on any pick and has no update channel, so a followup payload there
// still gets the goodbye screen, never a loading hand nothing will resolve.
const FOLLOWUP = ${payload.followup === true && Boolean(detachedKey) ? 'true' : 'false'};
const KEY = ${JSON.stringify(detachedKey || '')};
const keyQ = KEY ? '?key=' + encodeURIComponent(KEY) : '';
const beat = () => { try { navigator.sendBeacon('/heartbeat' + keyQ); } catch { fetch('/heartbeat' + keyQ, { method: 'POST' }); } };
const beat = () => { try { navigator.sendBeacon('/heartbeat'); } catch { fetch('/heartbeat', { method: 'POST' }); } };
${waiting && waitBudgetMs <= 0 ? '' : 'beat();'}
const beatTimer = setInterval(beat, 5000);
// A dead server must fail loudly: awaiting a rejected fetch here used to
@@ -1032,7 +1030,7 @@ ${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria
// is in flight would overwrite the answer being collected.
document.querySelectorAll('.reroll-btn, #canon').forEach(b => b.setAttribute('disabled', ''));
try {
await fetch('/answer' + keyQ, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId, steer: steer() }) });
await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId, steer: steer() }) });
} catch {
document.body.innerHTML = '<div class="done">The question server went away before this choice could land.<br>Tell the agent your pick in the chat instead.</div>';
return;
@@ -1306,7 +1304,7 @@ ${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria
};
const apply = (value) => {
set(value);
fetch('/build-path' + keyQ, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ value }) });
fetch('/build-path', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ value }) });
if (value === 'comp') enterComp(); else exitComp();
};
// Flipping to comp starts real generation, so it confirms first; the
@@ -1474,7 +1472,7 @@ ${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria
// re-roll and renewed the delivery deadline.
document.querySelectorAll('.reroll-btn, #canon').forEach(b => b.setAttribute('disabled', ''));
try {
await fetch('/answer' + keyQ, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: steer(), ...(register ? { register } : {}) }) });
await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: steer(), ...(register ? { register } : {}) }) });
} catch {
document.body.innerHTML = '<div class="done">The question server went away before this choice could land.<br>Tell the agent your pick in the chat instead.</div>';
return;
@@ -1568,39 +1566,8 @@ ${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria
</script>`;
}
// Browsers omit the :80 suffix on the default HTTP port, so a server on
// --port 80 sees bare loopback hosts and origins.
function allowedHost(host, port) {
if (host === `127.0.0.1:${port}` || host === `localhost:${port}`) return true;
return port === 80 && (host === '127.0.0.1' || host === 'localhost');
}
function allowedOrigin(origin, port) {
if (origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`) return true;
return port === 80 && (origin === 'http://127.0.0.1' || origin === 'http://localhost');
}
function rejectDetachedPost(req, res, url, port) {
if (detachedKey && url.searchParams.get('key') !== detachedKey) {
res.writeHead(401); res.end(); return true;
}
const origin = req.headers.origin;
if (origin && !allowedOrigin(origin, port)) {
res.writeHead(403); res.end(); return true;
}
return false;
}
const server = http.createServer((req, res) => {
const { port } = server.address();
if (!allowedHost(req.headers.host, port)) {
res.writeHead(403); res.end(); return;
}
let url;
try { url = new URL(req.url, 'http://127.0.0.1'); }
catch { res.writeHead(400); res.end(); return; }
const pathname = url.pathname;
if (req.method === 'GET' && pathname === '/') {
if (req.method === 'GET' && req.url === '/') {
const pending = nextFile();
if (pending && fs.existsSync(pending)) {
// A next file the round cannot load has to leave the disk either way:
@@ -1626,8 +1593,7 @@ const server = http.createServer((req, res) => {
res.end(page(awaitingNext));
return;
}
if (req.method === 'POST' && pathname === '/heartbeat') {
if (rejectDetachedPost(req, res, url, port)) return;
if (req.method === 'POST' && req.url === '/heartbeat') {
res.writeHead(204); res.end();
server.lastBeatSeen = Date.now();
if (detachedKey) {
@@ -1643,13 +1609,13 @@ const server = http.createServer((req, res) => {
}
return;
}
if (req.method === 'GET' && pathname === '/next-status') {
if (req.method === 'GET' && req.url === '/next-status') {
const pending = nextFile();
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && pathname.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
@@ -1662,8 +1628,7 @@ const server = http.createServer((req, res) => {
fs.createReadStream(abs).pipe(res);
return;
}
if (req.method === 'POST' && pathname === '/build-path') {
if (rejectDetachedPost(req, res, url, port)) return;
if (req.method === 'POST' && req.url === '/build-path') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
@@ -1683,8 +1648,7 @@ const server = http.createServer((req, res) => {
});
return;
}
if (req.method === 'POST' && pathname === '/answer') {
if (rejectDetachedPost(req, res, url, port)) return;
if (req.method === 'POST' && req.url === '/answer') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
+1 -1
View File
@@ -843,7 +843,7 @@ describe('new-work-e2e: serve-question decision page', () => {
// the server keeps its real clock, so its own idle grace never fires.
await page.clock.install();
let beats = 0;
page.on('request', (r) => { if (new URL(r.url()).pathname === '/heartbeat') beats += 1; });
page.on('request', (r) => { if (r.url().endsWith('/heartbeat')) beats += 1; });
await page.goto(url, { waitUntil: 'load' });
// Playwright actionability waits on rAF, which the fake clock owns, so
// dispatch the click directly.
+11 -134
View File
@@ -1,7 +1,6 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { spawn, execSync } from 'node:child_process';
import http from 'node:http';
import { writeFileSync, readFileSync, rmSync, utimesSync, mkdtempSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
@@ -42,25 +41,6 @@ const PAYLOAD = {
steer: true,
};
function rawRequest(port, { method = 'GET', path: reqPath = '/', headers = {} } = {}, body) {
return new Promise((resolve, reject) => {
const req = http.request({
host: '127.0.0.1',
port,
method,
path: reqPath,
headers,
}, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() }));
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
describe('serve-question', () => {
it('opens Windows URLs through cmd.exe and reserves the start title argument', () => {
assert.deepEqual(
@@ -125,115 +105,12 @@ describe('serve-question', () => {
assert.ok(url, started.out);
const waiting = await run(['--wait', '--key', 'tk', '--poll', '1']);
assert.equal(waiting.code, 3);
await fetch(`${url}answer?key=tk`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'assigned', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'assigned', steer: '' }) });
const collected = await run(['--wait', '--key', 'tk', '--poll', '5']);
assert.equal(collected.code, 0);
assert.match(collected.out, /"optionId":"assigned"/);
});
it('rejects detached POSTs without the session key or with bad Host/Origin', async () => {
const dir = mkdtempSync(path.join(tmpdir(), 'serve-question-'));
const payloadPath = path.join(dir, 'q.json');
const key = 'seckey';
const answerPath = path.join(dir, '.impeccable', 'questions', `${key}.answer.json`);
writeFileSync(payloadPath, JSON.stringify(PAYLOAD));
const run = (args) => new Promise((resolve) => {
const child = spawn(process.execPath, [SCRIPT, ...args], { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] });
let out = '';
child.stdout.on('data', (chunk) => { out += chunk; });
child.on('exit', (code) => resolve({ code, out }));
});
const started = await run(['--start', '--payload', payloadPath, '--no-open', '--key', key]);
assert.equal(started.code, 0);
const url = started.out.match(/QUESTION URL: (\S+)/)?.[1];
assert.ok(url, started.out);
const port = Number(new URL(url).port);
const goodHost = `127.0.0.1:${port}`;
const body = JSON.stringify({ optionId: 'assigned', steer: '' });
const jsonHeaders = { 'content-type': 'application/json', Host: goodHost };
const noKey = await fetch(`http://${goodHost}/answer`, { method: 'POST', headers: jsonHeaders, body });
assert.equal(noKey.status, 401);
assert.equal(existsSync(answerPath), false);
const waiting = await run(['--wait', '--key', key, '--poll', '1']);
assert.equal(waiting.code, 3);
const wrongKey = await fetch(`http://${goodHost}/answer?key=wrong`, { method: 'POST', headers: jsonHeaders, body });
assert.equal(wrongKey.status, 401);
const evilOrigin = await rawRequest(port, {
method: 'POST',
path: `/answer?key=${key}`,
headers: { ...jsonHeaders, Origin: 'https://evil.example' },
}, body);
assert.equal(evilOrigin.status, 403);
assert.equal(existsSync(answerPath), false);
const spoofedHostPost = await rawRequest(port, {
method: 'POST',
path: `/answer?key=${key}`,
headers: { ...jsonHeaders, Host: `evil.example:${port}` },
}, body);
assert.equal(spoofedHostPost.status, 403);
const noKeyBeat = await fetch(`http://${goodHost}/heartbeat`, { method: 'POST', headers: { Host: goodHost } });
assert.equal(noKeyBeat.status, 401);
const spoofedHostGet = await rawRequest(port, {
path: '/',
headers: { Host: `evil.example:${port}` },
});
assert.equal(spoofedHostGet.status, 403);
// The bare-host allowance exists only for --port 80, where browsers omit
// the suffix; on any other port a portless Host stays rejected.
const bareHostGet = await rawRequest(port, {
path: '/',
headers: { Host: '127.0.0.1' },
});
assert.equal(bareHostGet.status, 403);
const slashSlash = await rawRequest(port, {
path: '//',
headers: { Host: goodHost },
});
assert.equal(slashSlash.status, 400);
assert.equal((await fetch(url)).status, 200);
// The build-path flip commands the agent through --wait, so it takes the
// same key and Origin gate as /answer.
const flipPath = path.join(dir, '.impeccable', 'questions', `${key}.flip.json`);
const flipBody = JSON.stringify({ value: 'comp' });
const noKeyFlip = await fetch(`http://${goodHost}/build-path`, { method: 'POST', headers: jsonHeaders, body: flipBody });
assert.equal(noKeyFlip.status, 401);
assert.equal(existsSync(flipPath), false);
const evilOriginFlip = await rawRequest(port, {
method: 'POST',
path: `/build-path?key=${key}`,
headers: { ...jsonHeaders, Origin: 'https://evil.example' },
}, flipBody);
assert.equal(evilOriginFlip.status, 403);
assert.equal(existsSync(flipPath), false);
const okFlip = await fetch(`http://${goodHost}/build-path?key=${key}`, { method: 'POST', headers: jsonHeaders, body: flipBody });
assert.equal(okFlip.status, 200);
assert.equal(existsSync(flipPath), true);
const flipped = await run(['--wait', '--key', key, '--poll', '2']);
assert.equal(flipped.code, 0);
assert.match(flipped.out, /BUILD PATH FLIPPED/);
const html = await (await fetch(url)).text();
assert.match(html, /const KEY = "seckey"/);
assert.match(html, /\/answer' \+ keyQ/);
assert.match(html, /\/heartbeat' \+ keyQ/);
assert.match(html, /\/build-path' \+ keyQ/);
const ok = await fetch(`http://${goodHost}/answer?key=${key}`, { method: 'POST', headers: jsonHeaders, body });
assert.equal(ok.status, 200);
const collected = await run(['--wait', '--key', key, '--poll', '5']);
assert.equal(collected.code, 0);
assert.match(collected.out, /"optionId":"assigned"/);
});
it('headless detection spares the modes that never open a browser', async () => {
// Only the blocking serve path auto-opens a URL. --wait polls a daemon
// that is already running, --stop kills one, --schema just prints text,
@@ -334,7 +211,7 @@ describe('serve-question', () => {
// Beat well past the 3s timeout: the timer must not fire under a live page.
const beatUntil = Date.now() + 5500;
while (Date.now() < beatUntil) {
await fetch(`${url}heartbeat?key=life`, { method: 'POST' });
await fetch(`${url}heartbeat`, { method: 'POST' });
await new Promise((r) => setTimeout(r, 400));
}
const alive = await fetch(url);
@@ -419,7 +296,7 @@ describe('serve-question', () => {
// One beat, then silence: the idle grace must still reclaim the daemon.
// Before the fix, the whole lifetime check sat inside timeoutSec > 0 and
// a closed tab leaked this daemon forever.
await fetch(`${url}heartbeat?key=zero`, { method: 'POST' });
await fetch(`${url}heartbeat`, { method: 'POST' });
const deadline = Date.now() + 12000;
let gone = false;
while (Date.now() < deadline && !gone) {
@@ -444,8 +321,8 @@ describe('serve-question', () => {
const url = started.out.match(/QUESTION URL: (\S+)/)?.[1];
assert.ok(url, started.out);
try {
await fetch(`${url}heartbeat?key=latehand`, { method: 'POST' });
await fetch(`${url}answer?key=latehand`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}heartbeat`, { method: 'POST' });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
// Go silent like a stalled page until just before the 3s idle
// deadline, then deliver: the daemon used to exit before the page's
// watch could claim the hand, orphaning a delivery --update had
@@ -490,7 +367,7 @@ describe('serve-question', () => {
// serving decision has to live here: once a re-roll answer is collected
// and no replacement has landed, GET / re-enters the bounded shuffle
// wait instead of re-serving the answered cards.
await fetch(`${url}answer?key=refresh`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
const waitingPage = await (await fetch(url)).text();
assert.ok(waitingPage.includes('awaitNextRound(false,'), 'a refresh mid re-roll re-enters the shuffle wait');
const nextPath = path.join(dir, 'next.json');
@@ -520,7 +397,7 @@ describe('serve-question', () => {
const url = started.out.match(/QUESTION URL: (\S+)/)?.[1];
assert.ok(url, started.out);
try {
await fetch(`${url}answer?key=deadline`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
const fresh = await (await fetch(url)).text();
const budget = Number(fresh.match(/awaitNextRound\(false, (\d+)\);/)?.[1]);
assert.ok(budget > 0 && budget <= 3000, `the waiting page carries the remaining allowance, got ${budget}`);
@@ -529,7 +406,7 @@ describe('serve-question', () => {
// click-time disable can race a second click, so the server keeps the
// first stamp instead of renewing the allowance.
await new Promise((r) => setTimeout(r, 1200));
await fetch(`${url}answer?key=deadline`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
const restamped = Number((await (await fetch(url)).text()).match(/awaitNextRound\(false, (\d+)\);/)?.[1]);
assert.ok(restamped > 0 && restamped < 2500, `a duplicate re-roll does not renew the allowance, got ${restamped}`);
await new Promise((r) => setTimeout(r, 3500));
@@ -565,7 +442,7 @@ describe('serve-question', () => {
// A bad file that reaches the disk anyway must not trap the page:
// GET / discards it, so /next-status stops reporting a hand that can
// never render and the bounded wait resumes instead of reload-looping.
await fetch(`${url}answer?key=badhand`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
writeFileSync(path.join(dir, '.impeccable', 'questions', 'badhand.next.json'), JSON.stringify({ title: 'No options' }));
const page = await (await fetch(url)).text();
assert.ok(page.includes('awaitNextRound(false,'), 'the round stays in the wait');
@@ -591,7 +468,7 @@ describe('serve-question', () => {
const url = started.out.match(/QUESTION URL: (\S+)/)?.[1];
assert.ok(url, started.out);
try {
await fetch(`${url}answer?key=silent`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
const collected = await run(['--wait', '--key', 'silent', '--poll', '2']);
assert.equal(collected.code, 0, collected.out);
// The stalled page went silent by design: fake a beat older than the
@@ -644,7 +521,7 @@ describe('serve-question', () => {
const url = started.out.match(/QUESTION URL: (\S+)/)?.[1];
assert.ok(url, started.out);
try {
await fetch(`${url}answer?key=claimgap`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
await fetch(`${url}answer`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: '' }) });
const collected = await run(['--wait', '--key', 'claimgap', '--poll', '2']);
assert.equal(collected.code, 0, collected.out);
const statePath = path.join(dir, '.impeccable', 'questions', 'claimgap.state.json');
+173 -1
View File
@@ -11,7 +11,7 @@
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { execSync, execFileSync } from 'child_process';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync } from 'fs';
import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync, statSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
@@ -19,6 +19,8 @@ import {
copyProviderHooks,
copyProviderSkills,
decideHookInstall,
downloadAndExtractBundle,
downloadFile,
expectedHookDests,
formatInstallDetectionLines,
mergeHookManifests,
@@ -2214,3 +2216,173 @@ describe('hermesGlobalHome resolver (PR #521)', () => {
rmSync(home, { recursive: true, force: true });
}, 20000);
});
describe('downloadAndExtractBundle: safe staging dir (#479)', () => {
test('local bundle uses mkdtemp under tmpdir with 0700 perms', async () => {
const tmp = mkdtempSync(join(tmpdir(), 'imp-test-staging-'));
const bundleRoot = createFakeUniversalBundle(tmp, ['.claude']);
const prev = process.env.IMPECCABLE_BUNDLE_PATH;
let stagingDir;
try {
process.env.IMPECCABLE_BUNDLE_PATH = bundleRoot;
stagingDir = await downloadAndExtractBundle();
expect(stagingDir.startsWith(tmpdir())).toBe(true);
const basename = stagingDir.split(/[/\\]/).pop();
expect(basename.startsWith('impeccable-local-bundle-')).toBe(true);
expect(basename).not.toMatch(/^impeccable-local-bundle-\d+-\d+$/);
if (process.platform !== 'win32') {
expect(statSync(stagingDir).mode & 0o777).toBe(0o700);
}
expect(existsSync(join(stagingDir, '.claude', 'skills', 'impeccable', 'SKILL.md'))).toBe(true);
} finally {
if (prev === undefined) delete process.env.IMPECCABLE_BUNDLE_PATH;
else process.env.IMPECCABLE_BUNDLE_PATH = prev;
if (stagingDir) rmSync(stagingDir, { recursive: true, force: true });
rmSync(tmp, { recursive: true, force: true });
}
});
});
describe('downloadFile (#479)', () => {
test('200 writes body to dest with wx flag', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async () => new Response('hello', { status: 200 });
await downloadFile('https://example.com/file', dest, { fetchImpl });
expect(readFileSync(dest, 'utf8')).toBe('hello');
await expect(downloadFile('https://example.com/file', dest, { fetchImpl }))
.rejects.toThrow();
expect(readFileSync(dest, 'utf8')).toBe('hello');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('404 throws and dest does not exist', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async () => new Response('not found', { status: 404 });
await expect(downloadFile('https://example.com/missing', dest, { fetchImpl }))
.rejects.toThrow(/HTTP 404/);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('redirect 302 to 200 follows location and writes second body', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
let callCount = 0;
const fetchImpl = async (url) => {
callCount++;
if (url === 'https://example.com/start') {
return new Response('', { status: 302, headers: { location: 'https://example.com/final' } });
}
return new Response('final body', { status: 200 });
};
await downloadFile('https://example.com/start', dest, { fetchImpl });
expect(callCount).toBe(2);
expect(readFileSync(dest, 'utf8')).toBe('final body');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('redirect 302 to 404 throws and dest does not exist', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async (url) => {
if (url.includes('/start')) {
return new Response('', { status: 302, headers: { location: 'https://example.com/bad' } });
}
return new Response('error', { status: 404 });
};
await expect(downloadFile('https://example.com/start', dest, { fetchImpl }))
.rejects.toThrow(/HTTP 404/);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('redirect to http throws non-HTTPS and dest does not exist', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async () => new Response('', { status: 302, headers: { location: 'http://example.com/insecure' } });
await expect(downloadFile('https://example.com/start', dest, { fetchImpl }))
.rejects.toThrow(/non-HTTPS/i);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('relative redirect location resolved against current URL', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async (url) => {
if (url === 'https://example.com/api/start') {
return new Response('', { status: 302, headers: { location: '/final' } });
}
expect(url).toBe('https://example.com/final');
return new Response('ok', { status: 200 });
};
await downloadFile('https://example.com/api/start', dest, { fetchImpl });
expect(readFileSync(dest, 'utf8')).toBe('ok');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('more than maxRedirects hops throws and dest does not exist', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async () => new Response('', { status: 302, headers: { location: 'https://example.com/loop' } });
await expect(downloadFile('https://example.com/loop', dest, { fetchImpl }))
.rejects.toThrow(/Too many redirects/);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('fetchImpl rejection leaves dest absent', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
const fetchImpl = async () => { throw new Error('network down'); };
await expect(downloadFile('https://example.com/file', dest, { fetchImpl }))
.rejects.toThrow(/network down/);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('http initial URL throws without calling fetch', async () => {
const dir = mkdtempSync(join(tmpdir(), 'imp-dl-'));
const dest = join(dir, 'out.bin');
try {
let called = false;
const fetchImpl = async () => { called = true; return new Response('x', { status: 200 }); };
await expect(downloadFile('http://example.com/file', dest, { fetchImpl }))
.rejects.toThrow(/non-HTTPS/i);
expect(called).toBe(false);
expect(existsSync(dest)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});