diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ac28ebf0..536a7d93f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,12 +1,27 @@ # The gate: every suite on every push. `npm run acceptance` stays manual — # it spends a real claude turn and needs credentials. +# +# windows-latest keeps the win32 paths honest (WSL wrap in electron/wsl.js). +# There is no node-pty in this repo, so the ConPTY rebuild named in #397 is +# a no-op — do not add a rebuild step. Most of the electron suite still +# cannot run on win32 (shebang fakes); scripts/test-electron.js skips those +# and runs the WSL contract only. Winget is blocked on an installer — see +# the note in scripts/package-cross.sh. +# +# `smoke` is a separate job on purpose: a boot/dispatch failure must show +# up as a boot failure, not a needle inside `npm test`. It is the first +# thing in CI that actually starts the app on Windows (#449). name: test on: push jobs: test: - runs-on: macos-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -18,3 +33,23 @@ jobs: - run: npm ci --prefix core - run: npm ci --prefix memory-server - run: npm test + + smoke: + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: "**/package-lock.json" + - run: npm ci + - run: npm ci --prefix core + - run: npm run build --prefix core + - run: npx vite build + - run: npm run test:smoke diff --git a/README.md b/README.md index 5723195e2..5107e497e 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,8 @@ Grab an archive from the [latest release](https://github.com/currentbits/solenta `Solenta.app` to `/Applications`. Not notarized, so right-click → **Open** the first time (or `xattr -dr com.apple.quarantine /Applications/Solenta.app`). - **Windows (x64)** — `Solenta--win32-x64.zip`. Unzip anywhere, run - `solenta.exe`. Unsigned, so SmartScreen asks once. + `solenta.exe`. Unsigned, so SmartScreen asks once. There is no installer + and no winget package; see the note in `scripts/package-cross.sh`. - **Linux (x64)** — `Solenta--linux-x64.tar.gz`. Extract, run `./solenta`. ### Updates diff --git a/electron/agent.js b/electron/agent.js index b4e1e557e..e9ad9a20f 100644 --- a/electron/agent.js +++ b/electron/agent.js @@ -1,6 +1,10 @@ "use strict"; -const { spawn } = require("node:child_process"); +// cross-spawn, not child_process: on Windows the agent CLIs install as +// .cmd shims and Node refuses to exec those directly. cross-spawn routes +// them through cmd.exe with correct escaping, which matters because the +// prompt travels in argv (#442). +const spawn = require("cross-spawn"); const { killTree } = require("./proc.js"); const CHUNK_THROTTLE_MS = 250; diff --git a/electron/claude.js b/electron/claude.js index 4ffd74656..5767e1b13 100644 --- a/electron/claude.js +++ b/electron/claude.js @@ -1,6 +1,10 @@ "use strict"; -const { spawn } = require("node:child_process"); +// cross-spawn, not child_process: on Windows the agent CLIs install as +// .cmd shims and Node refuses to exec those directly. cross-spawn routes +// them through cmd.exe with correct escaping, which matters because the +// prompt travels in argv (#442). +const spawn = require("cross-spawn"); const { getClaudeMcpArgs } = require("./memory-sup.js"); const { killTree } = require("./proc.js"); diff --git a/electron/codex.js b/electron/codex.js index 95713fc5c..fcccddf53 100644 --- a/electron/codex.js +++ b/electron/codex.js @@ -1,6 +1,10 @@ "use strict"; -const { spawn } = require("node:child_process"); +// cross-spawn, not child_process: on Windows the agent CLIs install as +// .cmd shims and Node refuses to exec those directly. cross-spawn routes +// them through cmd.exe with correct escaping, which matters because the +// prompt travels in argv (#442). +const spawn = require("cross-spawn"); const { killTree } = require("./proc.js"); const SIGKILL_AFTER_MS = 3000; diff --git a/electron/commitmsg.js b/electron/commitmsg.js index dd4787efb..0a3e65bac 100644 --- a/electron/commitmsg.js +++ b/electron/commitmsg.js @@ -1,6 +1,10 @@ "use strict"; -const { spawn } = require("node:child_process"); +// cross-spawn, not child_process: on Windows the agent CLIs install as +// .cmd shims and Node refuses to exec those directly. cross-spawn routes +// them through cmd.exe with correct escaping, which matters because the +// prompt travels in argv (#442). +const spawn = require("cross-spawn"); const { getProvider, resolveBin, isBinAvailable } = require("./providers.js"); const { diff, assertNoOutboundSecrets } = require("./worktrees.js"); const { fmRun } = require("./fm.js"); diff --git a/electron/devservers.js b/electron/devservers.js index a7dfd3d7b..3f01cfdb6 100644 --- a/electron/devservers.js +++ b/electron/devservers.js @@ -2,7 +2,11 @@ const fs = require("node:fs"); const path = require("node:path"); -const { spawn } = require("node:child_process"); +// cross-spawn, not child_process: on Windows npm is npm.cmd and Node +// refuses to exec .cmd shims without a shell. Same reason as agent.js (#442). +const spawn = require("cross-spawn"); +const { wrapCommand } = require("./ssh.js"); +const { wslTarget } = require("./wsl.js"); const PREFERRED_SCRIPTS = ["dev", "start", "serve"]; const RING_LIMIT = 50; @@ -75,6 +79,7 @@ function detectScripts(root) { * pending: string, * dead: boolean, * deadAt: number | null, + * platform: NodeJS.Platform, * }} DevServerRecord */ @@ -165,11 +170,16 @@ function toState(rec) { /** * @param {number} pid + * @param {NodeJS.Platform} [platform] */ -function killProcessGroup(pid) { +function killProcessGroup(pid, platform = process.platform) { if (!pid) return; + // Windows has no POSIX process groups; process.kill(-pid) throws and + // SIGTERM is terminate. Kill the pid directly instead of failing closed + // through the catch. + const target = platform === "win32" ? pid : -pid; try { - process.kill(-pid, "SIGTERM"); + process.kill(target, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); @@ -181,7 +191,7 @@ function killProcessGroup(pid) { const timer = setTimeout(() => { pendingKills.delete(pid); try { - process.kill(-pid, "SIGKILL"); + process.kill(target, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); @@ -198,21 +208,40 @@ function killProcessGroup(pid) { * Spawn `npm run