mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* test(server): reduce slow unit test waits Co-authored-by: multica-agent <[email protected]> * test(server): cut slow Go unit test time further Follow-up to the first pass on MUL-7271. Test-only changes plus a few package-private seams whose production values are unchanged. - Re-executed race-built test binaries no longer pay the race runtime's 1s atexit sleep (GORACE=atexit_sleep_ms=0 for child processes only). - pkg/agent: event-driven or shorter test-only bounds for handshake, drain and idle graces; redundant timeout variants merged. - daemon/execenv: git fixtures copied from a template repo built once; tests that re-ran the same worktree lifecycle folded into their supersets; ForkLock-guarded fake-CLI writes so parallel tests cannot hit ETXTBSY. - handler/service: cap and paging tests shrink their caps instead of seeding thousands of rows; lock waits probe pg_locks instead of sleeping; one shared pgxpool per service test binary; cheaper cron and RSA fixtures. - wecom: the phishing-link sweep reuses one parser and derives CRLF from LF with a sampled cross-check; fake sockets acknowledge instead of waiting out the ack timeout. - migrate: the migration corpus is parsed once; isolated DB tests run in parallel. Co-authored-by: multica-agent <[email protected]> * test(server): remove transport timeout unit tests Co-authored-by: multica-agent <[email protected]> * test(server): restore two non-timeout tests and make a bound load-bearing Follow-up to the review of #8318. All test-only; no production code changes. - daemon: restore the three read-liveness cases that the transport-timeout sweep took with it. They assert that a ping, a pong and an application frame each EXTEND the read deadline, so a healthy wakeup socket survives without traffic of our own — the deadline never fires on the happy path, which is why they are not transport-timeout tests. Verified they fail (all three) when the extension calls are removed from wakeup.go, and pass in 0.57s together. The pure "times out without peer traffic" case stays deleted. - pkg/agent: restore the parent-DEADLINE half of the opaque-env redaction pair. codex.go treats a parent deadline and a parent cancellation as one ending; only the Canceled arm was left covered. The fake now writes the secret to stderr before reading the handshake, so the check no longer races our own initialize write, and the budget is 2s against a measured 430-465ms launch. Verified it fails when the !contextEnded guard is removed. - pkg/agent: TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn had its bound widened to a literal 5s, which the shrunk 750ms drain grace can never reach — the test could no longer fail. It now raises the drain grace to 10s for its own duration and asserts under 3s: healthy runs take 0.19-0.44s, and a broken short-circuit takes 10.19s and fails. - wecom: gofmt the import block in relay_settle_budget_test.go. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: J <[email protected]>
467 lines
18 KiB
Go
467 lines
18 KiB
Go
//go:build !windows
|
|
|
|
package execenv
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Regression tests for the task-critical half of MUL-5467. prepareOpenclawConfig
|
|
// shells out to `openclaw config file` / `openclaw config get ...` while
|
|
// preparing a task's execution environment, so both OpenClaw misbehaviours land
|
|
// on the path between "task claimed" and "agent started":
|
|
//
|
|
// - openclaw forks a long-lived `openclaw-config` helper that inherits
|
|
// stdout/stderr. With cmd.Output(), os/exec waits for those pipes to reach
|
|
// EOF, which never comes while the helper lives — and cancelling the context
|
|
// kills the direct child without unblocking that wait, so openclawCLITimeout
|
|
// could not bound the call.
|
|
// - `openclaw config file` and `openclaw agents list` print the correct answer
|
|
// in ~250ms and then do not exit, so waiting for exit turned two working
|
|
// commands into a task-fatal error.
|
|
|
|
// writeHelperForkingOpenclaw creates a fake openclaw with the first shape: emit
|
|
// JSON on stdout, fork a helper that keeps the inherited stdout/stderr open far
|
|
// longer than the test would wait, then exit 0.
|
|
func writeHelperForkingOpenclaw(t *testing.T, pidFile string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "openclaw")
|
|
body := `#!/bin/sh
|
|
( echo $$ > "` + pidFile + `"; sleep 300 ) &
|
|
# Make the helper's registration deterministic: without this the parent can
|
|
# exit and the group be reaped before the helper runs, leaving the test with
|
|
# no pid to assert on.
|
|
while [ ! -s "` + pidFile + `" ]; do sleep 0.01; done
|
|
echo '{}'
|
|
exit 0
|
|
`
|
|
writeTestExecutable(t, bin, []byte(body))
|
|
return bin
|
|
}
|
|
|
|
func readHelperPid(t *testing.T, pidFile string) int {
|
|
t.Helper()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if data, err := os.ReadFile(pidFile); err == nil {
|
|
if pid, convErr := strconv.Atoi(strings.TrimSpace(string(data))); convErr == nil && pid > 0 {
|
|
return pid
|
|
}
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Fatalf("helper never wrote its pid to %s", pidFile)
|
|
return 0
|
|
}
|
|
|
|
func helperGone(pid int, within time.Duration) bool {
|
|
deadline := time.Now().Add(within)
|
|
for time.Now().Before(deadline) {
|
|
if syscall.Kill(pid, 0) != nil {
|
|
return true
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestExecOpenclawCLIReturnsDespitePipeHoldingHelper is the assertion that
|
|
// makes openclawCLITimeout meaningful: the call must come back on the direct
|
|
// child's exit, not on the helper's lifetime and not on the deadline.
|
|
func TestExecOpenclawCLIReturnsDespitePipeHoldingHelper(t *testing.T) {
|
|
t.Parallel()
|
|
pidFile := filepath.Join(t.TempDir(), "helper.pid")
|
|
bin := writeHelperForkingOpenclaw(t, pidFile)
|
|
|
|
// A deliberately generous deadline: before the fix this hung past it, so a
|
|
// tight ctx would have hidden the bug behind a plausible-looking timeout.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
out, err := execOpenclawCLI(ctx, bin, "config", "get", "--json")
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
t.Fatalf("execOpenclawCLI: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != "{}" {
|
|
t.Errorf("stdout = %q, want {}", out)
|
|
}
|
|
if elapsed > 15*time.Second {
|
|
t.Errorf("execOpenclawCLI took %v — it waited on the helper instead "+
|
|
"of returning once openclaw itself exited", elapsed)
|
|
}
|
|
}
|
|
|
|
// TestExecOpenclawCLIReapsForkedHelper pins the cleanup half. Task preparation
|
|
// runs per task, and this is where the orphan `openclaw-config` processes came
|
|
// from. It is also what the reverted cmd.WaitDelay backstop could not do.
|
|
func TestExecOpenclawCLIReapsForkedHelper(t *testing.T) {
|
|
t.Parallel()
|
|
pidFile := filepath.Join(t.TempDir(), "helper.pid")
|
|
bin := writeHelperForkingOpenclaw(t, pidFile)
|
|
|
|
if _, err := execOpenclawCLI(context.Background(), bin, "config", "get", "--json"); err != nil {
|
|
t.Fatalf("execOpenclawCLI: %v", err)
|
|
}
|
|
|
|
pid := readHelperPid(t, pidFile)
|
|
if !helperGone(pid, 5*time.Second) {
|
|
_ = syscall.Kill(pid, syscall.SIGKILL)
|
|
t.Fatalf("forked helper (pid %d) survived execOpenclawCLI — the "+
|
|
"orphan leak is back", pid)
|
|
}
|
|
}
|
|
|
|
// TestExecOpenclawCLIDoesNotSalvagePartialJSON pins that a `--json` subcommand
|
|
// still streaming when the deadline arrives is an error, not a truncated success.
|
|
func TestExecOpenclawCLIDoesNotSalvagePartialJSON(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "openclaw")
|
|
body := "#!/bin/sh\nprintf '{\"agents\":['\n" +
|
|
"while :; do printf '{\"id\":\"a\"},'; sleep 0.12; done\n"
|
|
writeTestExecutable(t, bin, []byte(body))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
|
defer cancel()
|
|
|
|
out, err := execOpenclawCLI(ctx, bin, "config", "get", "--json")
|
|
if err == nil {
|
|
t.Fatalf("partial JSON reported as success: %q", out)
|
|
}
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("errors.Is(err, context.DeadlineExceeded) must hold\ngot: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestOpenclawOutputCompleteRules pins which rule each subcommand shape gets, and
|
|
// that everything else gets none — which makes RunCollectQuiet wait for exit
|
|
// rather than guess.
|
|
func TestOpenclawOutputCompleteRules(t *testing.T) {
|
|
banner := []byte("┌────┐\n│ hi │\n└────┘\n")
|
|
partialJSON := []byte(`{"agents":[{"id":"a"},`)
|
|
fullJSON := []byte("{\"agents\":[]}\n")
|
|
|
|
// Only `--json` shapes get a rule. A JSON document has to parse as a whole, so
|
|
// no pause mid-write can be mistaken for a finished answer, and upstream keeps
|
|
// incidental output off a `--json` stdout entirely.
|
|
cases := []struct {
|
|
name string
|
|
env map[string]string
|
|
args []string
|
|
out []byte
|
|
want bool
|
|
}{
|
|
{name: "json, banner before the document never parses", args: []string{"config", "validate", "--json"}, out: append(append([]byte{}, banner...), fullJSON...), want: false},
|
|
{name: "json, validate payload", args: []string{"config", "validate", "--json"}, out: []byte(`{"valid":true,"path":"/home/u/.openclaw/openclaw.json"}`), want: true},
|
|
{name: "json, partial", args: []string{"config", "get", "--json"}, out: partialJSON, want: false},
|
|
{name: "json, complete", args: []string{"config", "get", "--json"}, out: fullJSON, want: true},
|
|
{name: "json, null is a real answer", args: []string{"config", "get", "agents.list", "--json"}, out: []byte("null\n"), want: true},
|
|
{name: "json, empty", args: []string{"agents", "list", "--json"}, out: nil, want: false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
rule := openclawOutputComplete(tc.args)
|
|
if rule == nil {
|
|
t.Fatalf("no completeness rule for %v", tc.args)
|
|
}
|
|
if got := rule(tc.out); got != tc.want {
|
|
t.Errorf("rule(%q) = %v, want %v", tc.out, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, args := range [][]string{
|
|
{"doctor"},
|
|
// `config file` deliberately has no rule any more. Its answer is the last
|
|
// line of a stdout it shares with Doctor and plugin warnings, so a rule
|
|
// could only ask "does the last line look like a path" — and review broke
|
|
// exactly that with a warning line that was one. See
|
|
// openclawActiveConfigPath, which prefers `config validate --json`.
|
|
{"config", "file"},
|
|
} {
|
|
if rule := openclawOutputComplete(args); rule != nil {
|
|
t.Errorf("%v must have no completeness rule, so the runner waits for exit "+
|
|
"instead of judging output whose shape cannot identify the answer", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestExecOpenclawCLIToleratesNonExitingCLI covers the second failure mode:
|
|
// a CLI that prints its answer and then never exits.
|
|
//
|
|
// Measured on a 2026.5.27 host, `openclaw config file` printed the path in ~250ms
|
|
// and then hung until killed, which reached the user as
|
|
//
|
|
// agent_error.process_failure (prepare execution environment: execenv:
|
|
// prepare openclaw config: locate openclaw active config:
|
|
// openclaw config file: context deadline exceeded (process: signal: killed))
|
|
//
|
|
// while the answer had been on stdout the whole time.
|
|
//
|
|
// Exercised on a `--json` subcommand, because that is where the tolerance lives
|
|
// now: a JSON document is recognisable as finished, so waiting is provably
|
|
// unnecessary. `config file` gave up its early return — see
|
|
// TestOpenclawActiveConfigPathFailsClosedWhenConfigFileNeverExits for what it
|
|
// does instead.
|
|
func TestExecOpenclawCLIToleratesNonExitingCLI(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "openclaw")
|
|
body := "#!/bin/sh\n" +
|
|
"printf '{\"mcp\":{}}\\n'\n" +
|
|
"sleep 300\n"
|
|
writeTestExecutable(t, bin, []byte(body))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
out, err := execOpenclawCLI(ctx, bin, "config", "get", "mcp", "--json")
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
t.Fatalf("execOpenclawCLI: %v", err)
|
|
}
|
|
if strings.TrimSpace(out) != `{"mcp":{}}` {
|
|
t.Errorf("stdout = %q, want the printed document", out)
|
|
}
|
|
// Loose on purpose: only has to sit far below the 60s ctx and the stub's 300s
|
|
// sleep, either of which a broken mechanism would take.
|
|
if elapsed > 10*time.Second {
|
|
t.Errorf("took %v — waited for an exit that never comes", elapsed)
|
|
}
|
|
}
|
|
|
|
// writeOpenclawConfigStub writes a stub CLI that answers `config validate --json`
|
|
// with validateOut, exiting validateExit, and `config file` with fileOut, appending
|
|
// trailing to the `config file` branch (for stubs that must linger or delay).
|
|
//
|
|
// validateExit is a parameter because a non-zero exit is the *normal* case for two
|
|
// of the three states measured on a real host: upstream exits 1 both for a missing
|
|
// config file and for an invalid one, and carries the path in the payload either
|
|
// way. A stub that could only exit 0 would test the rarest branch.
|
|
func writeOpenclawConfigStub(t *testing.T, validateOut string, validateExit int, fileOut, fileTrailing string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "openclaw")
|
|
body := "#!/bin/sh\n" +
|
|
"if [ \"$1\" = \"config\" ] && [ \"$2\" = \"validate\" ]; then\n" +
|
|
validateOut +
|
|
" exit " + strconv.Itoa(validateExit) + "\n" +
|
|
"fi\n" +
|
|
"if [ \"$1\" = \"config\" ] && [ \"$2\" = \"file\" ]; then\n" +
|
|
fileOut +
|
|
fileTrailing +
|
|
" exit 0\n" +
|
|
"fi\n" +
|
|
"exit 9\n"
|
|
writeTestExecutable(t, bin, []byte(body))
|
|
return bin
|
|
}
|
|
|
|
// TestOpenclawActiveConfigPathIgnoresAPathShapedWarning is the regression for the
|
|
// second review finding on #6275, and the reason `config file` is no longer the
|
|
// primary source of the config path.
|
|
//
|
|
// The reproduction from that review: a CLI whose last warning line is itself an
|
|
// existing path, followed by a pause longer than any fixed grace, followed by the
|
|
// real path. Judging `config file`'s stdout by shape returned the warning path
|
|
// after 5.57s — before the real answer existed — and expandOpenclawPath then
|
|
// turned it into a confident absolute path, so nothing downstream caught it.
|
|
//
|
|
// `config validate --json` reports the path in a named field, so the warning
|
|
// cannot be mistaken for it however long the CLI pauses. This test keeps the
|
|
// hostile `config file` branch in the stub precisely so that a future change that
|
|
// reinstates shape-based parsing as the primary path fails here.
|
|
func TestOpenclawActiveConfigPathIgnoresAPathShapedWarning(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
realPath := filepath.Join(dir, "openclaw.json")
|
|
warnPath := filepath.Join(dir, "plugin-cache.json")
|
|
for _, p := range []string{realPath, warnPath} {
|
|
if err := os.WriteFile(p, []byte("{}\n"), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", p, err)
|
|
}
|
|
}
|
|
|
|
bin := writeOpenclawConfigStub(t,
|
|
" printf '{\"valid\":true,\"path\":\"%s\"}\\n' '"+realPath+"'\n", 0,
|
|
" printf 'plugin cache written to:\\n'\n printf '%s\\n' '"+warnPath+"'\n",
|
|
" sleep 6\n printf '%s\\n' '"+realPath+"'\n")
|
|
|
|
start := time.Now()
|
|
got, exists, err := openclawActiveConfigPath(bin, 30*time.Second)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
t.Fatalf("openclawActiveConfigPath: %v", err)
|
|
}
|
|
if got != realPath {
|
|
t.Errorf("path = %q, want %q — a path-shaped warning line was accepted as the answer",
|
|
got, realPath)
|
|
}
|
|
if !exists {
|
|
t.Error("exists = false for a file that is on disk")
|
|
}
|
|
// The validate answer arrives immediately, so this must not have paid the
|
|
// `config file` branch's 6s pause at all.
|
|
if elapsed > 5*time.Second {
|
|
t.Errorf("took %v — the JSON answer was not preferred", elapsed)
|
|
}
|
|
}
|
|
|
|
// TestOpenclawActiveConfigPathFallsBackToConfigFile keeps the fallback honest: a
|
|
// CLI whose `config validate --json` is unusable (too old, or a shape we do not
|
|
// recognise) must still resolve through `config file`.
|
|
func TestOpenclawActiveConfigPathFallsBackToConfigFile(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
realPath := filepath.Join(dir, "openclaw.json")
|
|
if err := os.WriteFile(realPath, []byte("{}\n"), 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
validateOut string
|
|
validateExit int
|
|
}{
|
|
{"validate prints nothing", " :\n", 0},
|
|
{"validate prints non-JSON", " echo 'Unknown command: validate' >&2\n", 1},
|
|
{"validate omits the path field", " printf '{\"valid\":true}\\n'\n", 0},
|
|
{"validate reports a relative path", " printf '{\"valid\":false,\"path\":\"openclaw.json\"}\\n'\n", 1},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
bin := writeOpenclawConfigStub(t, tc.validateOut, tc.validateExit,
|
|
" printf '%s\\n' '"+realPath+"'\n", "")
|
|
got, exists, err := openclawActiveConfigPath(bin, 30*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("openclawActiveConfigPath: %v", err)
|
|
}
|
|
if got != realPath || !exists {
|
|
t.Errorf("path = %q exists = %v, want %q true", got, exists, realPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenclawActiveConfigPathReadsThePathFromANonZeroExit pins the case that is
|
|
// *normal* rather than exceptional, and which the first version of these tests
|
|
// missed: `config validate --json` exits 1 both for a missing config file and for
|
|
// an invalid one, and carries the path in its payload either way.
|
|
//
|
|
// A fresh install is the missing-file case, so treating a non-zero exit as "no
|
|
// answer" would break the most common first-run path. Both payloads below are the
|
|
// real shapes measured on OpenClaw 2026.7.1-2, with stderr empty in both.
|
|
func TestOpenclawActiveConfigPathReadsThePathFromANonZeroExit(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
presentPath := filepath.Join(dir, "openclaw.json")
|
|
if err := os.WriteFile(presentPath, []byte("{}\n"), 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
missingPath := filepath.Join(dir, "absent", "openclaw.json")
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
payload string
|
|
want string
|
|
wantExists bool
|
|
}{
|
|
{
|
|
// {"valid":false,"path":"…","error":"file not found"}, exit 1.
|
|
name: "file not found",
|
|
payload: `{"valid":false,"path":"` + missingPath + `","error":"file not found"}`,
|
|
want: missingPath,
|
|
wantExists: false,
|
|
},
|
|
{
|
|
// {"valid":false,"path":"…","issues":[…]}, exit 1. The user's config is
|
|
// broken, which openclaw itself will report when it runs; all this
|
|
// resolution step owes the caller is where the file is.
|
|
name: "invalid config",
|
|
payload: `{"valid":false,"path":"` + presentPath + `","issues":[{"path":"<root>","message":"Invalid input"}]}`,
|
|
want: presentPath,
|
|
wantExists: true,
|
|
},
|
|
{
|
|
// The healthy case still carries plugin warnings *inside* the document
|
|
// rather than as text before it, which is what keeps stdout a single
|
|
// parseable object.
|
|
name: "valid with warnings in the payload",
|
|
payload: `{"valid":true,"path":"` + presentPath + `","warnings":[{"path":"plugins.allow","message":"plugin not found: openclaw-lark"}]}`,
|
|
want: presentPath,
|
|
wantExists: true,
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
exit := 1
|
|
if strings.Contains(tc.payload, `"valid":true`) {
|
|
exit = 0
|
|
}
|
|
bin := writeOpenclawConfigStub(t,
|
|
" printf '%s\\n' '"+tc.payload+"'\n", exit,
|
|
" echo 'config file must not be consulted'\n", "")
|
|
|
|
got, exists, err := openclawActiveConfigPath(bin, 30*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("openclawActiveConfigPath: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("path = %q, want %q — the exit status must not decide whether "+
|
|
"the payload carries an answer", got, tc.want)
|
|
}
|
|
if exists != tc.wantExists {
|
|
t.Errorf("exists = %v, want %v", exists, tc.wantExists)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenclawActiveConfigPathFailsClosedWhenConfigFileNeverExits is the other
|
|
// side of dropping the `config file` completeness rule: with no way to recognise
|
|
// the answer, a CLI that prints something and then hangs must fail rather than
|
|
// have its output guessed at.
|
|
//
|
|
// This is a deliberate loss of tolerance on that one command, taken because the
|
|
// alternative was demonstrated to return a wrong path. The deadline is what makes
|
|
// it bounded, and MULTICA_OPENCLAW_CLI_TIMEOUT (#7142) is what makes it tunable.
|
|
func TestOpenclawActiveConfigPathFailsClosedWhenConfigFileNeverExits(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
realPath := filepath.Join(dir, "openclaw.json")
|
|
bin := writeOpenclawConfigStub(t,
|
|
" echo 'validate unsupported' >&2\n", 1,
|
|
" printf '%s\\n' '"+realPath+"'\n",
|
|
" sleep 300\n")
|
|
|
|
start := time.Now()
|
|
_, _, err := openclawActiveConfigPath(bin, 500*time.Millisecond)
|
|
elapsed := time.Since(start)
|
|
if err == nil {
|
|
t.Fatal("a `config file` that never exits must fail closed, not have its " +
|
|
"stdout accepted on shape")
|
|
}
|
|
if elapsed > 3*time.Second {
|
|
t.Errorf("took %v — the deadline did not bound the call", elapsed)
|
|
}
|
|
}
|