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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ different:

- compile/run, Monaco, and extension-runtime flows are supported
- Firefox 153+ uses WebAssembly JSPI for live, line-buffered `std::cin`,
`std::getline`, and `scanf` input; Firefox 140–152 (or a runtime without
JSPI) retains the pre-supplied buffered stdin fallback (up to 256 KiB)
`std::getline`, and `scanf` input; Firefox runtimes without worker-side JSPI
cannot run stdin-reading programs
- file open/save and folder import use fallback browser flows rather than
Chromium File System Access APIs
- persistent folder write-back and directory-handle session restore may be
Expand Down Expand Up @@ -464,7 +464,7 @@ manual/GitHub-distributed channel.
signed artifact under `release/firefox-unlisted/`.
5. Install the signed XPI in Firefox and complete the manual QA checklist
in `docs/firefox-stdin-runtime-acceptance.md`, paying special attention to
JSPI live stdin, its buffered fallback, and the documented
JSPI live stdin and the documented
workspace-persistence limitations.

### Manual release QA checklist
Expand Down Expand Up @@ -530,7 +530,7 @@ Copy the resulting `clang.js` and `clang.wasm` into `dist/clang/`.
the UI.
- **Browser scope**: Full parity targets desktop Chrome, Edge, Brave, and
Chromium. Firefox 153+ supports live canonical (line-buffered) stdin through
JSPI; older supported Firefox versions use buffered stdin. This is not a raw
JSPI; older or no-JSPI Firefox runtimes cannot provide terminal stdin. This is not a raw
POSIX PTY, and Firefox workspace-persistence limitations remain. Safari is
outside the current release target.
- **Managed browsers**: Enterprise policies that block File System Access prompts
Expand Down
25 changes: 10 additions & 15 deletions docs/firefox-stdin-runtime-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Use this procedure to validate live JSPI terminal input from GitHub issue #55
in a real Firefox extension context. `npm run test:browser:firefox` validates
packaging and manifest compatibility, but does not execute a compiled program.

Firefox 153+ should use live, canonical (line-buffered) input. Firefox 140–152,
or a runtime where JSPI is unavailable, should keep the pre-supplied buffered
fallback. Persistent folder write-back is outside this test and retains its
documented Firefox limitations.
Firefox 153+ with worker-side JSPI must use live, canonical (line-buffered)
input. A Firefox runtime where JSPI is unavailable must report stdin as
unsupported and must not open a pre-supplied-input dialog. Persistent folder
write-back is outside this test and retains its documented Firefox limitations.

## Setup

