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
28 changes: 28 additions & 0 deletions scripts/e2e-multifile-build.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseGxxArgs,
resolveWorkspacePath,
resolveRunTarget,
selectRunBinaryBytes,
selectWorkspaceSources,
buildCompileOverlay,
isProjectSource,
Expand Down Expand Up @@ -137,6 +138,33 @@ test('e2e: ./name before any successful build reports no binary', () => {
assert.deepEqual(resolveRunTarget('./a.out', null), { ok: false, error: 'no-binary' });
});

test('e2e: a restored workspace binary is runnable without build-session state', () => {
assert.deepEqual(
resolveRunTarget('build/app', null, ['a.out', 'build/app']),
{ ok: true, path: 'build/app', source: 'workspace' }
);
assert.deepEqual(
resolveRunTarget('missing', null, ['a.out', 'build/app']),
{ ok: false, error: 'not-found' }
);
});

test('e2e: explicit restored target bytes override a stale cached binary', () => {
const restored = new Uint8Array([0, 97, 115, 109]);
const stale = new Uint8Array([1, 2, 3]);
const selected = selectRunBinaryBytes(
{ artifactPath: 'build/app' },
[{ path: 'build/app', bytes: restored }],
stale
);

assert.deepEqual(selected, restored);
assert.equal(
selectRunBinaryBytes({ artifactPath: 'missing' }, [{ path: 'build/app', bytes: restored }], stale),
null
);
});

// ── Compile & Run uses the worker-reported output path ────────────────────────

