Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion qa/audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const out = join(root, 'qa', 'shots');
mkdirSync(out, { recursive: true });

const userData = mkdtempSync(join(tmpdir(), 'devdeck-qa-'));
const userData = sandboxDir('devdeck-qa-');
const app = await electron.launch({
args: ['.', `--user-data-dir=${userData}`, '--no-sandbox', '--disable-gpu'],
cwd: root,
Expand Down
3 changes: 2 additions & 1 deletion qa/claude-update.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

// Windows harness (fixture claude.cmd on PATH). Run with npm run qa:update.
const root = resolve(process.argv[2] ?? dirname(dirname(fileURLToPath(import.meta.url))));
const shots = resolve(process.argv[3] ?? join(root, 'qa', 'shots', 'claude-update'));
mkdirSync(shots, { recursive: true });
const { closeElectron } = await import('file:///' + join(root, 'qa', 'electron-lifecycle.mjs').replace(/\\/g, '/'));

const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-update-'));
const sandbox = sandboxDir('devdeck-update-');
const repo = join(sandbox, 'update-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
7 changes: 4 additions & 3 deletions qa/close.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,21 @@ import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { closeElectron } from './electron-lifecycle.mjs';
import { sandboxDir } from './sandbox.mjs';

const repo = dirname(dirname(fileURLToPath(import.meta.url)));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const stamp = () => new Date().toISOString().slice(11, 19);

const fixtureRepo = mkdtempSync(join(tmpdir(), 'devdeck-close-fixture-'));
const fixtureRepo = sandboxDir('devdeck-close-fixture-');
writeFileSync(join(fixtureRepo, 'README.md'), '# fixture');
execFileSync('git', ['init', '-q'], { cwd: fixtureRepo });
execFileSync('git', ['-c', 'user.email=qa@devdeck', '-c', 'user.name=qa', 'add', '-A'], { cwd: fixtureRepo });
execFileSync('git', ['-c', 'user.email=qa@devdeck', '-c', 'user.name=qa', 'commit', '-q', '-m', 'fixture'], { cwd: fixtureRepo });

async function launch(tag, registerFolder) {
const userData = mkdtempSync(join(tmpdir(), `devdeck-close-${tag}-`));
const temp = mkdtempSync(join(tmpdir(), `devdeck-closetmp-${tag}-`));
const userData = sandboxDir(`devdeck-close-${tag}-`);
const temp = sandboxDir(`devdeck-closetmp-${tag}-`);
const app = await electron.launch({
args: ['.', `--user-data-dir=${userData}`, '--no-sandbox', '--disable-gpu'],
cwd: repo,
Expand Down
3 changes: 2 additions & 1 deletion qa/cockpit-tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

const root = resolve(process.argv[2] ?? dirname(dirname(fileURLToPath(import.meta.url))));
if (process.platform !== 'win32') { console.log('SKIP: Windows native PTY required'); process.exit(0); }
const shots = resolve(process.argv[3] ?? join(root, 'qa', 'shots', 'cockpit-tools'));
mkdirSync(shots, { recursive: true });
const { closeElectron } = await import('file:///' + join(root, 'qa', 'electron-lifecycle.mjs').replace(/\\/g, '/'));

const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-cockpit-tools-'));
const sandbox = sandboxDir('devdeck-cockpit-tools-');
const repo = join(sandbox, 'tools-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
3 changes: 2 additions & 1 deletion qa/image-rows.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

const root = resolve(process.argv[2] ?? dirname(dirname(fileURLToPath(import.meta.url))));
if (process.platform !== 'win32') { console.log('SKIP: Windows native PTY required'); process.exit(0); }
const shots = resolve(process.argv[3] ?? join(root, 'qa', 'shots', 'image-rows'));
mkdirSync(shots, { recursive: true });
const { closeElectron } = await import('file:///' + join(root, 'qa', 'electron-lifecycle.mjs').replace(/\\/g, '/'));

const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-image-rows-'));
const sandbox = sandboxDir('devdeck-image-rows-');
const repo = join(sandbox, 'rows-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
5 changes: 3 additions & 2 deletions qa/images-remote.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { closeElectron } from './electron-lifecycle.mjs';
import { sandboxDir } from './sandbox.mjs';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const shots = resolve(process.env.DEVDECK_IMAGES_OUT || join(root, 'qa', 'shots', 'images-remote'));
mkdirSync(shots, { recursive: true });
const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-images-remote-'));
const sandbox = sandboxDir('devdeck-images-remote-');
const repo = join(sandbox, 'host-workspace');
const bin = join(sandbox, 'bin');
for (const p of [repo, bin]) mkdirSync(p, { recursive: true });
Expand Down Expand Up @@ -146,7 +147,7 @@ try {

await check('a Claude in Chrome screenshot in the host temp directory travels; other temp files do not', async () => {
const shotDir = mkdtempSync(join(tmpdir(), 'claude-chrome-screenshots-'));
const other = mkdtempSync(join(tmpdir(), 'devdeck-qa-other-'));
const other = sandboxDir('devdeck-qa-other-');
copyFileSync(join(root, 'build', 'icon.png'), join(shotDir, 'screenshot-1-1.jpg'));
copyFileSync(join(root, 'build', 'icon.png'), join(other, 'shot.png'));
try {
Expand Down
3 changes: 2 additions & 1 deletion qa/images.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

// Windows harness (fixture claude.cmd on PATH). Run with npm run qa:images.
const root = resolve(process.argv[2] ?? dirname(dirname(fileURLToPath(import.meta.url))));
const shots = resolve(process.argv[3] ?? join(root, 'qa', 'shots', 'images'));
mkdirSync(shots, { recursive: true });
const { closeElectron } = await import('file:///' + join(root, 'qa', 'electron-lifecycle.mjs').replace(/\\/g, '/'));

const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-images-'));
const sandbox = sandboxDir('devdeck-images-');
const repo = join(sandbox, 'image-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
7 changes: 4 additions & 3 deletions qa/link.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { dirname, join } from 'node:path';
import { connect as netConnect } from 'node:net';
import { fileURLToPath } from 'node:url';
import { closeElectron } from './electron-lifecycle.mjs';
import { sandboxDir } from './sandbox.mjs';

const repo = dirname(dirname(fileURLToPath(import.meta.url)));

Expand All @@ -25,7 +26,7 @@ const repo = dirname(dirname(fileURLToPath(import.meta.url)));
// child processes down with it (renderer, GPU, network service, exit code -1, no dump). Bisected
// across five builds before the pattern showed: every run from a worktree passed, every run from the
// checkout crashed. A fresh repo has no conversation to continue.
const fixtureRepo = mkdtempSync(join(tmpdir(), 'devdeck-link-fixture-'));
const fixtureRepo = sandboxDir('devdeck-link-fixture-');
writeFileSync(join(fixtureRepo, 'README.md'), '# fixture');
execFileSync('git', ['init', '-q'], { cwd: fixtureRepo });
execFileSync('git', ['-c', 'user.email=qa@devdeck', '-c', 'user.name=qa', 'add', '-A'], { cwd: fixtureRepo });
Expand All @@ -34,12 +35,12 @@ execFileSync('git', ['-c', 'user.email=qa@devdeck', '-c', 'user.name=qa', 'commi
async function launch(tag, registerFolder, reuse = null) {
// A relaunch keeps its user-data directory: that is where the machine's identity, its pairings and
// its port live, and "the host came back" is only meaningful if it comes back as the same machine.
const userData = reuse?.userData ?? mkdtempSync(join(tmpdir(), `devdeck-rc-${tag}-`));
const userData = reuse?.userData ?? sandboxDir(`devdeck-rc-${tag}-`);
// Its own temp directory. Both instances run against one filesystem here, so a pasted image landing
// in the HOST's temp dir is the only thing that distinguishes "the bytes crossed the link and the
// machine running the agent wrote the file" from "the local paste ran and typed a path that machine
// cannot read" — which is the whole failure this feature exists to avoid, and it is silent.
const temp = reuse?.temp ?? mkdtempSync(join(tmpdir(), `devdeck-tmp-${tag}-`));
const temp = reuse?.temp ?? sandboxDir(`devdeck-tmp-${tag}-`);
const app = await electron.launch({
args: ['.', `--user-data-dir=${userData}`, '--no-sandbox', '--disable-gpu'],
cwd: repo,
Expand Down
3 changes: 2 additions & 1 deletion qa/perf-gate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { closeElectron } from './electron-lifecycle.mjs';
import { sandboxDir } from './sandbox.mjs';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const out = resolve(process.env.DEVDECK_PERF_OUT || join(root, 'qa', 'shots', 'perf-gate'));
Expand All @@ -41,7 +42,7 @@ const LIMITS = {
streamSeconds: 45, // the stream must finish drawing at all
};

const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-perf-gate-'));
const sandbox = sandboxDir('devdeck-perf-gate-');
const repo = join(sandbox, 'perf-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
3 changes: 2 additions & 1 deletion qa/perf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sandboxDir } from './sandbox.mjs';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const SESSIONS = Number(process.argv[2] ?? 4);
assert.ok(Number.isInteger(SESSIONS) && SESSIONS >= 1 && SESSIONS <= 16, 'request 1–16 sessions');
if (!process.argv.includes('--allow-live-agents')) throw new Error('Manual live-agent benchmark. Pass --allow-live-agents explicitly; do not run unattended.');
const IDLE_MS = 10_000;

const qaUserData = mkdtempSync(join(tmpdir(), 'devdeck-perf-'));
const qaUserData = sandboxDir('devdeck-perf-');
writeFileSync(join(qaUserData, 'state.json'), JSON.stringify({
projects: {}, settings: { folders: [{ path: root, kind: 'repo' }], viewMode: 'list', cockpitSessions: [] },
}, null, 2));
Expand Down
3 changes: 2 additions & 1 deletion qa/restore-pacing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { closeElectron } from './electron-lifecycle.mjs';
import { sandboxDir } from './sandbox.mjs';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
if (process.platform !== 'win32') { console.log('SKIP: Windows native PTY required'); process.exit(0); }
const sandbox = mkdtempSync(join(tmpdir(), 'devdeck-restore-pacing-'));
const sandbox = sandboxDir('devdeck-restore-pacing-');
const repo = join(sandbox, 'pacing-workspace');
const bin = join(sandbox, 'bin');
const profile = join(sandbox, 'profile');
Expand Down
24 changes: 24 additions & 0 deletions qa/sandbox.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Temp folders a harness run works in. Every run used to leave its own behind (about 5 GB after a
// few weeks). A passing run removes them on exit; a failing one keeps them and says where.
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const made = [];

/** A new folder in the OS temp directory, named `<prefix><random>`, removed when the run passes. */
export function sandboxDir(prefix) {
const dir = mkdtempSync(join(tmpdir(), prefix));
made.push(dir);
return dir;
}

process.on('exit', (code) => {
if (code) {
if (made.length) console.log('kept for inspection: ' + made.join(', '));
return;
}
for (const dir of made) {
try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* still held; temp is swept by the OS */ }
}
});
8 changes: 5 additions & 3 deletions src/main/diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { describe, it, expect } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { afterAll, describe, it, expect } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DiagnosticsLog, adoptLegacyErrorLog, formatDiagLine } from './diagnostics';

const dir = (): string => mkdtempSync(join(tmpdir(), 'devdeck-diag-'));
const made: string[] = [];
const dir = (): string => { const d = mkdtempSync(join(tmpdir(), 'devdeck-diag-')); made.push(d); return d; };
afterAll(() => { for (const d of made) rmSync(d, { recursive: true, force: true, maxRetries: 3 }); });
const at = new Date('2026-08-26T01:02:03.000Z');

describe('formatDiagLine', () => {
Expand Down
Loading