From 29aa60d0eb31a4097b8f1bd629e8028cb5b1f1d6 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 3 Oct 2026 16:08:52 +0800 Subject: [PATCH] =?UTF-8?q?test(platform):=20=E4=BF=AE=20Windows=20?= =?UTF-8?q?=E4=B8=8A=E5=B9=B3=E5=8F=B0=E8=84=86=E5=BC=B1=E7=9A=84=E6=96=AD?= =?UTF-8?q?=E8=A8=80=EF=BC=88=E4=B8=8D=E5=89=8A=E5=BC=B1=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=84=8F=E5=9B=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - trace/task: 亚 tick 耗时合法读到 0(本机时钟步长约 0.44ms),改为断言非负且 Duration == EndedAt-StartedAt - user/runtimeconfig: Windows 无法表示普通文件的 POSIX 权限位(Stat 恒 0666),0600 断言仅在非 Windows 生效,注释写明实现确实请求 0600 - exec: MSYS pwd 把同一目录渲染成 /tmp/...;改用「标记文件相对名解析」+「与显式 root 的 pwd 输出逐字相等」两条跨平台断言 + 变异验证 - 只读排查全仓同类脆弱断言,结果记入 DevDocs/工作小结 --- .../config/runtimeconfig/channels_test.go | 11 ++++- internal/config/runtimeconfig/plugins_test.go | 11 ++++- internal/runtime/exec/terminal_test.go | 49 +++++++++++++++---- internal/runtime/task/task_test.go | 8 ++- internal/runtime/trace/trace_test.go | 20 ++++++-- internal/user/save_test.go | 11 ++++- 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/internal/config/runtimeconfig/channels_test.go b/internal/config/runtimeconfig/channels_test.go index 9dbc93e..2d843ec 100644 --- a/internal/config/runtimeconfig/channels_test.go +++ b/internal/config/runtimeconfig/channels_test.go @@ -3,6 +3,7 @@ package runtimeconfig import ( "os" "path/filepath" + "runtime" "strings" "testing" @@ -176,8 +177,14 @@ func TestSaveChannelsRoundTrip(t *testing.T) { if err != nil { t.Fatalf("stat: %v", err) } - if info.Mode().Perm()&0o077 != 0 { - t.Fatalf("channels.json mode = %v, want owner-only (0600)", info.Mode().Perm()) + if runtime.GOOS != "windows" { + // Windows has no POSIX permission bits for regular files: Chmod only + // toggles the read-only flag and Stat reports 0666 regardless of the + // requested 0600, so this assertion is only meaningful on Unix-like + // platforms. SaveChannels still requests 0600 (see channels.go). + if info.Mode().Perm()&0o077 != 0 { + t.Fatalf("channels.json mode = %v, want owner-only (0600)", info.Mode().Perm()) + } } } diff --git a/internal/config/runtimeconfig/plugins_test.go b/internal/config/runtimeconfig/plugins_test.go index 039ca13..330f985 100644 --- a/internal/config/runtimeconfig/plugins_test.go +++ b/internal/config/runtimeconfig/plugins_test.go @@ -5,6 +5,7 @@ import ( "errors" "os" "path/filepath" + "runtime" "strings" "testing" ) @@ -39,8 +40,14 @@ func TestSaveLoadPlugins_RoundTrip(t *testing.T) { if err != nil { t.Fatalf("stat: %v", err) } - if perm := info.Mode().Perm(); perm != 0o600 { - t.Fatalf("mode = %o, want 600", perm) + if runtime.GOOS != "windows" { + // Windows has no POSIX permission bits for regular files: Chmod only + // toggles the read-only flag and Stat reports 0666 regardless of the + // requested 0600, so this assertion is only meaningful on Unix-like + // platforms. SavePlugins still requests 0600 (see plugins.go). + if perm := info.Mode().Perm(); perm != 0o600 { + t.Fatalf("mode = %o, want 600", perm) + } } got, err := LoadPlugins(path) if err != nil { diff --git a/internal/runtime/exec/terminal_test.go b/internal/runtime/exec/terminal_test.go index fe21157..c7eeccf 100644 --- a/internal/runtime/exec/terminal_test.go +++ b/internal/runtime/exec/terminal_test.go @@ -144,20 +144,51 @@ func TestTerminal_Injection_Rejected(t *testing.T) { func TestTerminal_WorkDir_DefaultToRoot(t *testing.T) { t.Parallel() - requireBin(t, "pwd") - root := t.TempDir() - ex := newEnabledExecutor(t, enabledCfg("pwd", root)) - res, err := ex.Execute(context.Background(), ExecRequest{Command: []string{"pwd"}}) + // Probe the default working directory without comparing rendered path + // strings. On Windows the command may resolve to Git's MSYS coreutils, which + // reports the Windows cwd it was handed as a POSIX path (the user Temp + // directory comes back as /tmp/...), so a "Stdout contains root" check is not + // portable. A marker file that exists only in the sandbox root, read through + // a relative name, is found only when the process really runs in that root. + const ( + probeFile = "cwd-probe.txt" + probeBody = "cwd-probe" + ) + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, probeFile), []byte(probeBody), 0o600); err != nil { + t.Fatalf("write probe: %v", err) + } + requireBin(t, "cat") + catEx := newEnabledExecutor(t, enabledCfg("cat", root)) + res, err := catEx.Execute(context.Background(), ExecRequest{Command: []string{"cat", probeFile}}) if err != nil { t.Fatalf("Execute: %v", err) } - realRoot := root - if r, err := filepath.EvalSymlinks(root); err == nil { - realRoot = r + if res.ExitCode != 0 || res.Stdout != probeBody { + t.Errorf("cat %s with no WorkDir: exit=%d stdout=%q stderr=%q, want %q read from the sandbox root %s", + probeFile, res.ExitCode, res.Stdout, res.Stderr, probeBody, root) } - if !strings.Contains(res.Stdout, realRoot) { - t.Errorf("Stdout = %q, want contain %q", res.Stdout, realRoot) + + // And the default must be the root itself, not a parent or a subdirectory: + // pwd with no WorkDir must print exactly what pwd prints for WorkDir=root. + // Both runs share pwd's own rendering, so the comparison stays platform + // independent. + requireBin(t, "pwd") + pwdEx := newEnabledExecutor(t, enabledCfg("pwd", root)) + deflt, err := pwdEx.Execute(context.Background(), ExecRequest{Command: []string{"pwd"}}) + if err != nil { + t.Fatalf("Execute pwd (default workdir): %v", err) + } + explicit, err := pwdEx.Execute(context.Background(), ExecRequest{ + Command: []string{"pwd"}, + WorkDir: root, + }) + if err != nil { + t.Fatalf("Execute pwd (WorkDir=%s): %v", root, err) + } + if got, want := strings.TrimSpace(deflt.Stdout), strings.TrimSpace(explicit.Stdout); got != want { + t.Errorf("pwd with no WorkDir = %q, want the sandbox root as pwd renders it with WorkDir=%s: %q", got, root, want) } } diff --git a/internal/runtime/task/task_test.go b/internal/runtime/task/task_test.go index 7945786..c367773 100644 --- a/internal/runtime/task/task_test.go +++ b/internal/runtime/task/task_test.go @@ -219,8 +219,12 @@ func TestManager_ForegroundTask_FastCompletes(t *testing.T) { if snap.Output.Stdout != "fast out" { t.Errorf("Stdout=%q, want fast out", snap.Output.Stdout) } - if !snap.FinishedAt.After(snap.StartedAt) { - t.Errorf("FinishedAt=%v should be after StartedAt=%v", snap.FinishedAt, snap.StartedAt) + // The monotonic clock may be too coarse to tick between StartedAt and + // FinishedAt, so a task that finishes in less than one tick legitimately + // reads the same stamp for both. Assert the platform-independent invariant + // instead: the finish time is never before the start time. + if snap.FinishedAt.Before(snap.StartedAt) { + t.Errorf("FinishedAt=%v should not be before StartedAt=%v", snap.FinishedAt, snap.StartedAt) } } diff --git a/internal/runtime/trace/trace_test.go b/internal/runtime/trace/trace_test.go index 073139f..506b2b6 100644 --- a/internal/runtime/trace/trace_test.go +++ b/internal/runtime/trace/trace_test.go @@ -72,8 +72,15 @@ func TestSpan_Lifecycle(t *testing.T) { if sp.EndedAt.Before(before) { t.Fatalf("EndedAt %v is before snapshot %v", sp.EndedAt, before) } - if sp.Duration <= 0 { - t.Fatalf("Duration = %v, want > 0", sp.Duration) + // The monotonic clock may be too coarse to tick between StartedAt and + // EndedAt, so a sub-tick span legitimately reads Duration == 0. Assert the + // platform-independent invariant instead: Duration is non-negative and is + // exactly the delta of the two stamps. + if sp.Duration < 0 { + t.Fatalf("Duration = %v, want >= 0", sp.Duration) + } + if want := sp.EndedAt.Sub(sp.StartedAt); sp.Duration != want { + t.Fatalf("Duration = %v, want EndedAt-StartedAt = %v", sp.Duration, want) } if len(sp.Events) != 1 || sp.Events[0].Type != "token" { t.Fatalf("Events = %+v, want one 'token' event", sp.Events) @@ -241,8 +248,13 @@ func TestJSONRecorder_Output(t *testing.T) { if childRec.Status != SpanStatusOK { t.Fatalf("childRec.Status = %q, want ok", childRec.Status) } - if childRec.Duration <= 0 { - t.Fatalf("childRec.Duration = %v, want > 0", childRec.Duration) + // As in TestSpan_Lifecycle: a sub-tick span reads Duration == 0 on platforms + // with a coarse monotonic clock, so assert the recorded delta invariant. + if childRec.Duration < 0 { + t.Fatalf("childRec.Duration = %v, want >= 0", childRec.Duration) + } + if want := childRec.EndedAt.Sub(childRec.StartedAt); childRec.Duration != want { + t.Fatalf("childRec.Duration = %v, want EndedAt-StartedAt = %v", childRec.Duration, want) } if childRec.Props["model"] != "gpt-test" { t.Fatalf("childRec.Props = %+v, want model key", childRec.Props) diff --git a/internal/user/save_test.go b/internal/user/save_test.go index 2e30694..25c63fa 100644 --- a/internal/user/save_test.go +++ b/internal/user/save_test.go @@ -3,6 +3,7 @@ package user import ( "os" "path/filepath" + "runtime" "testing" ) @@ -35,8 +36,14 @@ notes: "主人。陪伴对象。" if err != nil { t.Fatalf("stat saved profile: %v", err) } - if perm := info.Mode().Perm(); perm != 0o600 { - t.Errorf("saved file mode = %o, want 0600", perm) + if runtime.GOOS != "windows" { + // Windows has no POSIX permission bits for regular files: Chmod only + // toggles the read-only flag and Stat reports 0666 regardless of the + // requested 0600, so this assertion is only meaningful on Unix-like + // platforms. SaveOwnerProfile still requests 0600 (see save.go). + if perm := info.Mode().Perm(); perm != 0o600 { + t.Errorf("saved file mode = %o, want 0600", perm) + } } got, err := LoadOwnerProfile(dst)