test('e2e: Compile & Run runs the worker output path rather than assuming a.out', () => {
Expand Down
18 changes: 18 additions & 0 deletions scripts/e2e-terminal-git-removal.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';

import {
__executeTerminalCommandForTesting,
__handleTerminalKeyForTesting,
__setTerminalTestHarness,
setWorkspace,
Expand Down Expand Up @@ -99,3 +100,20 @@ test('e2e: folders containing .git remain ordinary workspace content', () => {
assert.ok(output.includes('.git/'));
assert.ok(output.includes('main.cpp'));
});

test('e2e: terminal runs a binary restored from the workspace', async () => {
const writes = [];
const runCalls = [];
__setTerminalTestHarness({
term: { clear() {}, write(text) { writes.push(text); } },
onRun: (request) => runCalls.push(request),
});
setWorkspace({ name: 'project', entries: [{ path: 'build/app', kind: 'file' }] });

await __executeTerminalCommandForTesting('cd build');
await __executeTerminalCommandForTesting('./app');

assert.equal(runCalls.length, 1);
assert.equal(runCalls[0].artifactPath, 'build/app');
assert.ok(!writes.join('').includes('No binary found'));
});
9 changes: 8 additions & 1 deletion src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { createSessionPersistence, createPersistenceGate } from './session-persistence.mjs';
import { registerPageUnload } from './page-lifecycle.mjs';
import { getExtensionVersionLabel } from '../extension-api.mjs';
import { selectRunBinaryBytes } from './build-request.mjs';

// ── Boot ──────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -67,7 +68,13 @@ window.addEventListener('DOMContentLoaded', async () => {
},
onRun: async (runRequest) => {
const vfsFiles = await fsAPI.readAllWorkspaceFiles();
const binaryBytes = toolbarController?.getLastRunBinaryBytes?.() || null;
const cachedBinaryBytes = toolbarController?.getLastRunBinaryBytes?.() || null;
const binaryBytes = selectRunBinaryBytes(runRequest, vfsFiles, cachedBinaryBytes);
if (runRequest.artifactPath) {
if (!binaryBytes) {
throw new Error(`Could not read binary: ${runRequest.artifactPath}`);
}
}
worker.postMessage({ type: 'run', ...runRequest, vfsFiles, binaryBytes });
},
onStdinData: (message) => worker.postMessage(message),
Expand Down
38 changes: 33 additions & 5 deletions src/ui/build-request.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,45 @@ export function parseGxxArgs(args = []) {
*
* @param {string} command – e.g. './a.out' or './custom-name'
* @param {string|null} lastBuiltArtifactPath
* @returns {{ ok:boolean, error?:string }}
* @param {string[]} workspaceFilePaths
* @returns {{ ok:boolean, path?:string, source?:string, error?:string }}
*/
export function resolveRunTarget(command, lastBuiltArtifactPath) {
export function resolveRunTarget(command, lastBuiltArtifactPath, workspaceFilePaths = []) {
const requested = normalizeOverlayPath(String(command || '').replace(/^\.\//, ''));
const workspaceFiles = new Set(
workspaceFilePaths.map((path) => normalizeOverlayPath(path)).filter(Boolean)
);

if (workspaceFiles.has(requested)) {
return { ok: true, path: requested, source: 'workspace' };
}

if (!lastBuiltArtifactPath) {
return { ok: false, error: 'no-binary' };
return workspaceFiles.size > 0
? { ok: false, error: 'not-found' }
: { ok: false, error: 'no-binary' };
}
const requested = normalizeOverlayPath(String(command || '').replace(/^\.\//, ''));

const artifact = normalizeOverlayPath(lastBuiltArtifactPath);
const artifactBase = artifact.split('/').pop();
if (requested === artifact || requested === artifactBase) {
return { ok: true };
return { ok: true, path: artifact, source: 'last-built' };
}
return { ok: false, error: 'not-found' };
}

/**
* Select bytes for a worker run request. Explicit terminal targets always use
* their matching workspace file so an earlier in-memory compile cannot run by
* mistake. Toolbar runs have no target and retain the cached artifact path.
*
* @param {{artifactPath?:string}} request
* @param {Array<{path:string, bytes:Uint8Array}>} workspaceFiles
* @param {Uint8Array|null} cachedBinaryBytes
* @returns {Uint8Array|null}
*/
export function selectRunBinaryBytes(request, workspaceFiles = [], cachedBinaryBytes = null) {
if (!request?.artifactPath) return cachedBinaryBytes;
const artifact = workspaceFiles.find((file) => file.path === request.artifactPath);
return artifact ? new Uint8Array(artifact.bytes) : null;
}
15 changes: 10 additions & 5 deletions src/ui/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,10 +404,10 @@ export function setWorkerCapabilities(capabilities = {}) {
*
* @returns {Promise<boolean>} whether a valid worker run request was posted
*/
export async function startRun() {
export async function startRun({ artifactPath = null } = {}) {
if (!term || running || preparingRun) return false;

if (!lastBuiltArtifactPath) {
if (!artifactPath && !lastBuiltArtifactPath) {
term.write(`${C.red}No binary found. Compile first with: g++ main.cpp${C.reset}${CRLF}`);
writePrompt();
return false;
Expand Down Expand Up @@ -438,7 +438,7 @@ export async function startRun() {

if (!_onRun) throw new Error('Run callback is unavailable.');
term.write(CRLF);
await _onRun(request);
await _onRun(artifactPath ? { ...request, artifactPath } : request);
return true;
} catch (error) {
setRunPreparationState(false);
Expand Down Expand Up @@ -852,7 +852,12 @@ function cmdGxx(args) {
}

async function cmdRun(cmd) {
const { ok, error } = resolveRunTarget(cmd, lastBuiltArtifactPath);
const requestedPath = resolveWorkspacePath(workspaceCwd, cmd);
const { ok, path: artifactPath, source, error } = resolveRunTarget(
requestedPath,
lastBuiltArtifactPath,
[...workspaceFiles].map(normalizePath)
);
if (!ok) {
if (error === 'no-binary') {
term.write(`${C.red}No binary found. Compile first with: g++ main.cpp${C.reset}${CRLF}`);
Expand All @@ -862,7 +867,7 @@ async function cmdRun(cmd) {
writePrompt();
return;
}
await startRun();
await startRun({ artifactPath: source === 'workspace' ? artifactPath : null });
}

function cmdLs(args = []) {
Expand Down
Loading