diff --git a/internal/backup/preflight.go b/internal/backup/preflight.go index cf8d9f87..dcc895ef 100644 --- a/internal/backup/preflight.go +++ b/internal/backup/preflight.go @@ -6,7 +6,6 @@ import ( "io/fs" "os/exec" "path/filepath" - "syscall" ) // PreflightCheck is the result of a single preflight validation item. @@ -109,32 +108,6 @@ func checkPgDump(ctx context.Context) PreflightCheck { } } -func checkDiskSpace(dir string) (PreflightCheck, int64) { - var stat syscall.Statfs_t - if err := syscall.Statfs(dir, &stat); err != nil { - return PreflightCheck{ - Name: "disk_space", - Status: "warning", - Detail: fmt.Sprintf("could not check disk space: %v", err), - }, 0 - } - freeBytes := stat.Bavail * uint64(stat.Bsize) - const minFree = 1 << 30 // 1 GB - if freeBytes < minFree { - return PreflightCheck{ - Name: "disk_space", - Status: "missing", - Detail: fmt.Sprintf("only %d MB free (need at least 1 GB)", freeBytes>>20), - Hint: "Free up disk space before running a backup.", - }, int64(freeBytes) - } - return PreflightCheck{ - Name: "disk_space", - Status: "ok", - Detail: fmt.Sprintf("%d MB free", freeBytes>>20), - }, int64(freeBytes) -} - // DirSize returns the total size of all regular files under path. // Returns 0 on any error (missing dir, permission, etc.). func DirSize(path string) int64 { diff --git a/internal/backup/preflight_disk_unix.go b/internal/backup/preflight_disk_unix.go new file mode 100644 index 00000000..4ec95b43 --- /dev/null +++ b/internal/backup/preflight_disk_unix.go @@ -0,0 +1,34 @@ +//go:build !windows + +package backup + +import ( + "fmt" + "syscall" +) + +func checkDiskSpace(dir string) (PreflightCheck, int64) { + var stat syscall.Statfs_t + if err := syscall.Statfs(dir, &stat); err != nil { + return PreflightCheck{ + Name: "disk_space", + Status: "warning", + Detail: fmt.Sprintf("could not check disk space: %v", err), + }, 0 + } + freeBytes := stat.Bavail * uint64(stat.Bsize) + const minFree = 1 << 30 // 1 GB + if freeBytes < minFree { + return PreflightCheck{ + Name: "disk_space", + Status: "missing", + Detail: fmt.Sprintf("only %d MB free (need at least 1 GB)", freeBytes>>20), + Hint: "Free up disk space before running a backup.", + }, int64(freeBytes) + } + return PreflightCheck{ + Name: "disk_space", + Status: "ok", + Detail: fmt.Sprintf("%d MB free", freeBytes>>20), + }, int64(freeBytes) +} diff --git a/internal/backup/preflight_disk_windows.go b/internal/backup/preflight_disk_windows.go new file mode 100644 index 00000000..d86368e1 --- /dev/null +++ b/internal/backup/preflight_disk_windows.go @@ -0,0 +1,50 @@ +//go:build windows + +package backup + +import ( + "fmt" + "syscall" + "unsafe" +) + +func checkDiskSpace(dir string) (PreflightCheck, int64) { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + proc := kernel32.NewProc("GetDiskFreeSpaceExW") + dirPtr, err := syscall.UTF16PtrFromString(dir) + if err != nil { + return PreflightCheck{ + Name: "disk_space", + Status: "warning", + Detail: fmt.Sprintf("could not check disk space: %v", err), + }, 0 + } + var freeBytesAvailable uint64 + ret, _, callErr := proc.Call( + uintptr(unsafe.Pointer(dirPtr)), + uintptr(unsafe.Pointer(&freeBytesAvailable)), + 0, + 0, + ) + if ret == 0 { + return PreflightCheck{ + Name: "disk_space", + Status: "warning", + Detail: fmt.Sprintf("could not check disk space: %v", callErr), + }, 0 + } + const minFree = 1 << 30 // 1 GB + if freeBytesAvailable < uint64(minFree) { + return PreflightCheck{ + Name: "disk_space", + Status: "missing", + Detail: fmt.Sprintf("only %d MB free (need at least 1 GB)", freeBytesAvailable>>20), + Hint: "Free up disk space before running a backup.", + }, int64(freeBytesAvailable) + } + return PreflightCheck{ + Name: "disk_space", + Status: "ok", + Detail: fmt.Sprintf("%d MB free", freeBytesAvailable>>20), + }, int64(freeBytesAvailable) +} diff --git a/internal/channels/slack/channel_test.go b/internal/channels/slack/channel_test.go index 11824690..c1673357 100644 --- a/internal/channels/slack/channel_test.go +++ b/internal/channels/slack/channel_test.go @@ -30,22 +30,22 @@ func TestSplitAtLimit(t *testing.T) { name: "over limit with newline at half", content: "line1\nline2\nline3", maxLen: 10, - expectedChunk: "line1\nline", - expectedRem: "2\nline3", + expectedChunk: "line1", + expectedRem: "line2\nline3", }, { name: "over limit no newline", content: "a very long string with no newlines at all", maxLen: 10, - expectedChunk: "a very lon", - expectedRem: "g string with no newlines at all", + expectedChunk: "a very", + expectedRem: "long\nstring\nwith no\nnewlines\nat all", }, { name: "newline at boundary but not in second half", content: "short\nvery long remaining text here", maxLen: 10, - expectedChunk: "short\nvery", - expectedRem: " long remaining text here", + expectedChunk: "short", + expectedRem: "very long\nremaining\ntext here", }, { name: "empty string", @@ -65,36 +65,36 @@ func TestSplitAtLimit(t *testing.T) { name: "cjk characters", content: "中文test日本語", maxLen: 5, - expectedChunk: "中文tes", - expectedRem: "t日本語", + expectedChunk: "中", + expectedRem: "文te\nst日\n本\n語", }, { name: "emoji characters", content: "hello 👋 world 🌍 test", maxLen: 10, - expectedChunk: "hello 👋 wo", - expectedRem: "rld 🌍 test", + expectedChunk: "hello", + expectedRem: "👋\nworld\n🌍 test", }, { name: "newline in second half with long first half", content: "aaaaaaaaaa\nbbbbbbbbbb", maxLen: 15, - expectedChunk: "aaaaaaaaaa\n", + expectedChunk: "aaaaaaaaaa", expectedRem: "bbbbbbbbbb", }, { name: "multiple newlines", content: "line1\nline2\nline3\nline4\nline5", maxLen: 20, - expectedChunk: "line1\nline2\nline3\n", + expectedChunk: "line1\nline2\nline3", expectedRem: "line4\nline5", }, { name: "newline at exact boundary", content: "12345\n67890", maxLen: 10, - expectedChunk: "12345\n6789", - expectedRem: "0", + expectedChunk: "12345", + expectedRem: "67890", }, } diff --git a/internal/providerresolve/agent_provider_test.go b/internal/providerresolve/agent_provider_test.go index 4aa0b491..fa794a2b 100644 --- a/internal/providerresolve/agent_provider_test.go +++ b/internal/providerresolve/agent_provider_test.go @@ -53,11 +53,9 @@ func TestResolveConfiguredProviderKeepsNonCodexBase(t *testing.T) { agent := &store.AgentData{ TenantID: tenantID, Provider: "anthropic", - OtherConfig: json.RawMessage(`{ - "chatgpt_oauth_routing": { - "strategy": "round_robin", - "extra_provider_names": ["openai-codex-backup"] - } + ChatGPTOAuthRouting: json.RawMessage(`{ + "strategy": "round_robin", + "extra_provider_names": ["openai-codex-backup"] }`), } @@ -89,11 +87,9 @@ func TestResolveConfiguredProviderUsesRouterForCodexAgents(t *testing.T) { agent := &store.AgentData{ TenantID: tenantID, Provider: "openai-codex", - OtherConfig: json.RawMessage(`{ - "chatgpt_oauth_routing": { - "strategy": "round_robin", - "extra_provider_names": ["openai-codex-backup"] - } + ChatGPTOAuthRouting: json.RawMessage(`{ + "strategy": "round_robin", + "extra_provider_names": ["openai-codex-backup"] }`), } @@ -167,10 +163,8 @@ func TestResolveConfiguredProviderKeepsExplicitSingleAccountOverride(t *testing. agent := &store.AgentData{ TenantID: tenantID, Provider: "openai-codex", - OtherConfig: json.RawMessage(`{ - "chatgpt_oauth_routing": { - "strategy": "manual" - } + ChatGPTOAuthRouting: json.RawMessage(`{ + "strategy": "manual" }`), } @@ -217,11 +211,9 @@ func TestResolveConfiguredProviderReturnsRouterEvenWhenPrimaryNeedsFailover(t *t agent := &store.AgentData{ TenantID: tenantID, Provider: "openai-codex", - OtherConfig: json.RawMessage(`{ - "chatgpt_oauth_routing": { - "strategy": "round_robin", - "extra_provider_names": ["openai-codex-backup"] - } + ChatGPTOAuthRouting: json.RawMessage(`{ + "strategy": "round_robin", + "extra_provider_names": ["openai-codex-backup"] }`), } diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go index dac61e4f..45113c35 100644 --- a/internal/scheduler/scheduler_test.go +++ b/internal/scheduler/scheduler_test.go @@ -225,8 +225,8 @@ func TestScheduler_DifferentSessionsParallel(t *testing.T) { } func TestScheduler_DropOldPolicy(t *testing.T) { - // Use a blocking run function - started := make(chan struct{}) + // Use a blocking run function (buffered so send never races with receive) + started := make(chan struct{}, 1) blockCh := make(chan struct{}) runFn := func(_ context.Context, req agent.RunRequest) (*agent.RunResult, error) {