Files
multica/.github/workflows/ci.yml
Multica Eve a0ef1a43f3 fix(daemon): diagnose silent OpenClaw npm shim failures on Windows (MUL-5422) (#6084)
* fix(daemon): diagnose silent OpenClaw npm shim failures on Windows (MUL-5422)

#6061 reported every OpenClaw task failing in execenv prep on Windows with
a bare `exit status 1` and no stderr, leaving the user nothing to act on.

An npm-installed `openclaw.cmd` is a batch shim that re-execs OpenClaw's
`openclaw.mjs` entrypoint through `node`, resolved from PATH. The daemon pins
`openclaw` to an absolute path, so the shim always looks correct — but that
interpreter lookup is a second, invisible resolution step that can fail on its
own. The reporter had to run their own subprocess experiments to find it.

Enrich the error instead of guessing at a fix: when a `.cmd`/`.bat` shim exits
non-zero with no stderr, report whether the interpreter resolves. Both
directions are useful — missing names the likely cause with a next step,
present clears PATH of blame and points at the remaining hypotheses (PATH
drift between the runtime `--version` gate and task prep, or a broken install).

Deliberately NOT included: rebuilding or freezing a Windows PATH. The
version-probe gate (probeBuiltinRuntime skips a provider whose `--version`
fails) and the prep helper both inherit the same daemon environment, so a
daemon that could not resolve `node` would never have registered OpenClaw at
all. That contradiction is unresolved, and a boot-time PATH snapshot would also
fight the MUL-4486 self-heal design, which re-resolves per attempt on purpose.
This change collects the evidence needed to settle it.

- Error text only; no control flow change, and real stderr still wins.
- PATH summarised as an entry count, never dumped, so daemon logs and pasted
  bug reports carry no environment detail.
- Tests: shim detection (case, spaces, Unicode), both diagnostic directions,
  out-of-scope no-ops (timeout, missing binary, native exe), and end-to-end
  through execOpenclawCLI. A windows-tagged file reproduces a real npm shim
  with and without node on PATH, and pins TEMP/TMP as not load-bearing — the
  originally reported root cause, since retracted upstream.
- New scoped step in the existing ci.yml windows-execenv job.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): address review on OpenClaw shim diagnostics (MUL-5422)

Four must-fix items from PR review, each verified against the real behaviour
rather than assumed.

1. Timeout was misdiagnosed as a missing interpreter. openclawCLITimeout kills
   the child via CommandContext, and a killed process surfaces as
   *exec.ExitError ("signal: killed") — the same type a genuine exit 1
   produces. The errors.As gate accepted it and appended "install Node.js",
   sending users to fix something that was never broken. execOpenclawCLI now
   attributes ctx.Err() before consulting the diagnostic. Confirmed locally:
   `signal: killed`, errors.As(*exec.ExitError)=true, ctx.Err() set. The old
   test passed context.DeadlineExceeded directly and so never saw the real
   shape; replaced with a genuine CommandContext timeout regression on both
   Linux and Windows.

2. The interpreter lookup did not match npm's. npm's cmd-shim template emits
   `IF EXIST "%dp0%\node.exe" (...) ELSE ( SET "_prog=node" )`, so a co-located
   node.exe wins over PATH entirely. Checking only LookPath reported "node is
   not resolvable" for installs that actually run fine — confidently wrong,
   which is worse than silence. Now resolves co-located `node.exe`/`node`
   first, then PATH, and reports which. Wording is also conditional now
   ("if <name> is an npm-generated shim"): a batch extension does not prove npm
   authorship, since MULTICA_OPENCLAW_PATH can point at any batch file.

3. The message leaked local paths off-box. On prep failure this text is not
   log-local — it travels reportTerminalTask → Client.FailTask and is persisted
   server-side as the task error, so an absolute Windows shim path uploads the
   account name and install layout. Now reports only the shim's base name,
   whether the interpreter resolved and from where, and a PATH entry count.
   Never an absolute path, never PATH contents.

