From 5841b12bc94ecd1027e507e4b741e09fcb0a093d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 23:19:03 -0400 Subject: [PATCH] test(ssh): add private-console Windows diagnostic Co-authored-by: Orca --- .../build-windows-ssh-no-input-launcher.mjs | 3 +- ...runtime-windows-no-input-launcher.test.mjs | 70 ++++- ...elease-implementation-checklist-summary.md | 27 +- ...github-release-implementation-checklist.md | 110 ++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 18 +- .../WindowsConsoleEofProbe.cs | 65 +++++ .../WindowsPrivateConsoleInput.cs | 251 ++++++++++++++++++ ...onymousPipeSet.cs => WindowsSshChildIo.cs} | 46 ++-- .../WindowsSshChildProcess.cs | 16 +- ...sh-system-no-input-windows-openssh.test.ts | 16 +- 10 files changed, 550 insertions(+), 72 deletions(-) create mode 100644 native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs create mode 100644 native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs rename native/windows-ssh-no-input-launcher/{WindowsAnonymousPipeSet.cs => WindowsSshChildIo.cs} (86%) diff --git a/config/scripts/build-windows-ssh-no-input-launcher.mjs b/config/scripts/build-windows-ssh-no-input-launcher.mjs index aa0e76d3af2..2e8b40bd1f6 100644 --- a/config/scripts/build-windows-ssh-no-input-launcher.mjs +++ b/config/scripts/build-windows-ssh-no-input-launcher.mjs @@ -12,8 +12,9 @@ const repoRoot = resolve(import.meta.dirname, '../..') const sourceRoot = join(repoRoot, 'native', 'windows-ssh-no-input-launcher') const sourcePaths = [ 'OrcaSshNoInputLauncher.cs', - 'WindowsAnonymousPipeSet.cs', 'WindowsCommandLine.cs', + 'WindowsPrivateConsoleInput.cs', + 'WindowsSshChildIo.cs', 'WindowsSshChildProcess.cs' ].map((name) => join(sourceRoot, name)) const outputPath = readArg('--output') ?? join(sourceRoot, '.build', 'orca-ssh-no-input.exe') diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs index 4a6006c4437..d4d26fa5319 100644 --- a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs @@ -19,6 +19,7 @@ let fixtureRoot let launcherPath let childFixturePath let handleProbePath +let consoleEofProbePath beforeAll(() => { if (process.platform !== 'win32') { @@ -28,6 +29,7 @@ beforeAll(() => { launcherPath = join(fixtureRoot, 'orca-ssh-no-input.exe') childFixturePath = join(fixtureRoot, 'child-fixture.cjs') handleProbePath = join(fixtureRoot, 'windows-launcher-handle-probe.exe') + consoleEofProbePath = join(fixtureRoot, 'windows-console-eof-probe.exe') writeFileSync(childFixturePath, childFixtureSource, 'utf8') const build = spawnSync(process.execPath, [buildScript, '--output', launcherPath], { cwd: projectRoot, @@ -57,6 +59,27 @@ beforeAll(() => { { cwd: projectRoot, encoding: 'utf8' } ) expect(probeBuild.status, `${probeBuild.stdout}\n${probeBuild.stderr}`).toBe(0) + + const eofProbeSource = join( + projectRoot, + 'native', + 'windows-ssh-no-input-launcher-test', + 'WindowsConsoleEofProbe.cs' + ) + const eofProbeBuild = spawnSync( + compilerPath, + [ + '/nologo', + '/target:exe', + '/platform:anycpu', + '/optimize+', + '/warnaserror+', + `/out:${consoleEofProbePath}`, + eofProbeSource + ], + { cwd: projectRoot, encoding: 'utf8' } + ) + expect(eofProbeBuild.status, `${eofProbeBuild.stdout}\n${eofProbeBuild.stderr}`).toBe(0) }, 20_000) afterAll(() => { @@ -68,16 +91,28 @@ afterAll(() => { describe('Windows SSH no-input launcher', () => { it('keeps the artifact source scoped to the reviewed Win32 boundary', () => { const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8') - const pipeSource = readFileSync(join(sourceRoot, 'WindowsAnonymousPipeSet.cs'), 'utf8') + const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8') + const pipeSource = readFileSync(join(sourceRoot, 'WindowsSshChildIo.cs'), 'utf8') expect(processSource).toContain('ProcThreadAttributeHandleList') expect(processSource).toContain('IntPtr.Size * 3') expect(processSource).toContain('CreateSuspended') - expect(processSource).toContain('CreateNoWindow') + expect(processSource).not.toContain('CreateNoWindow') expect(processSource).toContain('JobObjectLimitKillOnJobClose') expect(processSource).toContain('WaitForMultipleObjects') expect(processSource).toContain('if (processStarted && !assignedToJob)') - expect(pipeSource).toContain('CloseHandleChecked(stdinWrite)') + expect(consoleSource).toContain('AllocConsole()') + expect(consoleSource).toContain('"CONIN$"') + expect(consoleSource).toContain('ShowWindow(window, SwHide)') + expect(consoleSource).toContain('WriteConsoleInput') + expect(consoleSource).toContain('console.QueueEndOfInput()') + expect(processSource).toContain('WindowsPrivateConsoleInput.Create()') + expect(consoleSource.indexOf('console.QueueEndOfInput()')).toBeLessThan( + consoleSource.indexOf('return console;') + ) + expect(processSource.indexOf('WindowsPrivateConsoleInput.Create()')).toBeLessThan( + processSource.indexOf('CreateProcess(') + ) expect(pipeSource).toContain('PumpCompletionTimeoutMilliseconds') expect(pipeSource).toContain('pumpFailureSignal.Set()') }) @@ -95,14 +130,29 @@ describe('Windows SSH no-input launcher', () => { } ) - itWindows('preserves exact arguments and provides child stdin EOF before execution', () => { + itWindows('preserves exact arguments and qualifies private-console stdin', () => { const args = ['', 'space value', 'quote"value', 'trailing slash\\', 'line one\nline two', '雪'] const result = runLauncher('arguments-and-stdin', args, { input: Buffer.from('this input must not reach the SSH child', 'utf8') }) expect(result.status, result.stderr.toString('utf8')).toBe(0) - expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ args, stdinBytes: 0 }) + expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ + args, + stdinIsTTY: true + }) + }) + + itWindows('queues cooked private-console EOF before child execution', () => { + const result = spawnSync(launcherPath, [consoleEofProbePath], { + cwd: fixtureRoot, + encoding: 'utf8', + timeout: 10_000, + windowsHide: true + }) + + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toBe('ORCA-PRIVATE-CONSOLE-EOF bytes=0') }) itWindows('preserves binary stdout and stderr and propagates the child exit code', () => { @@ -219,12 +269,10 @@ async function waitForProcessExit(pid, timeoutMs) { const childFixtureSource = String.raw`const mode = process.argv[2] if (mode === 'arguments-and-stdin') { - let stdinBytes = 0 - process.stdin.on('data', (chunk) => { stdinBytes += chunk.length }) - process.stdin.on('end', () => { - process.stdout.write(JSON.stringify({ args: process.argv.slice(3), stdinBytes })) - }) - process.stdin.resume() + process.stdout.write(JSON.stringify({ + args: process.argv.slice(3), + stdinIsTTY: process.stdin.isTTY === true + })) } else if (mode === 'binary-output') { process.stdout.write(Buffer.from([0x00, 0xff, 0x10, 0x0a, 0x80])) process.stderr.write(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a])) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index 4a3ae4325c6..80118bfb76b 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -1,6 +1,6 @@ # SSH Relay Runtime Distribution — Short Implementation Checklist -Last updated: 2026-07-15 +Last updated: 2026-07-16 Use this file to track the project. The [detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md) @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -launcher compiler correction, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private- +console Windows diagnostic, 2026-07-16, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -168,7 +168,26 @@ ARM64 live evidence next. Do not package or call it from production; SignPath re `29550660572`, and independent x64/ARM64 jobs `87792250731`/`87792250710`: both legacy compilers reject one constant/structure identifier collision before emitting an executable. The isolated correction renames only the integer information-class constant; focused 12/12 and release contracts -285/285 pass locally. Commit/push and rerun both architectures next. +285/285 pass locally. Corrected exact head `6a716de14`, run `29551003702`, and x64/ARM64 jobs +`87793308016`/`87793308044` then pass legacy compilation and every native launcher contract plus the +complete 101-file/859-case artifact contract step, but the live diagnostic still times out after +8024.0679/8031.6544 milliseconds. Both observe ended stdout, a closed SSH channel, no sentinel or +process exit, and zero stderr; both exact profile teardowns remain RED on locked `UsrClass.dat`. +`E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001` therefore falsifies Node's pipe type and broad +inherited-handle leakage. Next test only a hidden launcher-owned real-console stdin with console EOF +queued before child creation while retaining private output pipes, exact handle inheritance, job +ownership, and bounded cancellation. Keep it runner-temp-only and disconnected from product; +SignPath remains deferred. +`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the replacement candidate locally green. It +captures launcher output before detaching, allocates and hides a private console, queues Ctrl+Z plus +Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned SSH +child while retaining private output pipes and the exact three-handle list. A dedicated native probe +requires character-device stdin, successful `GetConsoleMode`, and a zero-byte cooked `ReadFile`. +Focused 119/119 with seven native skips, all 285 release contracts with five native skips, typecheck, +full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Commit/push +this artifact-only package and require fresh x64/ARM64 compiler, console/EOF, launcher, live SSH, and +exact teardown proof; no product, Beta, fallback, tuple, publication, default, or SignPath work is +included. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index c6d688faf93..f7376f6f840 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -7,9 +7,9 @@ This file is the detailed evidence ledger. Use the tracker above for project sta work; keep exact commands, runner identities, hashes, metrics, and residual gaps here. Date created: 2026-07-14
-Last updated: 2026-07-15
-Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows launcher compiler correction**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001` records exact head `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, run `29550660572`, and the same `CS0102` identifier collision from Windows x64/ARM64 jobs `87792250731`/`87792250710`. Rename only the integer job-information-class constant, re-prove local contracts, and require fresh native x64/ARM64 evidence. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
-Session checkpoint: **LAUNCHER LOCAL CONTRACTS GREEN / X64 AND ARM64 LEGACY COMPILERS RED / ONE-IDENTIFIER CORRECTION ACTIVE — 2026-07-15, Codex implementation owner** — local focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. Both native jobs independently reject the constant/structure name collision before producing an executable or reaching live SSH; the runner-temp build, live fixture, full-size test, upload, production caller, legacy behavior, Beta, tuple publication, and SignPath remain untouched. Rename only `JobObjectExtendedLimitInformation` constant to `JobObjectExtendedLimitInformationClass`, commit/push, and rerun both native architectures.
+Last updated: 2026-07-16
+Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-16, Codex implementation owner: disconnected private-console Windows diagnostic native proof**. `E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the unpackaged, unsigned, runner-temp diagnostic locally green. Commit/push only this artifact-only package and require fresh Windows x64/ARM64 legacy-compiler, real-console qualification, pre-execution cooked EOF, argv/binary/exit/handle-exclusion/cancellation, live lifecycle, and exact teardown proof. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
+Session checkpoint: **PRIVATE CONSOLE CONTRACTS LOCALLY GREEN / X64 AND ARM64 NATIVE PROOF REQUIRED — 2026-07-16, Codex implementation owner** — the launcher captures its caller output streams, detaches from any inherited console, allocates and hides a private console, queues Ctrl+Z plus Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned child while retaining private stdout/stderr pipes and the exact three-handle list. A dedicated native probe requires character-device stdin, successful `GetConsoleMode`, and zero-byte cooked `ReadFile`. Local focused 119/119 with seven native skips, release contracts 285/285 with five native skips, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, full-size transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -19627,8 +19627,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64 job `87788106340` on `windows-2022`, and Windows ARM64 job `87788106351` on `windows-11-arm`. - Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 15 - 'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile - teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each job. - Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`, `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, `stderrBytes=0`, @@ -19678,15 +19678,15 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de references in that step. Runtime archives, release evidence, product imports, production system- SSH arguments/handles, legacy fallback/default, Beta, tuple publication, and signing are unchanged. - Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 cases with six declared native +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 cases with six declared native skips in 4.73 seconds real; maximum RSS is 167,936,000 bytes and peak footprint is 98,735,864 bytes. - Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285/285 runnable cases with four declared Windows skips in 25.07 seconds real; maximum RSS is 195,428,352 bytes and peak footprint is 97,638,160 bytes. One preceding quoted-glob invocation selected no files and exited 1; it is recorded as operator error and is not evidence. @@ -19709,7 +19709,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, artifact run `29550660572`, Windows x64 job `87792250731` on `windows-2022`, and Windows ARM64 job `87792250710` on `windows-11-arm`. - Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 8 - 'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'` +'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'` for each job. - Native result: both architectures reach the purpose-named contract suite and the .NET Framework compiler independently returns `CS0102`: the integer job-information-class constant and the @@ -19729,6 +19729,84 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de declared Windows skips. Syntax, `git diff --check`, and protected-resolver zero-diff pass. Commit/ push the correction and require both native architectures from the corrected exact head. +### E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001 — private redirected pipes do not settle Win32-OpenSSH + +- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head + `6a716de14884f9c08c1e08140f87849f137622e2`, artifact run `29551003702`, Windows x64 job + `87793308016` on `windows-2022`, and Windows ARM64 job `87793308044` on `windows-11-arm`. + GitHub records the jobs from 2026-07-17 02:53Z through 03:03Z. +- Native launcher proof: both architectures compile with the legacy .NET Framework compiler and + pass exact argv quoting, pre-execution stdin EOF, binary stdout/stderr, exit-code propagation, + unrelated inherited-handle exclusion, job-owned cancellation, and bounded settlement. Each full + artifact contract step passes 101 files, 859 runnable cases, and 19 declared skips. Both jobs + build and verify the unchanged full runtime before starting the disconnected live fixture. +- Live x64 result: `elapsedMs=8024.0679`, `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and + `stderrBytes=0`. +- Live ARM64 result: `elapsedMs=8031.6544` with the same remaining lifecycle fields. Both official + server logs prove authentication and remote command-session closure. The launcher remains alive + until the bounded diagnostic terminates it, so this is not transfer or enablement evidence. +- Teardown result: both exact owned-profile teardowns remain RED because `UsrClass.dat` is locked. + The workflow correctly stops before the unchanged product full-size Windows transfer and upload. + All non-Windows primary jobs and both Linux oldest-userland supplements pass in the same run. +- Interpretation: Node's Windows pipe type and broad inherited-handle leakage are falsified as the + cause. Win32-OpenSSH issue #1330 separately reports the same hang when all standard handles are + redirected, including cases where `-n`/NUL and `-T` do not help, while a real console stdin + behaves differently. Do not add sentinel-based forced success or kill-after-output: trustworthy + remote exit-status framing would be a separate reviewed protocol contract. +- Next bounded diagnostic: allocate a launcher-owned private console, hide it, open `CONIN$`, queue + console EOF input before `CreateProcessW`, and share that console with the SSH child. Retain + private stdout/stderr pipes, the exact three-handle inheritance list, suspended job assignment, + bounded pumps, exit propagation, and cancellation ownership. First prove the precise console EOF + semantics with a native purpose-named probe, then require the live x64 and ARM64 lifecycle and + exact teardown. Keep the executable runner-temp-only and disconnected from product packaging. + +### E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001 — private real-console candidate is locally contract-green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted capability atop exact + private-pipe RED head `6a716de14884f9c08c1e08140f87849f137622e2` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Source audit: PowerShell/openssh-portable exact source `e98b11e5de035cbd0eecbddc95a87da42f14671f` + initializes Win32 standard handles as synchronous external I/O, classifies console input as + `FILE_TYPE_CHAR`, and routes it through the dedicated synchronous console `ReadFile` path in + `contrib/win32/win32compat/w32fd.c`, `fileio.c`, and `termio.c`. Orca's existing `-T` in + `system-ssh-args.ts` prevents real-console stdin from requesting a remote PTY. +- Artifact boundary: the launcher captures its caller's stdout/stderr streams before detaching from + any inherited console, allocates a private console, hides any console window, opens inheritable + `CONIN$`, selects processed line input, flushes it, and queues Ctrl+Z key-down/up plus Enter key- + down/up before `CreateProcessW`. The child shares that private console; `CREATE_NO_WINDOW` is + deliberately absent because it would detach the child from the console. Private anonymous stdout/ + stderr pipes, exact three-handle inheritance, suspended job assignment, bounded pumps, exit + propagation, and kill-on-job-close cancellation remain intact. +- Native contract design: a purpose-named legacy-compiler probe rejects non-character stdin or + failed `GetConsoleMode`, selects cooked input, directly calls `ReadFile`, and requires zero bytes. + The existing suite separately requires console-qualified stdin plus exact argv, binary output, + exit propagation, unrelated inherited-handle exclusion, and bounded job-owned cancellation. + Five Windows-native cases are declared skips locally; neither the console semantics nor the live + OpenSSH result is claimed without x64 and ARM64 runners. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with seven declared + native/live skips in 3.27 seconds real; maximum RSS is 171,180,032 bytes and peak footprint is + 96,999,352 bytes. +- Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +--reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285/285 runnable + cases with five declared Windows skips in 16.17 seconds. One prior quoted-glob invocation selected + no files and exited 1; it is operator error and not evidence. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 3.55 seconds real with maximum RSS + 1,257,357,312 bytes and peak footprint 97,507,040 bytes. `/usr/bin/time -l pnpm lint` passes in + 19.82 seconds real with the same 26 existing warnings, all 41 reliability gates, and the 355-entry + max-lines ratchet; maximum RSS is 2,041,757,696 bytes and peak footprint 97,179,360 bytes. Purpose- + file formatting, syntax, `git diff --check`, and protected-resolver zero-diff pass. +- Residual boundary: commit/push this exact artifact-only package and require native x64/ARM64 + legacy compilation plus all purpose-named contracts before interpreting the live result. A green + live diagnostic still would not authorize product packaging/caller, full-size transfer, fallback, + Beta, tuple enablement, publication, default behavior, or signing. A RED native probe or live + result must be recorded before selecting another mechanism such as ConPTY. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19787,10 +19865,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the one-identifier compiler correction recorded by -`E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001`, then require exact-head Windows x64 and ARM64 legacy- -compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/ -directory teardown evidence. Do not +Commit and push only the private real-console stdin diagnostic recorded by +`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy- +compiler, console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, live +diagnostic, and exact profile/directory teardown evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index c6d07e314fd..ec3a4995eed 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1657,15 +1657,21 @@ created zero-length regular-file descriptor that is already at EOF before CreateProcess; its fixture cleanup passes, but the direct client still times out after the server closes the command session. The next bounded - artifact-only capability is a disconnected Windows launcher with private + artifact-only capability was a disconnected Windows launcher with private anonymous stdin/stdout/stderr pipes, the stdin writer closed before CreateProcessW, an exact inherited-handle list, hidden process creation, bounded stdout/stderr pumps, exit propagation, and an owned job object - that kills the SSH child when the launcher is cancelled. Build and live-test it - on native x64/arm64 before packaging, signing, or adding a product caller. POSIX - retains ignored stdin plus -n; fixture-owned Windows session - processes settle before profile hives are boundedly unloaded and native profile - deletion begins; + that kills the SSH child when the launcher is cancelled. Exact native x64/arm64 + evidence proves that launcher compiles and passes all of those contracts, but + Win32-OpenSSH still remains alive after authenticated remote command-session + closure. This falsifies Node's pipe type and broad inherited-handle leakage. The + next artifact-only diagnostic uses a hidden launcher-owned private real console, + queues console EOF before child creation, and shares that console with the SSH + child while retaining private output pipes, exact handle inheritance, job + ownership, and bounded cancellation. Build and live-test it on native x64/arm64 + before packaging, signing, or adding a product caller. POSIX retains ignored + stdin plus -n; fixture-owned Windows session processes settle + before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs b/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs new file mode 100644 index 00000000000..d2d1b06a03f --- /dev/null +++ b/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.InteropServices; + +internal static class WindowsConsoleEofProbe +{ + private const int StandardInputHandle = -10; + private const uint FileTypeChar = 0x0002; + private const uint EnableProcessedInput = 0x0001; + private const uint EnableLineInput = 0x0002; + + private static int Main() + { + IntPtr input = GetStdHandle(StandardInputHandle); + uint mode; + if (input == IntPtr.Zero || input == new IntPtr(-1) || GetFileType(input) != FileTypeChar) + { + Console.Error.WriteLine("stdin is not a character-device handle"); + return 2; + } + if (!GetConsoleMode(input, out mode)) + { + Console.Error.WriteLine("stdin is not a console input handle"); + return 3; + } + if (!SetConsoleMode(input, mode | EnableProcessedInput | EnableLineInput)) + { + Console.Error.WriteLine("unable to select cooked console input"); + return 4; + } + + byte[] buffer = new byte[1]; + uint bytesRead; + if (!ReadFile(input, buffer, (uint)buffer.Length, out bytesRead, IntPtr.Zero)) + { + Console.Error.WriteLine("console ReadFile failed: {0}", Marshal.GetLastWin32Error()); + return 5; + } + Console.Out.Write("ORCA-PRIVATE-CONSOLE-EOF bytes={0}", bytesRead); + return bytesRead == 0 ? 0 : 6; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int standardHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint GetFileType(IntPtr file); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetConsoleMode(IntPtr console, out uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetConsoleMode(IntPtr console, uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ReadFile( + IntPtr file, + [Out] byte[] buffer, + uint bytesToRead, + out uint bytesRead, + IntPtr overlapped + ); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs b/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs new file mode 100644 index 00000000000..b1da1beff1b --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs @@ -0,0 +1,251 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +internal sealed class WindowsPrivateConsoleInput : IDisposable +{ + private const uint EnableProcessedInput = 0x0001; + private const uint EnableLineInput = 0x0002; + private const uint GenericRead = 0x80000000; + private const uint GenericWrite = 0x40000000; + private const uint FileShareRead = 0x00000001; + private const uint FileShareWrite = 0x00000002; + private const uint OpenExisting = 3; + private const uint HandleFlagInherit = 0x00000001; + private const short KeyEvent = 0x0001; + private const ushort VirtualKeyZ = 0x5a; + private const ushort VirtualKeyReturn = 0x0d; + private const uint MapVirtualKeyToScanCode = 0; + private const uint LeftCtrlPressed = 0x0008; + private const int SwHide = 0; + private const int ErrorInvalidHandle = 6; + + private IntPtr inputHandle; + private bool ownsConsole; + + private WindowsPrivateConsoleInput() + { + } + + internal IntPtr InputHandle { get { return inputHandle; } } + + internal static WindowsPrivateConsoleInput Create() + { + WindowsPrivateConsoleInput console = new WindowsPrivateConsoleInput(); + try + { + console.DetachFromInheritedConsole(); + if (!AllocConsole()) + { + throw LastWin32("AllocConsole failed."); + } + console.ownsConsole = true; + HideConsoleWindow(); + + console.inputHandle = CreateFile( + "CONIN$", + GenericRead | GenericWrite, + FileShareRead | FileShareWrite, + IntPtr.Zero, + OpenExisting, + 0, + IntPtr.Zero + ); + if (console.inputHandle == new IntPtr(-1)) + { + console.inputHandle = IntPtr.Zero; + throw LastWin32("Unable to open the private console input buffer."); + } + if (!SetConsoleMode(console.inputHandle, EnableProcessedInput | EnableLineInput)) + { + throw LastWin32("SetConsoleMode failed for private console input."); + } + if (!SetHandleInformation( + console.inputHandle, + HandleFlagInherit, + HandleFlagInherit + )) + { + throw LastWin32("Unable to make private console input inheritable."); + } + if (!FlushConsoleInputBuffer(console.inputHandle)) + { + throw LastWin32("FlushConsoleInputBuffer failed."); + } + console.QueueEndOfInput(); + return console; + } + catch + { + console.Dispose(); + throw; + } + } + + public void Dispose() + { + if (inputHandle != IntPtr.Zero) + { + CloseHandle(inputHandle); + inputHandle = IntPtr.Zero; + } + if (ownsConsole) + { + FreeConsole(); + ownsConsole = false; + } + } + + private void QueueEndOfInput() + { + // Why: Win32 console EOF is Ctrl+Z followed by Enter in cooked input mode; queue it before + // child creation so the SSH input worker cannot race launcher startup. + InputRecord[] records = new InputRecord[] + { + CreateKeyRecord(true, VirtualKeyZ, '\u001a', LeftCtrlPressed), + CreateKeyRecord(false, VirtualKeyZ, '\u001a', LeftCtrlPressed), + CreateKeyRecord(true, VirtualKeyReturn, '\r', 0), + CreateKeyRecord(false, VirtualKeyReturn, '\r', 0) + }; + uint written; + if (!WriteConsoleInput(inputHandle, records, (uint)records.Length, out written)) + { + throw LastWin32("WriteConsoleInputW failed."); + } + if (written != (uint)records.Length) + { + throw new Win32Exception("WriteConsoleInputW queued an incomplete EOF sequence."); + } + } + + private void DetachFromInheritedConsole() + { + if (!FreeConsole()) + { + int error = Marshal.GetLastWin32Error(); + if (error != ErrorInvalidHandle) + { + throw new Win32Exception(error, "FreeConsole failed."); + } + } + } + + private static void HideConsoleWindow() + { + IntPtr window = GetConsoleWindow(); + if (window == IntPtr.Zero) + { + return; + } + ShowWindow(window, SwHide); + if (IsWindowVisible(window)) + { + throw new Win32Exception("The private SSH console window remained visible."); + } + } + + private static InputRecord CreateKeyRecord( + bool keyDown, + ushort virtualKey, + char character, + uint controlState + ) + { + InputRecord record = new InputRecord(); + record.EventType = KeyEvent; + record.KeyEvent = new KeyEventRecord(); + record.KeyEvent.KeyDown = keyDown; + record.KeyEvent.RepeatCount = 1; + record.KeyEvent.VirtualKeyCode = virtualKey; + record.KeyEvent.VirtualScanCode = (ushort)MapVirtualKey(virtualKey, MapVirtualKeyToScanCode); + record.KeyEvent.UnicodeChar = character; + record.KeyEvent.ControlKeyState = controlState; + return record; + } + + private static Win32Exception LastWin32(string message) + { + return new Win32Exception(Marshal.GetLastWin32Error(), message); + } + + [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)] + private struct InputRecord + { + [FieldOffset(0)] internal short EventType; + [FieldOffset(4)] internal KeyEventRecord KeyEvent; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct KeyEventRecord + { + [MarshalAs(UnmanagedType.Bool)] internal bool KeyDown; + internal ushort RepeatCount; + internal ushort VirtualKeyCode; + internal ushort VirtualScanCode; + internal char UnicodeChar; + internal uint ControlKeyState; + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FreeConsole(); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ShowWindow(IntPtr window, int command); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindowVisible(IntPtr window); + + [DllImport( + "kernel32.dll", + EntryPoint = "CreateFileW", + CharSet = CharSet.Unicode, + SetLastError = true + )] + private static extern IntPtr CreateFile( + string fileName, + uint desiredAccess, + uint shareMode, + IntPtr securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + IntPtr templateFile + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetConsoleMode(IntPtr consoleInput, uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FlushConsoleInputBuffer(IntPtr consoleInput); + + [DllImport("kernel32.dll", EntryPoint = "WriteConsoleInputW", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool WriteConsoleInput( + IntPtr consoleInput, + [In] InputRecord[] records, + uint recordCount, + out uint recordsWritten + ); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKey(uint code, uint mapType); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs similarity index 86% rename from native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs rename to native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs index 0a6efc8bd1f..a7d59c19254 100644 --- a/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs @@ -6,12 +6,14 @@ using System.Runtime.InteropServices; using System.Threading; using Microsoft.Win32.SafeHandles; -internal sealed class WindowsAnonymousPipeSet : IDisposable +internal sealed class WindowsSshChildIo : IDisposable { private const uint HandleFlagInherit = 0x00000001; private const int PumpCompletionTimeoutMilliseconds = 5000; - private IntPtr stdinRead; + private readonly IntPtr stdinRead; + private readonly Stream stdoutDestination; + private readonly Stream stderrDestination; private IntPtr stdoutWrite; private IntPtr stderrWrite; private SafeFileHandle stdoutRead; @@ -21,8 +23,15 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable private Exception pumpFailure; private readonly ManualResetEvent pumpFailureSignal = new ManualResetEvent(false); - private WindowsAnonymousPipeSet() + private WindowsSshChildIo( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) { + this.stdinRead = stdinRead; + this.stdoutDestination = stdoutDestination; + this.stderrDestination = stderrDestination; } internal IntPtr StdinRead { get { return stdinRead; } } @@ -33,21 +42,23 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable get { return pumpFailureSignal.SafeWaitHandle.DangerousGetHandle(); } } - internal static WindowsAnonymousPipeSet Create() + internal static WindowsSshChildIo Create( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) { - WindowsAnonymousPipeSet pipes = new WindowsAnonymousPipeSet(); + WindowsSshChildIo pipes = new WindowsSshChildIo( + stdinRead, + stdoutDestination, + stderrDestination + ); try { SecurityAttributes security = new SecurityAttributes(); security.Length = Marshal.SizeOf(typeof(SecurityAttributes)); security.InheritHandle = true; - IntPtr stdinWrite; - CreateCheckedPipe(out pipes.stdinRead, out stdinWrite, security); - SetNonInheritable(stdinWrite); - // Why: Win32-OpenSSH must see EOF before CreateProcessW, not after startup. - CloseHandleChecked(stdinWrite); - IntPtr stdoutReadHandle; CreateCheckedPipe(out stdoutReadHandle, out pipes.stdoutWrite, security); SetNonInheritable(stdoutReadHandle); @@ -68,7 +79,6 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable internal void CloseChildEndsInParent() { - CloseOwnedHandle(ref stdinRead); CloseOwnedHandle(ref stdoutWrite); CloseOwnedHandle(ref stderrWrite); } @@ -77,11 +87,11 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable { SafeFileHandle stdoutHandle = stdoutRead; stdoutRead = null; - stdoutPump = StartPump(stdoutHandle, Console.OpenStandardOutput(), "stdout"); + stdoutPump = StartPump(stdoutHandle, stdoutDestination, "stdout"); SafeFileHandle stderrHandle = stderrRead; stderrRead = null; - stderrPump = StartPump(stderrHandle, Console.OpenStandardError(), "stderr"); + stderrPump = StartPump(stderrHandle, stderrDestination, "stderr"); } internal void CompletePumps() @@ -208,14 +218,6 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable } } - private static void CloseHandleChecked(IntPtr handle) - { - if (!CloseHandle(handle)) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "CloseHandle failed."); - } - } - private static void CloseOwnedHandle(ref IntPtr handle) { if (handle != IntPtr.Zero) diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs index c7327e593b1..e24c83a0b2b 100644 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -8,7 +8,6 @@ internal static class WindowsSshChildProcess { private const uint CreateSuspended = 0x00000004; private const uint ExtendedStartupInfoPresent = 0x00080000; - private const uint CreateNoWindow = 0x08000000; private const uint StartfUseStdHandles = 0x00000100; private const uint JobObjectLimitKillOnJobClose = 0x00002000; private const int JobObjectExtendedLimitInformationClass = 9; @@ -19,7 +18,14 @@ internal static class WindowsSshChildProcess internal static int Run(string executablePath, string[] args) { - using (WindowsAnonymousPipeSet pipes = WindowsAnonymousPipeSet.Create()) + Stream launcherStdout = Console.OpenStandardOutput(); + Stream launcherStderr = Console.OpenStandardError(); + using (WindowsPrivateConsoleInput console = WindowsPrivateConsoleInput.Create()) + using (WindowsSshChildIo pipes = WindowsSshChildIo.Create( + console.InputHandle, + launcherStdout, + launcherStderr + )) { IntPtr job = IntPtr.Zero; IntPtr attributeList = IntPtr.Zero; @@ -32,7 +38,9 @@ internal static class WindowsSshChildProcess job = CreateKillOnCloseJob(); StartupInfoEx startup = CreateStartupInfo(pipes, out attributeList, out inheritedHandles); StringBuilder commandLine = WindowsCommandLine.Build(executablePath, args); - uint flags = CreateSuspended | ExtendedStartupInfoPresent | CreateNoWindow; + // Why: the SSH child must share the hidden private console for CONIN$ to remain a + // real console handle; CREATE_NO_WINDOW would detach it from that console. + uint flags = CreateSuspended | ExtendedStartupInfoPresent; if (!CreateProcess( executablePath, commandLine, @@ -125,7 +133,7 @@ internal static class WindowsSshChildProcess } private static StartupInfoEx CreateStartupInfo( - WindowsAnonymousPipeSet pipes, + WindowsSshChildIo pipes, out IntPtr attributeList, out IntPtr inheritedHandles ) diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index b11cb29a9f6..f7f8730ef7e 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'private-pipe-launcher-no-n' +type DiagnosticMode = 'private-console-launcher-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -48,10 +48,10 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'private-pipe-launcher-no-n' + const mode: DiagnosticMode = 'private-console-launcher-no-n' const startedAt = performance.now() - // Why: the disconnected launcher creates private child pipes so Win32-OpenSSH receives EOF - // before CreateProcessW without depending on Node's Windows named-pipe behavior. + // Why: redirected stdio hangs in Win32-OpenSSH even with private pipes; this diagnostic gives + // only stdin a hidden real-console boundary with EOF queued before CreateProcessW. const child = spawn( launcherPath as string, [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], @@ -138,16 +138,16 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies private-pipe stdin EOF without the Windows null-input option', + 'qualifies private-console stdin EOF without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const privatePipeLauncherNoN = await runDiagnostic() + const privateConsoleLauncherNoN = await runDiagnostic() console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privatePipeLauncherNoN })}` + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleLauncherNoN })}` ) - expect(privatePipeLauncherNoN.success).toBe(true) + expect(privateConsoleLauncherNoN.success).toBe(true) } ) })