mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 22:13:16 +00:00
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:
1 parent
826d516f24
commit
99da7edd01
6 files changed
+111
-62
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}`),
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user