4. Windows CI was green without exercising the new code. The job log showed
   `cmd.exe stderr DID reach Go's pipe` with `'node' is not recognized`, so the
   missing-node case takes the existing stderr branch and the diagnostic never
   ran — masked by an "either branch passes" assertion. That disjunction is
   gone: the missing-node test now asserts the observed stderr behaviour
   (disproving #6061's premise), and a new test drives a genuinely silent shim
   to prove the diagnostic branch itself works on Windows. Also added Windows
   coverage for the co-located interpreter and the timeout case.

The windows-tagged shim is now npm's real generated template rather than a
hand-simplified `node ...` one-liner, so the co-located branch is reproduced
faithfully instead of hidden.

Co-authored-by: multica-agent <github@multica.ai>

* test(daemon): make the OpenClaw timeout regression PATH-independent

The new timeout test stripped PATH (so a stray interpreter lookup would report
"missing") while its hanging shim invoked `sleep` through a PATH lookup. macOS
`sh` quietly falls back to a default PATH so this passed locally; dash on Linux
does not, so CI failed with `exit status 127 (stderr: sleep: not found)` — the
shim died instantly instead of hanging, and the assertion never saw a timeout.

Resolve `sleep` before PATH is stripped and embed it by absolute path, so the
shim needs no PATH of its own. Verified the failure mode and the fix directly:
`env -i /bin/sh -c 'PATH=/nonexistent; sleep 0.05'` reproduces
"sleep: command not found", while the absolute path runs fine with the same
empty PATH.

Windows is skipped here and covered by TestWindowsOpenclawShimTimeoutIsNotMisdiagnosed,
which has a real cmd.exe host and a System32 PATH that can resolve its own helper.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): bound execOpenclawCLI so its 5s timeout is actually enforceable

The new timeout regression exposed a real bug in the code it was testing, not
just a flaky test: openclawCLITimeout could not bound the call at all.

CommandContext kills only the direct child, and cmd.Output() blocks in Wait()
until the stdout pipe closes. Any grandchild that inherited stdout keeps the
call parked for its own lifetime. Verified on linux/dash: a shim whose child
slept 5s ran the FULL 5.01s against a 150ms deadline. With a WaitDelay backstop
the same case returns in ~2.17s.

This is not a hypothetical shape — it is precisely an npm shim on Windows
(cmd.exe → node), so a wedged node could stall task prep far past the 5s cap
that comment claims. detectCLIVersion already carries this exact backstop for
the `--version` probe for the same reason; execOpenclawCLI now matches it.

Also corrected the test comment: an earlier revision claimed a trailing
`exit 0` was needed to force the grandchild. Docker showed otherwise — dash
hangs either way and macOS reproduces neither, which is why CI caught this and
local runs did not. The comment now records the measured behaviour.

Verified in a linux/dash container (the CI platform, not just macOS): the full
execenv package passes with -race, and the timeout case takes 2.17s.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): drop the WaitDelay change and wrap the context error (MUL-5422)

Round-2 review: take option 1 — keep this PR to diagnostics and split the
timeout/process-tree work out.

The reviewer is right that WaitDelay traded a hang for a process leak, and I
had the mechanism wrong. Measured on linux/dash by recording the grandchild PID
and reading /proc/<pid>/stat at the moment Output returns:

  no WaitDelay:   elapsed 6.01s (6s sleep, 150ms deadline), grandchild state Z
  with WaitDelay: elapsed 2.17s (60s sleep, 150ms deadline), grandchild state S

So without WaitDelay the call is hostage to the descendant's lifetime but no
live process is left behind — it returned precisely because the descendant had
exited. With WaitDelay the call is bounded but a live descendant survives. My
earlier claim that the orphan pre-existed was an artifact of a sleep duration
that happened to equal the return time.