Expand Down Expand Up @@ -55,7 +55,7 @@ int main() {

1. Choose **Compile and Run**.
2. Confirm `Name? ` appears before entering anything and that the
**Pre-supplied stdin** dialog does not open.
pre-supplied-input dialog does not open.
3. Type `Ada`, press Enter, and confirm `Age? ` appears afterward.
4. Type `41` and press Enter.
5. Confirm the final line is `Hello Ada, next year 42` and the process exits
Expand All @@ -72,19 +72,14 @@ This validates two independent suspend/resume cycles and prompt ordering.
3. Run once more and complete both inputs successfully. Confirm no input from
either prior run appears in the new process.

## Buffered fallback test
## Unsupported-runtime test

Repeat in Firefox 140–152, or in a controlled environment where the worker does
not expose both `WebAssembly.Suspending` and `WebAssembly.promising`.

1. Choose **Compile and Run**.
2. Confirm the **Pre-supplied stdin** dialog opens.
3. Enter `Ada`, a newline, `41`, and a final newline; then choose **Run
program**.
4. Confirm the same successful final output.

The fallback must not attempt message-interactive stdin merely from the Firefox
version string or main-window capabilities.
2. Confirm no pre-supplied-input dialog opens.
3. Confirm the terminal reports that live stdin is unavailable and posts no run.

## Error exclusions

Expand All @@ -107,6 +102,6 @@ Record the following in the pull request or follow-up release evidence:
- exact observed prompt/output ordering
- confirmation that each input was entered only after its prompt appeared
- Ctrl+D, Ctrl+C, and clean-rerun results
- confirmation that the buffered dialog was absent on Firefox 153+ JSPI and
present in the no-JSPI fallback test
- confirmation that no pre-supplied-input dialog exists and no-JSPI reports
unsupported stdin
- confirmation that stderr and the Browser Console had no unexpected errors
2 changes: 1 addition & 1 deletion docs/release-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ real Firefox desktop build:
console/runtime errors.
3. Compile and run the default sample program.
4. Complete `docs/firefox-stdin-runtime-acceptance.md`: confirm Firefox 153+
JSPI live stdin and the older/no-JSPI buffered fallback both work without
JSPI live stdin works without
SharedArrayBuffer/COOP/COEP errors.
5. Open a local source file with Firefox's fallback picker and save changes.
6. Import a folder, compile a multi-file project, and confirm diagnostics appear
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint .",
"build": "npm run build:webpack && npm run build:targets",
"build:firefox": "npm run build",
"test:e2e": "node --experimental-detect-module --test scripts/e2e-session-persistence.test.mjs scripts/e2e-session-restore-choice.test.mjs scripts/e2e-multifile-build.test.mjs scripts/e2e-workspace-file-tracking.test.mjs scripts/e2e-terminal-mkdir.test.mjs scripts/e2e-terminal-stop.test.mjs scripts/e2e-terminal-git-removal.test.mjs scripts/e2e-terminal-stop-icon.test.mjs scripts/e2e-buffered-stdin-ui.test.mjs scripts/e2e-browser-compatibility.test.mjs scripts/e2e-firefox-compatibility.test.mjs scripts/e2e-firefox-jspi-stdin.test.mjs scripts/e2e-wasi-shim.test.mjs scripts/e2e-run-request.test.mjs scripts/e2e-release-packaging.test.mjs",
"test:e2e": "node --experimental-detect-module --test scripts/e2e-session-persistence.test.mjs scripts/e2e-session-restore-choice.test.mjs scripts/e2e-multifile-build.test.mjs scripts/e2e-workspace-file-tracking.test.mjs scripts/e2e-terminal-mkdir.test.mjs scripts/e2e-terminal-stop.test.mjs scripts/e2e-terminal-git-removal.test.mjs scripts/e2e-terminal-stop-icon.test.mjs scripts/e2e-browser-compatibility.test.mjs scripts/e2e-firefox-compatibility.test.mjs scripts/e2e-firefox-jspi-stdin.test.mjs scripts/e2e-wasi-shim.test.mjs scripts/e2e-run-request.test.mjs scripts/e2e-release-packaging.test.mjs",
"test:preflight-clang": "node scripts/preflight-clang-artifacts.js",
"test:browser:chrome": "npm run test:preflight-clang && node scripts/smoke-browser.mjs chrome",
"test:browser:edge": "npm run test:preflight-clang && node scripts/smoke-browser.mjs edge",
Expand Down
25 changes: 0 additions & 25 deletions scripts/e2e-buffered-stdin-ui.test.mjs

This file was deleted.

6 changes: 3 additions & 3 deletions scripts/e2e-firefox-compatibility.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@ test('e2e: extracts Firefox major versions from Firefox user agents', () => {
assert.equal(getFirefoxMajor('Chrome/126.0.0.0 Safari/537.36'), null);
});

test('e2e: Firefox compatibility report accepts supported fallback-based capabilities', () => {
test('e2e: Firefox compatibility report identifies live stdin as unsupported without JSPI', () => {
const report = createBrowserCompatibilityReport(firefoxLikeRoot());
const message = formatBrowserCompatibilityMessage(report);

assert.equal(report.ok, true);
assert.equal(report.capabilities.browserFamily, 'firefox');
assert.equal(report.capabilities.supportLevel, 'compatible');
assert.equal(report.capabilities.interactiveStdin, false);
assert.equal(report.capabilities.stdinMode, 'buffered');
assert.equal(report.capabilities.stdinMode, 'unsupported');
assert.deepEqual(report.missing, []);
assert.ok(report.limitations.some((item) => item.key === 'limitedInteractiveStdin'));
assert.ok(report.limitations.some((item) => item.key === 'firefoxWorkspacePersistence'));
assert.ok(message.includes('pre-supplied buffered stdin'));
assert.ok(message.includes('requires worker-side JSPI'));
assert.ok(message.includes('Persistent folder write-back'));
});

Expand Down
6 changes: 3 additions & 3 deletions scripts/e2e-firefox-jspi-stdin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ test('e2e: Firefox 153 reports JSPI potential but waits for worker confirmation'
assert.equal(capabilities.jspi, true);
assert.equal(capabilities.jspiPotentialInteractiveStdin, true);
assert.equal(capabilities.interactiveStdin, false);
assert.equal(capabilities.stdinMode, 'buffered');
assert.equal(selectStdinTransport(capabilities, { jspi: false }), 'buffered');
assert.equal(capabilities.stdinMode, 'unsupported');
assert.equal(selectStdinTransport(capabilities, { jspi: false }), 'unsupported');
assert.equal(selectStdinTransport(capabilities, { jspi: true }), 'message-jspi');

const negotiatedReport = createBrowserCompatibilityReport({
Expand Down Expand Up @@ -142,7 +142,7 @@ test('e2e: stdin selection keeps Chromium SharedArrayBuffer first and gates olde
firefoxMajor: 152,
sharedBufferInteractiveStdin: false,
};
assert.equal(selectStdinTransport(firefox152, { jspi: true }), 'buffered');
assert.equal(selectStdinTransport(firefox152, { jspi: true }), 'unsupported');
});

test('e2e: stdin session routing ignores late messages from previous runs', () => {
Expand Down
19 changes: 3 additions & 16 deletions scripts/e2e-run-request.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import test from 'node:test';
import assert from 'node:assert/strict';

import {
BUFFERED_STDIN_MAX_BYTES,
INTERACTIVE_STDIN_CHUNK_MAX_BYTES,
validateRunRequest,
validateStdinMessage,
Expand Down Expand Up @@ -35,18 +34,6 @@ test('e2e: worker run contract accepts and normalizes all stdin modes', () => {
sessionId: 'stdin-session-1',
});

const buffered = validateRunRequest(interactiveRequest({
stdinMode: 'buffered',
sharedBuffer: undefined,
stdinBuffer: new TextEncoder().encode('Ada\n41\n').buffer,
}));
assert.equal(buffered.ok, true);
assert.equal(buffered.value.stdin.mode, 'buffered');
assert.deepEqual(
[...buffered.value.stdin.bytes],
[...new TextEncoder().encode('Ada\n41\n')]
);

const none = validateRunRequest(interactiveRequest({
stdinMode: 'none',
sharedBuffer: undefined,
Expand Down Expand Up @@ -118,15 +105,15 @@ test('e2e: worker stdin message contract rejects malformed and oversized input',
}
});

test('e2e: worker run contract rejects buffered stdin above the shared size limit', () => {
test('e2e: worker run contract rejects buffered stdin entirely', () => {
const result = validateRunRequest(interactiveRequest({
stdinMode: 'buffered',
sharedBuffer: undefined,
stdinBuffer: new Uint8Array(BUFFERED_STDIN_MAX_BYTES + 1),
stdinBuffer: new Uint8Array([1]),
}));

assert.equal(result.ok, false);
assert.match(result.error, /256 KiB/);
assert.match(result.error, /stdin/i);
});

test('e2e: worker run contract rejects invalid binary and VFS byte fields', () => {
Expand Down
69 changes: 4 additions & 65 deletions scripts/e2e-terminal-stop.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
function setupTerminalHarness({
supportsInteractiveStdin = true,
supportsMessageInteractiveStdin = false,
requestBufferedStdin = async () => '',
onRun,
} = {}) {
const writes = [];
Expand All @@ -40,7 +39,6 @@ function setupTerminalHarness({
onRunPreparationStateChange: (preparing) => runPreparationChanges.push(preparing),
supportsInteractiveStdin: () => supportsInteractiveStdin,
supportsMessageInteractiveStdin: () => supportsMessageInteractiveStdin,
requestBufferedStdin,
onStdinData: (message) => stdinMessages.push(message),
onStdinEOF: (message) => stdinMessages.push(message),
createStdinSessionId: () => 'stdin-session-test',
Expand Down Expand Up @@ -152,37 +150,23 @@ test('e2e: normal run completion reports not-running state', async () => {
assert.equal(__getTerminalStateForTesting().running, false);
});

test('e2e: non-SAB run posts UTF-8 buffered stdin before entering running state', async () => {
test('e2e: run without a live stdin transport reports unsupported and posts no request', async () => {
const ctx = setupTerminalHarness({
supportsInteractiveStdin: false,
requestBufferedStdin: async () => 'Grüße\n',
});

assert.equal(await startRun(), true);

assert.equal(ctx.runCalls.length, 1);
assert.equal(ctx.runCalls[0].stdinMode, 'buffered');
assert.deepEqual(
[...ctx.runCalls[0].stdinBuffer],
[...new TextEncoder().encode('Grüße\n')]
);
assert.equal(await startRun(), false);
assert.deepEqual(ctx.runCalls, []);
assert.deepEqual(ctx.runStateChanges, []);
assert.deepEqual(ctx.runPreparationChanges, [true]);
assert.equal(__getTerminalStateForTesting().preparingRun, true);

onRunStart({ stdinMode: 'buffered' });
assert.deepEqual(ctx.runStateChanges, [true]);
assert.deepEqual(ctx.runPreparationChanges, [true, false]);
assert.equal(__getTerminalStateForTesting().preparingRun, false);
assert.ok(ctx.writes.join('').includes('Live terminal stdin is unavailable'));
});

test('e2e: Firefox JSPI run forwards live terminal lines and EOF by session', async () => {
const ctx = setupTerminalHarness({
supportsInteractiveStdin: false,
supportsMessageInteractiveStdin: true,
requestBufferedStdin: async () => {
throw new Error('buffered input should not be requested');
},
});

assert.equal(await startRun(), true);
Expand Down Expand Up @@ -220,37 +204,6 @@ test('e2e: Firefox JSPI run forwards live terminal lines and EOF by session', as
});
});

test('e2e: canceling buffered stdin restores idle state and posts no run request', async () => {
const ctx = setupTerminalHarness({
supportsInteractiveStdin: false,
requestBufferedStdin: async () => null,
});

assert.equal(await startRun(), false);

assert.deepEqual(ctx.runCalls, []);
assert.deepEqual(ctx.runPreparationChanges, [true, false]);
assert.equal(__getTerminalStateForTesting().preparingRun, false);
assert.equal(__getTerminalStateForTesting().running, false);
});

test('e2e: duplicate run while buffered input is pending posts only one request', async () => {
let resolveInput;
const pendingInput = new Promise((resolve) => { resolveInput = resolve; });
const ctx = setupTerminalHarness({
supportsInteractiveStdin: false,
requestBufferedStdin: () => pendingInput,
});

const firstRun = startRun();
assert.equal(await startRun(), false);
resolveInput('Ada\n');
assert.equal(await firstRun, true);

assert.equal(ctx.runCalls.length, 1);
assert.equal(ctx.runCalls[0].stdinMode, 'buffered');
});

test('e2e: run callback failure restores idle state without run-start', async () => {
const writes = [];
__setTerminalTestHarness({
Expand All @@ -267,17 +220,3 @@ test('e2e: run callback failure restores idle state without run-start', async ()
assert.equal(state.running, false);
assert.ok(writes.join('').includes('Could not start program'));
});

test('e2e: oversized buffered stdin restores idle state and posts no run request', async () => {
const ctx = setupTerminalHarness({
supportsInteractiveStdin: false,
requestBufferedStdin: async () => 'x'.repeat((256 * 1024) + 1),
});

assert.equal(await startRun(), false);

assert.deepEqual(ctx.runCalls, []);
assert.deepEqual(ctx.runPreparationChanges, [true, false]);
assert.equal(__getTerminalStateForTesting().preparingRun, false);
assert.ok(ctx.writes.join('').includes('256 KiB'));
});
Loading
Loading