fix(ci): resolve 4 workflow failures — tests, race, Windows build

- slack: update splitAtLimit tests to match ChunkMarkdown behavior
- providerresolve: use ChatGPTOAuthRouting field instead of OtherConfig
- scheduler: buffer started channel to prevent race in DropOldPolicy
- backup: split checkDiskSpace into platform files (Unix/Windows)
This commit is contained in:
viettranx committed 2026-04-09 21:45:55 +07:00
1 parent 826d516f24
commit 99da7edd01
6 files changed
+111 -62

No files matched your search

-27
View File
@@ -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 {
+34
View File
@@ -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)
}
+50
View File
@@ -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)
}
+14 -14
View File
@@ -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",
},
}
+11 -19
View File
@@ -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"]
}`),
}
+2 -2
View File
@@ -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) {