Go's WaitDelay contract covers killing the direct child and closing our pipe
ends; it does not reap orphans. Closing this properly needs process-tree
ownership (Unix process group, Windows Job Object) so the deadline can terminate
the whole tree — and on Unix nothing else will, since
preparationProcessController.finish() is a no-op there (isolation_unix.go).
That is its own change with its own risk surface, so it is tracked separately
and openclawCLITimeout now documents the gap with the measurements rather than
shipping half a fix.

Also fixes the round-2 nit: the context branch %w-wrapped the process error
while printing ctxErr with %v, so errors.Is(err, context.DeadlineExceeded) was
false despite the text containing it. The context error is now the wrapped
cause and the process error is attached for diagnosis:

  openclaw config file: context deadline exceeded (process: signal: killed)

Tests: the timeout cases no longer depend on WaitDelay and no longer leave a
live process — short sleeps keep them about attribution, which is what they are
for. Added an explicit errors.Is assertion for both DeadlineExceeded and
Canceled. Verified in a linux/dash container (the CI platform): full execenv
package passes with -race and `ps` shows no leftover sleep processes.

Co-authored-by: multica-agent <github@multica.ai>

* docs(daemon): correct two stale comments on the OpenClaw CLI timeout (MUL-5422)

Both nits from the third review. Comment-only; no code change.

1. openclawCLITimeout's doc contradicted itself — it opened with "caps ...
   without letting a hung CLI stall task dispatch indefinitely" and then
   explained that the deadline cannot actually bound the call. Reworded to say
   what it is (a 5s context deadline) and to point at the gap rather than assert
   a guarantee it does not provide. Also names MUL-5467 instead of the vague
   "tracked separately".

2. The two timeout tests claimed a long wait would "leave a live process
   behind". That described the reverted WaitDelay behaviour, not the current
   code. Without WaitDelay, cmd.Output() returns only once the descendant has
   closed stdout — its exit is what produces the EOF — so a long wait makes the
   test slow, it does not leak. Re-verified on linux/dash after the fix: the
   case takes 1.01s for a 1s sleep and `ps` shows no leftover process, and the
   earlier PID probe recorded the grandchild in state Z at the return point.

Rebased onto c25a82eee.

Co-authored-by: multica-agent <github@multica.ai>

* docs(daemon): tighten OpenClaw timeout wording and pipe-EOF claims (MUL-5422)

Round-4 review nits. Comment-only; verified no non-comment line changed.

1. OpenclawConfigPrep.Timeout still said it "caps each CLI invocation", which
   contradicts the openclawCLITimeout doc corrected last round. It now says it
   sets the context deadline and points at that note. Fixed the same word in the
   struct's own doc comment, which had the identical claim.

2. The two timeout-test comments equated "descendant closes the pipe" with
   "descendant has exited". That holds for these helpers but is not a general
   property — a process can close its pipes and keep running — so the comments
   now scope the claim to the helper and say so explicitly.

   Also corrected "stdout" to the output pipes os/exec manages for both stdout
   AND stderr. Verified rather than assumed: with cmd.Stderr set to an
   in-memory writer (as execOpenclawCLI does), a grandchild holding EITHER
   stream parks cmd.Output() for its full 3s lifetime, while one holding
   neither returns in 0s. So Wait genuinely depends on both.

No rebase this round: the branch is 1 commit behind main, that commit does not
touch execenv, and GitHub already reports MERGEABLE — not worth another forced
CI rerun.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 16:30:35 +08:00

