Files
multica/server/internal/daemon/openclaw_cli_timeout_test.go
Bohan Jiang 4d5a679b9b fix(openclaw): unblock the OpenClaw runtime on slow hosts (MUL-6321) (#7142)
Fixes #7112.

Task preparation ran `openclaw config file` and `config get agents.list --json` with a hardcoded 5s deadline; on a 2020 Intel MacBook Pro those take 8.4-10.9s and 4.3s every time, so the OpenClaw runtime was unusable there with no way to raise the limit.

- Deadline 5s -> 30s, overridable via MULTICA_OPENCLAW_CLI_TIMEOUT / backends.openclaw.cli_timeout, clamped to [1s, 60s] so the worst case (4 serial calls) stays inside the 5-minute preparation budget.
- Config discovery cached per daemon profile (on disk, since preparation runs in a one-shot helper process), invalidated by binary identity, OpenClaw path env, active config stat, and a 60s TTL. Only complete successes are cached; any cache fault degrades to a live CLI run.
- The timeout is classified structurally into a new platform-side, non-retryable reason (runtime_cli_timeout) instead of being text-matched into agent_error.provider_network, which showed "check your network" copy and auto-retried a deterministic local failure. Copy added for web/desktop/mobile; a NormalizeDaemonReason shim upgrades reports from older daemons.
2026-08-18 18:33:23 +08:00

51 lines
2.0 KiB
Go

package daemon
import (
"context"
"errors"
"fmt"
"testing"
"github.com/multica-ai/multica/server/internal/daemon/execenv"
"github.com/multica-ai/multica/server/pkg/taskfailure"
)
// TestTaskRunFailureReasonLabelsOpenclawCLITimeout is the daemon half of the
// #7112 fix. The error text ends in "context deadline exceeded", which
// taskfailure.Classify maps to agent_error.provider_network — so before the
// sentinel, a local `openclaw config file` stall was reported to the user as a
// dropped connection to the model provider, and auto-retried because that
// reason is on the retry allowlist. The stall is local and deterministic: the
// retry re-pays the same 8-11s and fails identically.
func TestTaskRunFailureReasonLabelsOpenclawCLITimeout(t *testing.T) {
err := fmt.Errorf(
"prepare execution environment: execenv: prepare openclaw config: locate openclaw active config: %w",
fmt.Errorf("openclaw config file: context deadline exceeded (process: signal: killed): %w", execenv.ErrOpenclawCLITimeout),
)
want := taskfailure.ReasonRuntimeCLITimeout.String()
if got := taskRunFailureReason(err); got != want {
t.Errorf("taskRunFailureReason = %q, want %q", got, want)
}
// The reason must stay off the auto-retry allowlist (asserted next to that
// table in internal/service): nothing about the host changes between
// attempts, so a retry only burns another full preparation before failing
// the same way.
}
// TestTaskRunFailureReasonKeepsProviderDeadline pins the other side of the
// same boundary: a real provider-side deadline (no sentinel) must keep landing
// in the retryable provider bucket.
func TestTaskRunFailureReasonKeepsProviderDeadline(t *testing.T) {
err := fmt.Errorf("stream from provider: %w", context.DeadlineExceeded)
want := taskfailure.ReasonAgentProviderNetwork.String()
if got := taskRunFailureReason(err); got != want {
t.Errorf("taskRunFailureReason = %q, want %q", got, want)
}
if errors.Is(err, execenv.ErrOpenclawCLITimeout) {
t.Error("a bare context deadline must not match the openclaw CLI sentinel")
}
}