386 lines
16 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Decides whether the (heavy, ~6min) frontend job has anything to do.
# The frontend job validates the web/desktop apps, the shared packages,
# the install graph, and the selfhost / reserved-slugs scripts it runs;
# a pure backend-only or docs-only PR touches none of those and gains
# nothing from a full web build. This job emits a single `frontend`
# output consumed by the frontend job below.
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
frontend: ${{ steps.decide.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Filter paths
id: filter
uses: dorny/paths-filter@v3
with:
# apps/docs is excluded from the frontend turbo run, so a
# docs-only change does not need this job. apps/mobile has its
# own mobile-verify workflow. Everything else the frontend job
# touches is listed here; bias toward over-matching since a
# missed path silently skips validation.
filters: |
frontend:
- 'apps/web/**'
- 'apps/desktop/**'
- 'packages/**'
- 'package.json'
- '.npmrc'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- '.github/workflows/ci.yml'
- 'scripts/generate-reserved-slugs.mjs'
- 'server/internal/handler/reserved_slugs.json'
- 'scripts/selfhost-config.test.sh'
- 'scripts/check.sh'
- 'scripts/dev.sh'
- 'scripts/local-env.sh'
- '.env.example'
- 'docker-compose.selfhost.yml'
- name: Decide
id: decide
# Always run the frontend job on push to main (full validation);
# on pull_request, run only when frontend-relevant paths changed.
# The frontend job itself always runs and reports success — its
# steps are gated on this output rather than the job being skipped
# — so the required "frontend" status check is satisfied with a
# genuine green instead of being left pending on filtered PRs.
env:
EVENT_NAME: ${{ github.event_name }}
FRONTEND_CHANGED: ${{ steps.filter.outputs.frontend }}
run: |
if [ "$EVENT_NAME" != "pull_request" ] || [ "$FRONTEND_CHANGED" = "true" ]; then
echo "frontend=true" >> "$GITHUB_OUTPUT"
else
echo "frontend=false" >> "$GITHUB_OUTPUT"
fi
# The frontend validation is split across two runners on purpose. Both
# `@multica/web:build` (a webpack production build) and `@multica/views:test`
# (259 jsdom files) are CPU-saturating, and a standard runner only has
# 4 vCPUs. Running them in one job made them starve each other: the views
# suite needs ~104s wall when it owns 4 cores but took ~500s sharing them,
# and the identical webpack compile went from ~26s to ~342s. Splitting buys
# a second 4-vCPU box rather than reducing the work; total runner-minutes go
# up slightly, wall-clock feedback time goes down.
#
# The split is weighted, not even: the test group is by far the heavier half
# (~575 CPU-seconds vs ~250 for build + typecheck + lint), so it gets a
# runner to itself and everything else shares the other one.
frontend-build:
needs: changes
runs-on: ubuntu-latest
env:
# Pin turbo's filesystem cache somewhere actions/cache can address.
TURBO_CACHE_DIR: .turbo/cache
steps:
- name: Checkout
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/checkout@v6
- name: Setup pnpm
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: pnpm/action-setup@v4
- name: Setup Node.js
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- name: Install dependencies
if: ${{ needs.changes.outputs.frontend == 'true' }}
run: pnpm install
# `node-version: 22` above floats across patch releases, and turbo's
# global hash does not include the interpreter at all (`engines` is null
# in its dry-run cache inputs). Without the resolved version in the key,
# a runner silently moving to another 22.x would restore a cache built by
# the old interpreter and report green without executing anything.
- name: Resolve runtime for cache key
id: runtime
if: ${{ needs.changes.outputs.frontend == 'true' }}
run: echo "node=$(node --version)" >> "$GITHUB_OUTPUT"
# Cache entries are immutable, so the key carries the commit SHA to make
# every run publish a fresh one and `restore-keys` falls back to the most
# recent prefix match. The two frontend jobs run disjoint task sets, so
# they get their own prefixes rather than racing to save one key.
# GitHub scopes caches by branch: a PR reads main's entries (so unchanged
# tasks hit on the first push) and writes its own (so re-pushes hit too).
- name: Restore turbo cache
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-${{ github.sha }}
restore-keys: |
turbo-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-
- name: Test self-host env derivation
if: ${{ needs.changes.outputs.frontend == 'true' }}
run: bash scripts/selfhost-config.test.sh
- name: Verify reserved-slugs.ts is up to date
if: ${{ needs.changes.outputs.frontend == 'true' }}
# Re-runs the generator and fails on any drift from the
# checked-in TypeScript output. The Go side embeds the JSON
# source directly, so a passing diff here proves both sides
# share one source of truth.
run: |
pnpm generate:reserved-slugs
git diff --exit-code -- packages/core/paths/reserved-slugs.ts
- name: Build, type check, and lint
if: ${{ needs.changes.outputs.frontend == 'true' }}
# Mobile lives in a parallel mobile-verify workflow (path-filtered
# to apps/mobile/** + packages/core/**) so it doesn't add
# ~50s of expo-lint + tsc to every web/desktop PR. Keep this
# filter in sync with the root package.json scripts, which also
# exclude @multica/mobile.
run: pnpm exec turbo build typecheck lint --filter='!@multica/docs' --filter='!@multica/mobile'
frontend-test:
needs: changes
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/cache
steps:
- name: Checkout
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/checkout@v6
- name: Setup pnpm
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: pnpm/action-setup@v4
- name: Setup Node.js
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- name: Install dependencies
if: ${{ needs.changes.outputs.frontend == 'true' }}
run: pnpm install
- name: Resolve runtime for cache key
id: runtime
if: ${{ needs.changes.outputs.frontend == 'true' }}
run: echo "node=$(node --version)" >> "$GITHUB_OUTPUT"
# See frontend-build for the key strategy. These entries are tiny (~60KB
# measured): `test` declares no outputs, so turbo caches exit codes and
# logs rather than artifacts -- yet a hit still skips the whole suite,
# which is the single most expensive task in the graph.
- name: Restore turbo cache
if: ${{ needs.changes.outputs.frontend == 'true' }}
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-${{ github.sha }}
restore-keys: |
turbo-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-
- name: Test
if: ${{ needs.changes.outputs.frontend == 'true' }}
# Same filter rationale as frontend-build. Type errors are not this
# job's responsibility -- frontend-build owns the `typecheck` task.
# `test` reaches dependency sources through the hash-only
# `^cache-inputs` edge (see turbo.json), so no `tsc` runs here.
run: pnpm exec turbo test --filter='!@multica/docs' --filter='!@multica/mobile'
# Aggregate gate. `frontend` is the status-check name the repository's
# branch rules refer to, so it has to survive the split above: this job
# keeps reporting under that name and simply fails when either half fails.
# It also inherits the old contract that the check goes green (rather than
# staying pending) on PRs the path filter excluded — both halves succeed
# trivially in that case because every step is gated off.
frontend:
needs: [frontend-build, frontend-test]
# `!cancelled()` rather than `always()`: a run superseded by a newer push
# is cancelled by the concurrency group above, and there is no reason to
# spend a runner reporting a verdict nobody will read.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Check frontend job results
env:
BUILD_RESULT: ${{ needs.frontend-build.result }}
TEST_RESULT: ${{ needs.frontend-test.result }}
run: |
echo "frontend-build: $BUILD_RESULT"
echo "frontend-test: $TEST_RESULT"
if [ "$BUILD_RESULT" != "success" ] || [ "$TEST_RESULT" != "success" ]; then
echo "::error::frontend validation failed"
exit 1
fi
backend:
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_DB: multica
POSTGRES_USER: multica
POSTGRES_PASSWORD: multica
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U multica -d multica"
--health-interval 5s
--health-timeout 5s
--health-retries 20
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgres://multica:multica@localhost:5432/multica?sslmode=disable
# Wires up the RedisLocalSkill*_test.go suite. Distinct from REDIS_URL
# (which would flip the server binary itself onto the Redis-backed
# realtime relay + request stores); the tests talk to this Redis
# directly so they run alongside the Postgres-backed suite.
REDIS_TEST_URL: redis://localhost:6379/1
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.26.1"
cache-dependency-path: server/go.sum
- name: Setup Helm
uses: azure/setup-helm@v4
- name: Test Helm chart
run: bash scripts/helm-config.test.sh
- name: Build
run: cd server && go build ./...
- name: Run migrations
run: cd server && go run ./cmd/migrate up
- name: Verify Go test wrapper
run: bash scripts/test-go.test.sh
- name: Test
run: bash scripts/test-go.sh --race
windows-execenv:
# The environment-preparation deadline owns a process tree, not just a Go
# process. This Windows runtime test verifies Job Object cancellation kills
# a delayed descendant before an immediate retry can reuse the same root.
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.26.1"
cache-dependency-path: server/go.sum
- name: Test Windows execution-environment isolation
working-directory: server
# Keep this job scoped to the runtime regression it exists to prove.
# The package's legacy OpenClaw HOME tests are not Windows-safe and are
# outside this PR; the normal backend job still runs the full package.
run: go test ./internal/daemon/execenv -run '^TestPrepareIsolated_WindowsKillsDescendantBeforeRetry$' -count=1 -timeout=5m
- name: Test Windows agent launcher argv/stdin handling
working-directory: server
# Agent prompts must never reach a Windows launcher through argv: the
# official cursor-agent.ps1 ends in `& node.exe index.js $args`, and
# PowerShell re-serialises $args onto the child command line. Under
# Legacy native argument passing (powershell.exe 5.1, pwsh <= 7.2) a
# prompt holding embedded quotes is re-tokenised and fragments like
# `-X` become flags (#5649). Only a real PowerShell host proves this,
# so it cannot live in the ubuntu backend job. Scoped to the launcher
# tests, which are windows-tagged and therefore run nowhere else today;
# the backend job still runs the full package on Linux.
# -v so a silent skip (no PowerShell host resolved, or a -run pattern
# that stops matching) is visible in the log instead of passing as "ok".
run: go test ./pkg/agent -v -run '^(TestCursorExecutePromptSurvivesPowerShellShim|TestPlatformCursorInvocation|TestPlatformCopilotInvocation|TestPlatformPiInvocation)' -count=1 -timeout=5m
- name: Test bounded Codex cleanup with inherited stdout descendant
working-directory: server
# Windows cannot prove whole-tree termination without a Job Object,
# but a descendant holding inherited stdout must never keep Result
# blocked forever. -v makes RUN/PASS evidence explicit in CI logs.
run: go test ./pkg/agent -v -run '^TestCodexWindowsInheritedStdoutDescendantCleanupIsBounded$' -count=1 -timeout=5m
- name: Test Windows OpenClaw npm shim interpreter resolution
working-directory: server
# #6061: every OpenClaw task failed execenv prep on a Windows host with
# a bare `exit status 1` and no stderr. A batch shim resolves and runs
# fine while the `node` it re-execs is unreachable, and npm's real
# template prefers a co-located node.exe over PATH — none of which can
# be proven without a real cmd.exe host. These tests pin: the positive
# control (node on PATH → success), that a missing node surfaces
# cmd.exe's own stderr (the first run of this job disproved #6061's
# premise that it does not), that a genuinely silent shim DOES reach the
# new diagnostic, that a co-located interpreter is credited, that a
# context timeout is not misdiagnosed as a missing interpreter, and that
# TEMP/TMP are NOT load-bearing (the originally reported root cause,
# since retracted upstream).
# Scoped to the windows-tagged shim tests — the package's legacy
# OpenClaw HOME tests are not Windows-safe; the backend job still runs
# the full package plus the cross-platform half on Linux.
# -v so a skip (no node on the runner) is visible instead of passing
# silently as "ok".
run: go test ./internal/daemon/execenv -v -run '^TestWindowsOpenclawShim' -count=1 -timeout=5m
- name: Build Windows CLI helper entrypoint
working-directory: server
run: go build ./cmd/multica
installer:
# Stub-driven shell tests for scripts/install.sh. Kept off the heavy
# backend job so installer regressions surface independently, and
# exercised on macOS too because the installer targets macOS/Homebrew
# and `tar` / `sed` / `mktemp` differ between BSD and GNU userlands.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Test shell installers
run: bash scripts/install.test.sh