mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-15 03:32:43 +08:00
Compare commits
2 Commits
myclaude-m
...
fix/preven
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a09c103cfb | ||
|
|
b3f8fcfea6 |
257
CHANGELOG.md
257
CHANGELOG.md
@@ -1,129 +1,198 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 5.2.0 - 2025-12-13
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### 🚀 Core Features
|
## [5.2.3] - 2025-12-15
|
||||||
|
|
||||||
#### Skills System Enhancements
|
### 🐛 Bug Fixes
|
||||||
- **New Skills**: Added `codeagent`, `product-requirements`, `prototype-prompt-generator` to `skill-rules.json`
|
|
||||||
- **Auto-Activation**: Skills automatically trigger based on keyword/pattern matching via hooks
|
|
||||||
- **Backward Compatibility**: Retained `skills/codex/SKILL.md` for existing workflows
|
|
||||||
|
|
||||||
#### Multi-Backend Support (codeagent-wrapper)
|
- *(parser)* 修复 bufio.Scanner token too long 错误 (#64)
|
||||||
- **Renamed**: `codex-wrapper` → `codeagent-wrapper` with pluggable backend architecture
|
|
||||||
- **Three Backends**: Codex (default), Claude, Gemini via `--backend` flag
|
|
||||||
- **Smart Parser**: Auto-detects backend JSON stream formats
|
|
||||||
- **Session Resume**: All backends support `-r <session_id>` cross-session resume
|
|
||||||
- **Parallel Execution**: DAG task scheduling with global and per-task backend configuration
|
|
||||||
- **Concurrency Control**: `CODEAGENT_MAX_PARALLEL_WORKERS` env var limits concurrent tasks (max 100)
|
|
||||||
- **Test Coverage**: 93.4% (backend.go 100%, config.go 97.8%, executor.go 96.4%)
|
|
||||||
|
|
||||||
#### Dev Workflow
|
### 🧪 Testing
|
||||||
- **`/dev`**: 6-step minimal dev workflow with mandatory 90% test coverage
|
|
||||||
|
|
||||||
#### Hooks System
|
- 同步测试中的版本号至 5.2.3
|
||||||
- **UserPromptSubmit**: Auto-activate skills based on context
|
|
||||||
- **PostToolUse**: Auto-validation/formatting after tool execution
|
|
||||||
- **Stop**: Cleanup and reporting on session end
|
|
||||||
- **Examples**: Skill auto-activation, pre-commit checks
|
|
||||||
|
|
||||||
#### Skills System
|
## [5.2.2] - 2025-12-13
|
||||||
- **Auto-Activation**: `skill-rules.json` regex trigger rules
|
|
||||||
- **codeagent skill**: Multi-backend wrapper integration
|
|
||||||
- **Modular Design**: Easy to extend with custom skills
|
|
||||||
|
|
||||||
#### Installation System Enhancements
|
### 🧪 Testing
|
||||||
- **`merge_json` operation**: Auto-merge `settings.json` configuration
|
|
||||||
- **Modular Installation**: `python3 install.py --module dev`
|
- Fix tests for ClaudeBackend default --dangerously-skip-permissions
|
||||||
- **Verbose Logging**: `--verbose/-v` enables terminal real-time output
|
|
||||||
- **Streaming Output**: `op_run_command` streams bash script execution
|
### ⚙️ Miscellaneous Tasks
|
||||||
- **Configuration Cleanup**: Removed deprecated `gh` module from `config.json`
|
|
||||||
|
- *(v5.2.2)* Bump version and clean up documentation
|
||||||
|
|
||||||
|
## [5.2.0] - 2025-12-13
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(dev-workflow)* 替换 Codex 为 codeagent 并添加 UI 自动检测
|
||||||
|
- *(codeagent-wrapper)* 完整多后端支持与安全优化
|
||||||
|
- *(install)* 添加终端日志输出和 verbose 模式
|
||||||
|
- *(v5.2.0)* Improve release notes and installation scripts
|
||||||
|
- *(v5.2.0)* Complete skills system integration and config cleanup
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(merge)* 修复master合并后的编译和测试问题
|
||||||
|
- *(parallel)* 修复并行执行启动横幅重复打印问题
|
||||||
|
- *(ci)* 移除 .claude 配置文件验证步骤
|
||||||
|
- *(codeagent-wrapper)* 重构信号处理逻辑避免重复 nil 检查
|
||||||
|
- *(codeagent-wrapper)* 修复权限标志逻辑和版本号测试
|
||||||
|
- *(install)* Op_run_command 实时流式输出
|
||||||
|
- *(codeagent-wrapper)* 异常退出时显示最近错误信息
|
||||||
|
- *(codeagent-wrapper)* Remove binary artifacts and improve error messages
|
||||||
|
- *(codeagent-wrapper)* Use -r flag for claude backend resume
|
||||||
|
- *(install)* Clarify module list shows default state not enabled
|
||||||
|
- *(codeagent-wrapper)* Use -r flag for gemini backend resume
|
||||||
|
- *(codeagent-wrapper)* Add worker limit cap and remove legacy alias
|
||||||
|
- *(codeagent-wrapper)* Fix race condition in stdout parsing
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- *(pr-53)* 调整文件命名和技能定义
|
||||||
|
|
||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
- `docs/architecture.md` (21KB): Architecture overview with ASCII diagrams
|
- *(changelog)* Remove GitHub workflow related content
|
||||||
- `docs/CODEAGENT-WRAPPER.md` (9KB): Complete usage guide
|
|
||||||
- `docs/HOOKS.md` (4KB): Customization guide
|
|
||||||
- `README.md`: Added documentation index, corrected default backend description
|
|
||||||
|
|
||||||
### 🔧 Important Fixes
|
### 🧪 Testing
|
||||||
|
|
||||||
#### codeagent-wrapper
|
- *(codeagent-wrapper)* 添加 ExtractRecentErrors 单元测试
|
||||||
- Fixed Claude/Gemini backend `-C` (workdir) and `-r` (resume) parameter support (codeagent-wrapper/backend.go:80-120)
|
|
||||||
- Corrected Claude backend permission flag logic `if cfg.SkipPermissions` (codeagent-wrapper/backend.go:95)
|
|
||||||
- Fixed parallel mode startup banner duplication (codeagent-wrapper/main.go:184-194 removed)
|
|
||||||
- Extract and display recent errors on abnormal exit `Logger.ExtractRecentErrors()` (codeagent-wrapper/logger.go:156)
|
|
||||||
- Added task block index to parallel config error messages (codeagent-wrapper/config.go:245)
|
|
||||||
- Refactored signal handling logic to avoid duplicate nil checks (codeagent-wrapper/main.go:290-305)
|
|
||||||
- Removed binary artifacts from tracking (codeagent-wrapper, *.test, coverage.out)
|
|
||||||
|
|
||||||
#### Installation Scripts
|
### ⚙️ Miscellaneous Tasks
|
||||||
- Fixed issue #55: `op_run_command` uses Popen + selectors for real-time streaming output
|
|
||||||
- Fixed issue #56: Display recent errors instead of entire log
|
|
||||||
- Changed module list header from "Enabled" to "Default" to avoid ambiguity
|
|
||||||
|
|
||||||
#### CI/CD
|
- *(v5.2.0)* Update CHANGELOG and remove deprecated test files
|
||||||
- Removed `.claude/` config file validation step (.github/workflows/ci.yml:45)
|
|
||||||
- Updated version test case from 5.1.0 → 5.2.0 (codeagent-wrapper/main_test.go:23)
|
|
||||||
|
|
||||||
#### Commands & Documentation
|
## [5.1.4] - 2025-12-09
|
||||||
- Reverted `skills/codex/SKILL.md` to `codex-wrapper` for backward compatibility
|
|
||||||
|
|
||||||
#### dev-workflow
|
### 🐛 Bug Fixes
|
||||||
- Replaced Codex skill → codeagent skill throughout
|
|
||||||
- Added UI auto-detection: backend tasks use codex, UI tasks use gemini
|
|
||||||
- Corrected agent name: `develop-doc-generator` → `dev-plan-generator`
|
|
||||||
|
|
||||||
### ⚙️ Configuration & Environment Variables
|
- *(parallel)* 任务启动时立即返回日志文件路径以支持实时调试
|
||||||
|
|
||||||
#### New Environment Variables
|
## [5.1.3] - 2025-12-08
|
||||||
- `CODEAGENT_SKIP_PERMISSIONS`: Control permission check behavior
|
|
||||||
- Claude backend defaults to `--dangerously-skip-permissions` enabled, set to `true` to disable
|
|
||||||
- Codex/Gemini backends default to permission checks enabled, set to `true` to skip
|
|
||||||
- `CODEAGENT_MAX_PARALLEL_WORKERS`: Parallel task concurrency limit (default: unlimited, recommended: 8, max: 100)
|
|
||||||
|
|
||||||
#### Configuration Files
|
### 🐛 Bug Fixes
|
||||||
- `config.schema.json`: Added `op_merge_json` schema validation
|
|
||||||
|
|
||||||
### ⚠️ Breaking Changes
|
- *(test)* Resolve CI timing race in TestFakeCmdInfra
|
||||||
|
|
||||||
**codex-wrapper → codeagent-wrapper rename**
|
## [5.1.2] - 2025-12-08
|
||||||
|
|
||||||
**Migration**:
|
### 🐛 Bug Fixes
|
||||||
```bash
|
|
||||||
python3 install.py --module dev --force
|
|
||||||
```
|
|
||||||
|
|
||||||
**Backward Compatibility**: `codex-wrapper/main.go` provides compatibility entry point
|
- 修复channel同步竞态条件和死锁问题
|
||||||
|
|
||||||
### 📦 Installation
|
## [5.1.1] - 2025-12-08
|
||||||
|
|
||||||
```bash
|
### 🐛 Bug Fixes
|
||||||
# Install dev module
|
|
||||||
python3 install.py --module dev
|
|
||||||
|
|
||||||
# List all modules
|
- *(test)* Resolve data race on forceKillDelay with atomic operations
|
||||||
python3 install.py --list-modules
|
- 增强日志清理的安全性和可靠性
|
||||||
|
|
||||||
# Verbose logging mode
|
### 💼 Other
|
||||||
python3 install.py --module dev --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🧪 Test Results
|
- Resolve signal handling conflict preserving testability and Windows support
|
||||||
|
|
||||||
✅ **All tests passing**
|
### 🧪 Testing
|
||||||
- Overall coverage: 93.4%
|
|
||||||
- Security scan: 0 issues (gosec)
|
|
||||||
- Linting: Pass
|
|
||||||
|
|
||||||
### 📄 Related PRs & Issues
|
- 补充测试覆盖提升至 89.3%
|
||||||
|
|
||||||
- PR #53: Enterprise Workflow with Multi-Backend Support
|
## [5.1.0] - 2025-12-07
|
||||||
- Issue #55: Installation script execution not visible
|
|
||||||
- Issue #56: Unfriendly error logging on abnormal exit
|
|
||||||
|
|
||||||
### 👥 Contributors
|
### 🚀 Features
|
||||||
|
|
||||||
- Claude Sonnet 4.5
|
- Implement enterprise workflow with multi-backend support
|
||||||
- Claude Opus 4.5
|
- *(cleanup)* 添加启动时清理日志的功能和--cleanup标志支持
|
||||||
- SWE-Agent-Bot
|
|
||||||
|
## [5.0.0] - 2025-12-05
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Implement modular installation system
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(codex-wrapper)* Defer startup log until args parsed
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- Remove deprecated plugin modules
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Rewrite documentation for v5.0 modular architecture
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Clarify unit-test coverage levels in requirement questions
|
||||||
|
|
||||||
|
## [4.8.2] - 2025-12-02
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(codex-wrapper)* Capture and include stderr in error messages
|
||||||
|
- Correct Go version in go.mod from 1.25.3 to 1.21
|
||||||
|
- Make forceKillDelay testable to prevent signal test timeout
|
||||||
|
- Skip signal test in CI environment
|
||||||
|
|
||||||
|
## [4.8.1] - 2025-12-01
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(codex-wrapper)* Improve --parallel parameter validation and docs
|
||||||
|
|
||||||
|
### 🎨 Styling
|
||||||
|
|
||||||
|
- *(codex-skill)* Replace emoji with text labels
|
||||||
|
|
||||||
|
## [4.7.3] - 2025-11-29
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add async logging to temp file with lifecycle management
|
||||||
|
- Add parallel execution support to codex-wrapper
|
||||||
|
- Add session resume support and improve output format
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(logger)* 保留日志文件以便程序退出后调试并完善日志输出功能
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Improve codex skill parameter best practices
|
||||||
|
|
||||||
|
## [4.7.2] - 2025-11-28
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(main)* Improve buffer size and streamline message extraction
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- *(ParseJSONStream)* 增加对超大单行文本和非字符串文本的处理测试
|
||||||
|
|
||||||
|
## [4.7] - 2025-11-27
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Update repository URLs to cexll/myclaude
|
||||||
|
|
||||||
|
## [4.4] - 2025-11-22
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- 支持通过环境变量配置 skills 模型
|
||||||
|
|
||||||
|
## [4.1] - 2025-11-04
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- 新增 /enhance-prompt 命令并更新所有 README 文档
|
||||||
|
|
||||||
|
## [3.1] - 2025-09-17
|
||||||
|
|
||||||
|
### 💼 Other
|
||||||
|
|
||||||
|
- Sync READMEs with actual commands/agents; remove nonexistent commands; enhance requirements-pilot with testing decision gate and options.
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
|||||||
// args = append(args, "--dangerously-skip-permissions")
|
// args = append(args, "--dangerously-skip-permissions")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Prevent infinite recursion: disable all setting sources (user, project, local)
|
||||||
|
// This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent
|
||||||
|
args = append(args, "--setting-sources", "")
|
||||||
|
|
||||||
if cfg.Mode == "resume" {
|
if cfg.Mode == "resume" {
|
||||||
if cfg.SessionID != "" {
|
if cfg.SessionID != "" {
|
||||||
// Claude CLI uses -r <session_id> for resume.
|
// Claude CLI uses -r <session_id> for resume.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
|||||||
t.Run("new mode uses workdir without skip by default", func(t *testing.T) {
|
t.Run("new mode uses workdir without skip by default", func(t *testing.T) {
|
||||||
cfg := &Config{Mode: "new", WorkDir: "/repo"}
|
cfg := &Config{Mode: "new", WorkDir: "/repo"}
|
||||||
got := backend.BuildArgs(cfg, "todo")
|
got := backend.BuildArgs(cfg, "todo")
|
||||||
want := []string{"-p", "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose", "todo"}
|
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("got %v, want %v", got, want)
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
|||||||
t.Run("new mode opt-in skip permissions with default workdir", func(t *testing.T) {
|
t.Run("new mode opt-in skip permissions with default workdir", func(t *testing.T) {
|
||||||
cfg := &Config{Mode: "new", SkipPermissions: true}
|
cfg := &Config{Mode: "new", SkipPermissions: true}
|
||||||
got := backend.BuildArgs(cfg, "-")
|
got := backend.BuildArgs(cfg, "-")
|
||||||
want := []string{"-p", "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose", "-"}
|
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "-"}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("got %v, want %v", got, want)
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
|||||||
t.Run("resume mode uses session id and omits workdir", func(t *testing.T) {
|
t.Run("resume mode uses session id and omits workdir", func(t *testing.T) {
|
||||||
cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
|
cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
|
||||||
got := backend.BuildArgs(cfg, "resume-task")
|
got := backend.BuildArgs(cfg, "resume-task")
|
||||||
want := []string{"-p", "--dangerously-skip-permissions", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("got %v, want %v", got, want)
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
|||||||
t.Run("resume mode without session still returns base flags", func(t *testing.T) {
|
t.Run("resume mode without session still returns base flags", func(t *testing.T) {
|
||||||
cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
|
cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
|
||||||
got := backend.BuildArgs(cfg, "follow-up")
|
got := backend.BuildArgs(cfg, "follow-up")
|
||||||
want := []string{"-p", "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose", "follow-up"}
|
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("got %v, want %v", got, want)
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,25 +122,7 @@ type parseResult struct {
|
|||||||
threadID string
|
threadID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type taskLoggerContextKey struct{}
|
var runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||||
|
|
||||||
func withTaskLogger(ctx context.Context, logger *Logger) context.Context {
|
|
||||||
if ctx == nil || logger == nil {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, taskLoggerContextKey{}, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func taskLoggerFromContext(ctx context.Context) *Logger {
|
|
||||||
if ctx == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logger, _ := ctx.Value(taskLoggerContextKey{}).(*Logger)
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultRunCodexTaskFn is the default implementation of runCodexTaskFn (exposed for test reset)
|
|
||||||
func defaultRunCodexTaskFn(task TaskSpec, timeout int) TaskResult {
|
|
||||||
if task.WorkDir == "" {
|
if task.WorkDir == "" {
|
||||||
task.WorkDir = defaultWorkdir
|
task.WorkDir = defaultWorkdir
|
||||||
}
|
}
|
||||||
@@ -169,8 +151,6 @@ func defaultRunCodexTaskFn(task TaskSpec, timeout int) TaskResult {
|
|||||||
return runCodexTaskWithContext(parentCtx, task, backend, nil, false, true, timeout)
|
return runCodexTaskWithContext(parentCtx, task, backend, nil, false, true, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
var runCodexTaskFn = defaultRunCodexTaskFn
|
|
||||||
|
|
||||||
func topologicalSort(tasks []TaskSpec) ([][]TaskSpec, error) {
|
func topologicalSort(tasks []TaskSpec) ([][]TaskSpec, error) {
|
||||||
idToTask := make(map[string]TaskSpec, len(tasks))
|
idToTask := make(map[string]TaskSpec, len(tasks))
|
||||||
indegree := make(map[string]int, len(tasks))
|
indegree := make(map[string]int, len(tasks))
|
||||||
@@ -255,8 +235,13 @@ func executeConcurrentWithContext(parentCtx context.Context, layers [][]TaskSpec
|
|||||||
var startPrintMu sync.Mutex
|
var startPrintMu sync.Mutex
|
||||||
bannerPrinted := false
|
bannerPrinted := false
|
||||||
|
|
||||||
printTaskStart := func(taskID, logPath string) {
|
printTaskStart := func(taskID string) {
|
||||||
if logPath == "" {
|
logger := activeLogger()
|
||||||
|
if logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := logger.Path()
|
||||||
|
if path == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startPrintMu.Lock()
|
startPrintMu.Lock()
|
||||||
@@ -264,7 +249,7 @@ func executeConcurrentWithContext(parentCtx context.Context, layers [][]TaskSpec
|
|||||||
fmt.Fprintln(os.Stderr, "=== Starting Parallel Execution ===")
|
fmt.Fprintln(os.Stderr, "=== Starting Parallel Execution ===")
|
||||||
bannerPrinted = true
|
bannerPrinted = true
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Task %s: Log: %s\n", taskID, logPath)
|
fmt.Fprintf(os.Stderr, "Task %s: Log: %s\n", taskID, path)
|
||||||
startPrintMu.Unlock()
|
startPrintMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,11 +319,9 @@ func executeConcurrentWithContext(parentCtx context.Context, layers [][]TaskSpec
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(ts TaskSpec) {
|
go func(ts TaskSpec) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
var taskLogger *Logger
|
|
||||||
var taskLogPath string
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
resultsCh <- TaskResult{TaskID: ts.ID, ExitCode: 1, Error: fmt.Sprintf("panic: %v", r), LogPath: taskLogPath}
|
resultsCh <- TaskResult{TaskID: ts.ID, ExitCode: 1, Error: fmt.Sprintf("panic: %v", r)}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -355,20 +338,9 @@ func executeConcurrentWithContext(parentCtx context.Context, layers [][]TaskSpec
|
|||||||
logConcurrencyState("done", ts.ID, int(after), workerLimit)
|
logConcurrencyState("done", ts.ID, int(after), workerLimit)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if l, err := NewLoggerWithSuffix(ts.ID); err == nil {
|
ts.Context = ctx
|
||||||
taskLogger = l
|
printTaskStart(ts.ID)
|
||||||
taskLogPath = l.Path()
|
resultsCh <- runCodexTaskFn(ts, timeout)
|
||||||
defer func() { _ = taskLogger.Close() }()
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.Context = withTaskLogger(ctx, taskLogger)
|
|
||||||
printTaskStart(ts.ID, taskLogPath)
|
|
||||||
|
|
||||||
res := runCodexTaskFn(ts, timeout)
|
|
||||||
if res.LogPath == "" && taskLogPath != "" {
|
|
||||||
res.LogPath = taskLogPath
|
|
||||||
}
|
|
||||||
resultsCh <- res
|
|
||||||
}(task)
|
}(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +458,14 @@ func runCodexProcess(parentCtx context.Context, codexArgs []string, taskText str
|
|||||||
|
|
||||||
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backend Backend, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
|
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backend Backend, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
|
||||||
result := TaskResult{TaskID: taskSpec.ID}
|
result := TaskResult{TaskID: taskSpec.ID}
|
||||||
injectedLogger := taskLoggerFromContext(parentCtx)
|
setLogPath := func() {
|
||||||
logger := injectedLogger
|
if result.LogPath != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if logger := activeLogger(); logger != nil {
|
||||||
|
result.LogPath = logger.Path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Mode: taskSpec.Mode,
|
Mode: taskSpec.Mode,
|
||||||
@@ -543,17 +521,17 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
if silent {
|
if silent {
|
||||||
// Silent mode: only persist to file when available; avoid stderr noise.
|
// Silent mode: only persist to file when available; avoid stderr noise.
|
||||||
logInfoFn = func(msg string) {
|
logInfoFn = func(msg string) {
|
||||||
if logger != nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
logger.Info(prefixMsg(msg))
|
logger.Info(prefixMsg(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logWarnFn = func(msg string) {
|
logWarnFn = func(msg string) {
|
||||||
if logger != nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
logger.Warn(prefixMsg(msg))
|
logger.Warn(prefixMsg(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logErrorFn = func(msg string) {
|
logErrorFn = func(msg string) {
|
||||||
if logger != nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
logger.Error(prefixMsg(msg))
|
logger.Error(prefixMsg(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,11 +547,10 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
var stderrLogger *logWriter
|
var stderrLogger *logWriter
|
||||||
|
|
||||||
var tempLogger *Logger
|
var tempLogger *Logger
|
||||||
if logger == nil && silent && activeLogger() == nil {
|
if silent && activeLogger() == nil {
|
||||||
if l, err := NewLogger(); err == nil {
|
if l, err := NewLogger(); err == nil {
|
||||||
setLogger(l)
|
setLogger(l)
|
||||||
tempLogger = l
|
tempLogger = l
|
||||||
logger = l
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -581,16 +558,8 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
_ = closeLogger()
|
_ = closeLogger()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer setLogPath()
|
||||||
if result.LogPath != "" || logger == nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
result.LogPath = logger.Path()
|
|
||||||
}()
|
|
||||||
if logger == nil {
|
|
||||||
logger = activeLogger()
|
|
||||||
}
|
|
||||||
if logger != nil {
|
|
||||||
result.LogPath = logger.Path()
|
result.LogPath = logger.Path()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +659,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
}
|
}
|
||||||
|
|
||||||
logInfoFn(fmt.Sprintf("Starting %s with PID: %d", commandName, cmd.Process().Pid()))
|
logInfoFn(fmt.Sprintf("Starting %s with PID: %d", commandName, cmd.Process().Pid()))
|
||||||
if logger != nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,8 +756,8 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
result.ExitCode = 0
|
result.ExitCode = 0
|
||||||
result.Message = message
|
result.Message = message
|
||||||
result.SessionID = threadID
|
result.SessionID = threadID
|
||||||
if result.LogPath == "" && injectedLogger != nil {
|
if logger := activeLogger(); logger != nil {
|
||||||
result.LogPath = injectedLogger.Path()
|
result.LogPath = logger.Path()
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -18,12 +15,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var executorTestTaskCounter atomic.Int64
|
|
||||||
|
|
||||||
func nextExecutorTestTaskID(prefix string) string {
|
|
||||||
return fmt.Sprintf("%s-%d", prefix, executorTestTaskCounter.Add(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
type execFakeProcess struct {
|
type execFakeProcess struct {
|
||||||
pid int
|
pid int
|
||||||
signals []os.Signal
|
signals []os.Signal
|
||||||
@@ -85,7 +76,6 @@ type execFakeRunner struct {
|
|||||||
stdout io.ReadCloser
|
stdout io.ReadCloser
|
||||||
process processHandle
|
process processHandle
|
||||||
stdin io.WriteCloser
|
stdin io.WriteCloser
|
||||||
dir string
|
|
||||||
waitErr error
|
waitErr error
|
||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
startErr error
|
startErr error
|
||||||
@@ -127,7 +117,7 @@ func (f *execFakeRunner) StdinPipe() (io.WriteCloser, error) {
|
|||||||
return &writeCloserStub{}, nil
|
return &writeCloserStub{}, nil
|
||||||
}
|
}
|
||||||
func (f *execFakeRunner) SetStderr(io.Writer) {}
|
func (f *execFakeRunner) SetStderr(io.Writer) {}
|
||||||
func (f *execFakeRunner) SetDir(dir string) { f.dir = dir }
|
func (f *execFakeRunner) SetDir(string) {}
|
||||||
func (f *execFakeRunner) Process() processHandle {
|
func (f *execFakeRunner) Process() processHandle {
|
||||||
if f.process != nil {
|
if f.process != nil {
|
||||||
return f.process
|
return f.process
|
||||||
@@ -159,10 +149,6 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
rcWithCmd := &realCmd{cmd: &exec.Cmd{}}
|
rcWithCmd := &realCmd{cmd: &exec.Cmd{}}
|
||||||
rcWithCmd.SetStderr(io.Discard)
|
rcWithCmd.SetStderr(io.Discard)
|
||||||
rcWithCmd.SetDir("/tmp")
|
|
||||||
if rcWithCmd.cmd.Dir != "/tmp" {
|
|
||||||
t.Fatalf("expected SetDir to set cmd.Dir, got %q", rcWithCmd.cmd.Dir)
|
|
||||||
}
|
|
||||||
echoCmd := exec.Command("echo", "ok")
|
echoCmd := exec.Command("echo", "ok")
|
||||||
rcProc := &realCmd{cmd: echoCmd}
|
rcProc := &realCmd{cmd: echoCmd}
|
||||||
stdoutPipe, err := rcProc.StdoutPipe()
|
stdoutPipe, err := rcProc.StdoutPipe()
|
||||||
@@ -435,63 +421,6 @@ func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
|||||||
_ = closeLogger()
|
_ = closeLogger()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("injectedLogger", func(t *testing.T) {
|
|
||||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
||||||
return &execFakeRunner{
|
|
||||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"injected"}}`),
|
|
||||||
process: &execFakeProcess{pid: 12},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = closeLogger()
|
|
||||||
|
|
||||||
injected, err := NewLoggerWithSuffix("executor-injected")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix() error = %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = injected.Close()
|
|
||||||
_ = os.Remove(injected.Path())
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx := withTaskLogger(context.Background(), injected)
|
|
||||||
res := runCodexTaskWithContext(ctx, TaskSpec{ID: "task-injected", Task: "payload", WorkDir: "."}, nil, nil, false, true, 1)
|
|
||||||
if res.ExitCode != 0 || res.LogPath != injected.Path() {
|
|
||||||
t.Fatalf("expected injected logger path, got %+v", res)
|
|
||||||
}
|
|
||||||
if activeLogger() != nil {
|
|
||||||
t.Fatalf("expected no global logger to be created when injected")
|
|
||||||
}
|
|
||||||
|
|
||||||
injected.Flush()
|
|
||||||
data, err := os.ReadFile(injected.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read injected log file: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(data), "task-injected") {
|
|
||||||
t.Fatalf("injected log missing task prefix, content: %s", string(data))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("backendSetsDirAndNilContext", func(t *testing.T) {
|
|
||||||
var rc *execFakeRunner
|
|
||||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
|
||||||
rc = &execFakeRunner{
|
|
||||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"backend"}}`),
|
|
||||||
process: &execFakeProcess{pid: 13},
|
|
||||||
}
|
|
||||||
return rc
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = closeLogger()
|
|
||||||
res := runCodexTaskWithContext(nil, TaskSpec{ID: "task-backend", Task: "payload", WorkDir: "/tmp"}, ClaudeBackend{}, nil, false, false, 1)
|
|
||||||
if res.ExitCode != 0 || res.Message != "backend" {
|
|
||||||
t.Fatalf("unexpected result: %+v", res)
|
|
||||||
}
|
|
||||||
if rc == nil || rc.dir != "/tmp" {
|
|
||||||
t.Fatalf("expected backend to set cmd.Dir, got runner=%v dir=%q", rc, rc.dir)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("missingMessage", func(t *testing.T) {
|
t.Run("missingMessage", func(t *testing.T) {
|
||||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||||
return &execFakeRunner{
|
return &execFakeRunner{
|
||||||
@@ -506,476 +435,6 @@ func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecutorParallelLogIsolation(t *testing.T) {
|
|
||||||
mainLogger, err := NewLoggerWithSuffix("executor-main")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix() error = %v", err)
|
|
||||||
}
|
|
||||||
setLogger(mainLogger)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = closeLogger()
|
|
||||||
_ = os.Remove(mainLogger.Path())
|
|
||||||
})
|
|
||||||
|
|
||||||
taskA := nextExecutorTestTaskID("iso-a")
|
|
||||||
taskB := nextExecutorTestTaskID("iso-b")
|
|
||||||
markerA := "ISOLATION_MARKER:" + taskA
|
|
||||||
markerB := "ISOLATION_MARKER:" + taskB
|
|
||||||
|
|
||||||
origRun := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
logger := taskLoggerFromContext(task.Context)
|
|
||||||
if logger == nil {
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 1, Error: "missing task logger"}
|
|
||||||
}
|
|
||||||
switch task.ID {
|
|
||||||
case taskA:
|
|
||||||
logger.Info(markerA)
|
|
||||||
case taskB:
|
|
||||||
logger.Info(markerB)
|
|
||||||
default:
|
|
||||||
logger.Info("unexpected task: " + task.ID)
|
|
||||||
}
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 0}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = origRun })
|
|
||||||
|
|
||||||
stderrR, stderrW, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("os.Pipe() error = %v", err)
|
|
||||||
}
|
|
||||||
oldStderr := os.Stderr
|
|
||||||
os.Stderr = stderrW
|
|
||||||
defer func() { os.Stderr = oldStderr }()
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(nil, [][]TaskSpec{{{ID: taskA}, {ID: taskB}}}, 1, -1)
|
|
||||||
|
|
||||||
_ = stderrW.Close()
|
|
||||||
os.Stderr = oldStderr
|
|
||||||
stderrData, _ := io.ReadAll(stderrR)
|
|
||||||
_ = stderrR.Close()
|
|
||||||
stderrOut := string(stderrData)
|
|
||||||
|
|
||||||
if len(results) != 2 {
|
|
||||||
t.Fatalf("expected 2 results, got %d", len(results))
|
|
||||||
}
|
|
||||||
|
|
||||||
paths := map[string]string{}
|
|
||||||
for _, res := range results {
|
|
||||||
if res.ExitCode != 0 {
|
|
||||||
t.Fatalf("unexpected failure: %+v", res)
|
|
||||||
}
|
|
||||||
if res.LogPath == "" {
|
|
||||||
t.Fatalf("missing LogPath for task %q", res.TaskID)
|
|
||||||
}
|
|
||||||
paths[res.TaskID] = res.LogPath
|
|
||||||
}
|
|
||||||
if paths[taskA] == paths[taskB] {
|
|
||||||
t.Fatalf("expected distinct task log paths, got %q", paths[taskA])
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(stderrOut, mainLogger.Path()) {
|
|
||||||
t.Fatalf("stderr should not print main log path: %s", stderrOut)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stderrOut, paths[taskA]) || !strings.Contains(stderrOut, paths[taskB]) {
|
|
||||||
t.Fatalf("stderr should include task log paths, got: %s", stderrOut)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainLogger.Flush()
|
|
||||||
mainData, err := os.ReadFile(mainLogger.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read main log: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(string(mainData), markerA) || strings.Contains(string(mainData), markerB) {
|
|
||||||
t.Fatalf("main log should not contain task markers, content: %s", string(mainData))
|
|
||||||
}
|
|
||||||
|
|
||||||
taskAData, err := os.ReadFile(paths[taskA])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read task A log: %v", err)
|
|
||||||
}
|
|
||||||
taskBData, err := os.ReadFile(paths[taskB])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read task B log: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(taskAData), markerA) || strings.Contains(string(taskAData), markerB) {
|
|
||||||
t.Fatalf("task A log isolation failed, content: %s", string(taskAData))
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(taskBData), markerB) || strings.Contains(string(taskBData), markerA) {
|
|
||||||
t.Fatalf("task B log isolation failed, content: %s", string(taskBData))
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = os.Remove(paths[taskA])
|
|
||||||
_ = os.Remove(paths[taskB])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrentExecutorParallelLogIsolationAndClosure(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
t.Setenv("TMPDIR", tempDir)
|
|
||||||
|
|
||||||
oldArgs := os.Args
|
|
||||||
os.Args = []string{defaultWrapperName}
|
|
||||||
t.Cleanup(func() { os.Args = oldArgs })
|
|
||||||
|
|
||||||
mainLogger, err := NewLoggerWithSuffix("concurrent-main")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix() error = %v", err)
|
|
||||||
}
|
|
||||||
setLogger(mainLogger)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
mainLogger.Flush()
|
|
||||||
_ = closeLogger()
|
|
||||||
_ = os.Remove(mainLogger.Path())
|
|
||||||
})
|
|
||||||
|
|
||||||
const taskCount = 16
|
|
||||||
const writersPerTask = 4
|
|
||||||
const logsPerWriter = 50
|
|
||||||
const expectedTaskLines = writersPerTask * logsPerWriter
|
|
||||||
|
|
||||||
taskIDs := make([]string, 0, taskCount)
|
|
||||||
tasks := make([]TaskSpec, 0, taskCount)
|
|
||||||
for i := 0; i < taskCount; i++ {
|
|
||||||
id := nextExecutorTestTaskID("iso")
|
|
||||||
taskIDs = append(taskIDs, id)
|
|
||||||
tasks = append(tasks, TaskSpec{ID: id})
|
|
||||||
}
|
|
||||||
|
|
||||||
type taskLoggerInfo struct {
|
|
||||||
taskID string
|
|
||||||
logger *Logger
|
|
||||||
}
|
|
||||||
loggerCh := make(chan taskLoggerInfo, taskCount)
|
|
||||||
readyCh := make(chan struct{}, taskCount)
|
|
||||||
startCh := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for i := 0; i < taskCount; i++ {
|
|
||||||
<-readyCh
|
|
||||||
}
|
|
||||||
close(startCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
origRun := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
readyCh <- struct{}{}
|
|
||||||
|
|
||||||
logger := taskLoggerFromContext(task.Context)
|
|
||||||
loggerCh <- taskLoggerInfo{taskID: task.ID, logger: logger}
|
|
||||||
if logger == nil {
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 1, Error: "missing task logger"}
|
|
||||||
}
|
|
||||||
|
|
||||||
<-startCh
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(writersPerTask)
|
|
||||||
for g := 0; g < writersPerTask; g++ {
|
|
||||||
go func(g int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for i := 0; i < logsPerWriter; i++ {
|
|
||||||
logger.Info(fmt.Sprintf("TASK=%s g=%d i=%d", task.ID, g, i))
|
|
||||||
}
|
|
||||||
}(g)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 0}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = origRun })
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(context.Background(), [][]TaskSpec{tasks}, 1, 0)
|
|
||||||
|
|
||||||
if len(results) != taskCount {
|
|
||||||
t.Fatalf("expected %d results, got %d", taskCount, len(results))
|
|
||||||
}
|
|
||||||
|
|
||||||
taskLogPaths := make(map[string]string, taskCount)
|
|
||||||
seenPaths := make(map[string]struct{}, taskCount)
|
|
||||||
for _, res := range results {
|
|
||||||
if res.ExitCode != 0 || res.Error != "" {
|
|
||||||
t.Fatalf("unexpected task failure: %+v", res)
|
|
||||||
}
|
|
||||||
if res.LogPath == "" {
|
|
||||||
t.Fatalf("missing LogPath for task %q", res.TaskID)
|
|
||||||
}
|
|
||||||
if _, ok := taskLogPaths[res.TaskID]; ok {
|
|
||||||
t.Fatalf("duplicate TaskID in results: %q", res.TaskID)
|
|
||||||
}
|
|
||||||
taskLogPaths[res.TaskID] = res.LogPath
|
|
||||||
if _, ok := seenPaths[res.LogPath]; ok {
|
|
||||||
t.Fatalf("expected unique log path per task; duplicate path %q", res.LogPath)
|
|
||||||
}
|
|
||||||
seenPaths[res.LogPath] = struct{}{}
|
|
||||||
}
|
|
||||||
if len(taskLogPaths) != taskCount {
|
|
||||||
t.Fatalf("expected %d unique task IDs, got %d", taskCount, len(taskLogPaths))
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := primaryLogPrefix()
|
|
||||||
pid := os.Getpid()
|
|
||||||
for _, id := range taskIDs {
|
|
||||||
path := taskLogPaths[id]
|
|
||||||
if path == "" {
|
|
||||||
t.Fatalf("missing log path for task %q", id)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
t.Fatalf("task log file not created for %q: %v", id, err)
|
|
||||||
}
|
|
||||||
wantBase := fmt.Sprintf("%s-%d-%s.log", prefix, pid, id)
|
|
||||||
if got := filepath.Base(path); got != wantBase {
|
|
||||||
t.Fatalf("unexpected log filename for %q: got %q, want %q", id, got, wantBase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loggers := make(map[string]*Logger, taskCount)
|
|
||||||
for i := 0; i < taskCount; i++ {
|
|
||||||
info := <-loggerCh
|
|
||||||
if info.taskID == "" {
|
|
||||||
t.Fatalf("missing taskID in logger info")
|
|
||||||
}
|
|
||||||
if info.logger == nil {
|
|
||||||
t.Fatalf("missing logger in context for task %q", info.taskID)
|
|
||||||
}
|
|
||||||
if prev, ok := loggers[info.taskID]; ok && prev != info.logger {
|
|
||||||
t.Fatalf("task %q received multiple logger instances", info.taskID)
|
|
||||||
}
|
|
||||||
loggers[info.taskID] = info.logger
|
|
||||||
}
|
|
||||||
if len(loggers) != taskCount {
|
|
||||||
t.Fatalf("expected %d task loggers, got %d", taskCount, len(loggers))
|
|
||||||
}
|
|
||||||
|
|
||||||
for taskID, logger := range loggers {
|
|
||||||
if !logger.closed.Load() {
|
|
||||||
t.Fatalf("expected task logger to be closed for %q", taskID)
|
|
||||||
}
|
|
||||||
if logger.file == nil {
|
|
||||||
t.Fatalf("expected task logger file to be non-nil for %q", taskID)
|
|
||||||
}
|
|
||||||
if _, err := logger.file.Write([]byte("x")); err == nil {
|
|
||||||
t.Fatalf("expected task logger file to be closed for %q", taskID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainLogger.Flush()
|
|
||||||
mainData, err := os.ReadFile(mainLogger.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read main log: %v", err)
|
|
||||||
}
|
|
||||||
mainText := string(mainData)
|
|
||||||
if !strings.Contains(mainText, "parallel: worker_limit=") {
|
|
||||||
t.Fatalf("expected main log to include concurrency planning, content: %s", mainText)
|
|
||||||
}
|
|
||||||
if strings.Contains(mainText, "TASK=") {
|
|
||||||
t.Fatalf("main log should not contain task output, content: %s", mainText)
|
|
||||||
}
|
|
||||||
|
|
||||||
for taskID, path := range taskLogPaths {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open task log for %q: %v", taskID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
lines := 0
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.Contains(line, "parallel:") {
|
|
||||||
t.Fatalf("task log should not contain main log entries for %q: %s", taskID, line)
|
|
||||||
}
|
|
||||||
gotID, ok := parseTaskIDFromLogLine(line)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("task log entry missing task marker for %q: %s", taskID, line)
|
|
||||||
}
|
|
||||||
if gotID != taskID {
|
|
||||||
t.Fatalf("task log isolation failed: file=%q got TASK=%q want TASK=%q", path, gotID, taskID)
|
|
||||||
}
|
|
||||||
lines++
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
_ = f.Close()
|
|
||||||
t.Fatalf("scanner error for %q: %v", taskID, err)
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
t.Fatalf("failed to close task log for %q: %v", taskID, err)
|
|
||||||
}
|
|
||||||
if lines != expectedTaskLines {
|
|
||||||
t.Fatalf("unexpected task log line count for %q: got %d, want %d", taskID, lines, expectedTaskLines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range taskLogPaths {
|
|
||||||
_ = os.Remove(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTaskIDFromLogLine(line string) (string, bool) {
|
|
||||||
const marker = "TASK="
|
|
||||||
idx := strings.Index(line, marker)
|
|
||||||
if idx == -1 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
rest := line[idx+len(marker):]
|
|
||||||
end := strings.IndexByte(rest, ' ')
|
|
||||||
if end == -1 {
|
|
||||||
return rest, rest != ""
|
|
||||||
}
|
|
||||||
return rest[:end], rest[:end] != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecutorTaskLoggerContext(t *testing.T) {
|
|
||||||
if taskLoggerFromContext(nil) != nil {
|
|
||||||
t.Fatalf("expected nil logger from nil context")
|
|
||||||
}
|
|
||||||
if taskLoggerFromContext(context.Background()) != nil {
|
|
||||||
t.Fatalf("expected nil logger when context has no logger")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := NewLoggerWithSuffix("executor-taskctx")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix() error = %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = logger.Close()
|
|
||||||
_ = os.Remove(logger.Path())
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx := withTaskLogger(context.Background(), logger)
|
|
||||||
if got := taskLoggerFromContext(ctx); got != logger {
|
|
||||||
t.Fatalf("expected logger roundtrip, got %v", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
if taskLoggerFromContext(withTaskLogger(context.Background(), nil)) != nil {
|
|
||||||
t.Fatalf("expected nil logger when injected logger is nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecutorExecuteConcurrentWithContextBranches(t *testing.T) {
|
|
||||||
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open %s: %v", os.DevNull, err)
|
|
||||||
}
|
|
||||||
oldStderr := os.Stderr
|
|
||||||
os.Stderr = devNull
|
|
||||||
t.Cleanup(func() {
|
|
||||||
os.Stderr = oldStderr
|
|
||||||
_ = devNull.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("skipOnFailedDependencies", func(t *testing.T) {
|
|
||||||
root := nextExecutorTestTaskID("root")
|
|
||||||
child := nextExecutorTestTaskID("child")
|
|
||||||
|
|
||||||
orig := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
if task.ID == root {
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 1, Error: "boom"}
|
|
||||||
}
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 0}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = orig })
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(context.Background(), [][]TaskSpec{
|
|
||||||
{{ID: root}},
|
|
||||||
{{ID: child, Dependencies: []string{root}}},
|
|
||||||
}, 1, 0)
|
|
||||||
|
|
||||||
foundChild := false
|
|
||||||
for _, res := range results {
|
|
||||||
if res.LogPath != "" {
|
|
||||||
_ = os.Remove(res.LogPath)
|
|
||||||
}
|
|
||||||
if res.TaskID != child {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
foundChild = true
|
|
||||||
if res.ExitCode == 0 || !strings.Contains(res.Error, "skipped") {
|
|
||||||
t.Fatalf("expected skipped child task result, got %+v", res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundChild {
|
|
||||||
t.Fatalf("expected child task to be present in results")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("panicRecovered", func(t *testing.T) {
|
|
||||||
taskID := nextExecutorTestTaskID("panic")
|
|
||||||
|
|
||||||
orig := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
panic("boom")
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = orig })
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(context.Background(), [][]TaskSpec{{{ID: taskID}}}, 1, 0)
|
|
||||||
if len(results) != 1 {
|
|
||||||
t.Fatalf("expected 1 result, got %d", len(results))
|
|
||||||
}
|
|
||||||
if results[0].ExitCode == 0 || !strings.Contains(results[0].Error, "panic") {
|
|
||||||
t.Fatalf("expected panic result, got %+v", results[0])
|
|
||||||
}
|
|
||||||
if results[0].LogPath == "" {
|
|
||||||
t.Fatalf("expected LogPath on panic result")
|
|
||||||
}
|
|
||||||
_ = os.Remove(results[0].LogPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cancelWhileWaitingForWorker", func(t *testing.T) {
|
|
||||||
task1 := nextExecutorTestTaskID("slot")
|
|
||||||
task2 := nextExecutorTestTaskID("slot")
|
|
||||||
|
|
||||||
parentCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
started := make(chan struct{})
|
|
||||||
unblock := make(chan struct{})
|
|
||||||
var startedOnce sync.Once
|
|
||||||
|
|
||||||
orig := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
startedOnce.Do(func() { close(started) })
|
|
||||||
<-unblock
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 0}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = orig })
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-started
|
|
||||||
cancel()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
close(unblock)
|
|
||||||
}()
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(parentCtx, [][]TaskSpec{{{ID: task1}, {ID: task2}}}, 1, 1)
|
|
||||||
foundCancelled := false
|
|
||||||
for _, res := range results {
|
|
||||||
if res.LogPath != "" {
|
|
||||||
_ = os.Remove(res.LogPath)
|
|
||||||
}
|
|
||||||
if res.ExitCode == 130 {
|
|
||||||
foundCancelled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundCancelled {
|
|
||||||
t.Fatalf("expected a task to be cancelled")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("loggerCreateFails", func(t *testing.T) {
|
|
||||||
taskID := nextExecutorTestTaskID("bad") + "/id"
|
|
||||||
|
|
||||||
orig := runCodexTaskFn
|
|
||||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
|
||||||
return TaskResult{TaskID: task.ID, ExitCode: 0}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { runCodexTaskFn = orig })
|
|
||||||
|
|
||||||
results := executeConcurrentWithContext(context.Background(), [][]TaskSpec{{{ID: taskID}}}, 1, 0)
|
|
||||||
if len(results) != 1 || results[0].ExitCode != 0 {
|
|
||||||
t.Fatalf("unexpected results: %+v", results)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExecutorSignalAndTermination(t *testing.T) {
|
func TestExecutorSignalAndTermination(t *testing.T) {
|
||||||
forceKillDelay.Store(0)
|
forceKillDelay.Store(0)
|
||||||
defer forceKillDelay.Store(5)
|
defer forceKillDelay.Store(5)
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoggerNilReceiverNoop(t *testing.T) {
|
|
||||||
var logger *Logger
|
|
||||||
logger.Info("info")
|
|
||||||
logger.Warn("warn")
|
|
||||||
logger.Debug("debug")
|
|
||||||
logger.Error("error")
|
|
||||||
logger.Flush()
|
|
||||||
if err := logger.Close(); err != nil {
|
|
||||||
t.Fatalf("Close() on nil logger should return nil, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerConcurrencyLogHelpers(t *testing.T) {
|
|
||||||
setTempDirEnv(t, t.TempDir())
|
|
||||||
|
|
||||||
logger, err := NewLoggerWithSuffix("concurrency")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix error: %v", err)
|
|
||||||
}
|
|
||||||
setLogger(logger)
|
|
||||||
defer closeLogger()
|
|
||||||
|
|
||||||
logConcurrencyPlanning(0, 2)
|
|
||||||
logConcurrencyPlanning(3, 2)
|
|
||||||
logConcurrencyState("start", "task-1", 1, 0)
|
|
||||||
logConcurrencyState("done", "task-1", 0, 3)
|
|
||||||
logger.Flush()
|
|
||||||
|
|
||||||
data, err := os.ReadFile(logger.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read log file: %v", err)
|
|
||||||
}
|
|
||||||
output := string(data)
|
|
||||||
|
|
||||||
checks := []string{
|
|
||||||
"parallel: worker_limit=unbounded total_tasks=2",
|
|
||||||
"parallel: worker_limit=3 total_tasks=2",
|
|
||||||
"parallel: start task=task-1 active=1 limit=unbounded",
|
|
||||||
"parallel: done task=task-1 active=0 limit=3",
|
|
||||||
}
|
|
||||||
for _, c := range checks {
|
|
||||||
if !strings.Contains(output, c) {
|
|
||||||
t.Fatalf("log output missing %q, got: %s", c, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerConcurrencyLogHelpersNoopWithoutActiveLogger(t *testing.T) {
|
|
||||||
_ = closeLogger()
|
|
||||||
logConcurrencyPlanning(1, 1)
|
|
||||||
logConcurrencyState("start", "task-1", 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsSkipsUnsafeAndHandlesAlreadyDeleted(t *testing.T) {
|
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
|
||||||
|
|
||||||
unsafePath := createTempLog(t, tempDir, fmt.Sprintf("%s-%d.log", primaryLogPrefix(), 222))
|
|
||||||
orphanPath := createTempLog(t, tempDir, fmt.Sprintf("%s-%d.log", primaryLogPrefix(), 111))
|
|
||||||
|
|
||||||
stubFileStat(t, func(path string) (os.FileInfo, error) {
|
|
||||||
if path == unsafePath {
|
|
||||||
return fakeFileInfo{mode: os.ModeSymlink}, nil
|
|
||||||
}
|
|
||||||
return os.Lstat(path)
|
|
||||||
})
|
|
||||||
|
|
||||||
stubProcessRunning(t, func(pid int) bool {
|
|
||||||
if pid == 111 {
|
|
||||||
_ = os.Remove(orphanPath)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
stats, err := cleanupOldLogs()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats.Scanned != 2 {
|
|
||||||
t.Fatalf("scanned = %d, want %d", stats.Scanned, 2)
|
|
||||||
}
|
|
||||||
if stats.Deleted != 0 {
|
|
||||||
t.Fatalf("deleted = %d, want %d", stats.Deleted, 0)
|
|
||||||
}
|
|
||||||
if stats.Kept != 2 {
|
|
||||||
t.Fatalf("kept = %d, want %d", stats.Kept, 2)
|
|
||||||
}
|
|
||||||
if stats.Errors != 0 {
|
|
||||||
t.Fatalf("errors = %d, want %d", stats.Errors, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSkip := false
|
|
||||||
hasAlreadyDeleted := false
|
|
||||||
for _, name := range stats.KeptFiles {
|
|
||||||
if strings.Contains(name, "already deleted") {
|
|
||||||
hasAlreadyDeleted = true
|
|
||||||
}
|
|
||||||
if strings.Contains(name, filepath.Base(unsafePath)) {
|
|
||||||
hasSkip = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasSkip {
|
|
||||||
t.Fatalf("expected kept files to include unsafe log %q, got %+v", filepath.Base(unsafePath), stats.KeptFiles)
|
|
||||||
}
|
|
||||||
if !hasAlreadyDeleted {
|
|
||||||
t.Fatalf("expected kept files to include already deleted marker, got %+v", stats.KeptFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerIsUnsafeFileErrorPaths(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
t.Run("stat ErrNotExist", func(t *testing.T) {
|
|
||||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
})
|
|
||||||
|
|
||||||
unsafe, reason := isUnsafeFile("missing.log", tempDir)
|
|
||||||
if !unsafe || reason != "" {
|
|
||||||
t.Fatalf("expected missing file to be skipped silently, got unsafe=%v reason=%q", unsafe, reason)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("stat error", func(t *testing.T) {
|
|
||||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
|
||||||
return nil, fmt.Errorf("boom")
|
|
||||||
})
|
|
||||||
|
|
||||||
unsafe, reason := isUnsafeFile("broken.log", tempDir)
|
|
||||||
if !unsafe || !strings.Contains(reason, "stat failed") {
|
|
||||||
t.Fatalf("expected stat failure to be unsafe, got unsafe=%v reason=%q", unsafe, reason)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("EvalSymlinks error", func(t *testing.T) {
|
|
||||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
|
||||||
return fakeFileInfo{}, nil
|
|
||||||
})
|
|
||||||
stubEvalSymlinks(t, func(string) (string, error) {
|
|
||||||
return "", fmt.Errorf("resolve failed")
|
|
||||||
})
|
|
||||||
|
|
||||||
unsafe, reason := isUnsafeFile("cannot-resolve.log", tempDir)
|
|
||||||
if !unsafe || !strings.Contains(reason, "path resolution failed") {
|
|
||||||
t.Fatalf("expected resolution failure to be unsafe, got unsafe=%v reason=%q", unsafe, reason)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoggerWithSuffixNamingAndIsolation(t *testing.T) {
|
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
|
||||||
|
|
||||||
taskA := "task-1"
|
|
||||||
taskB := "task-2"
|
|
||||||
|
|
||||||
loggerA, err := NewLoggerWithSuffix(taskA)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix(%q) error = %v", taskA, err)
|
|
||||||
}
|
|
||||||
defer loggerA.Close()
|
|
||||||
|
|
||||||
loggerB, err := NewLoggerWithSuffix(taskB)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewLoggerWithSuffix(%q) error = %v", taskB, err)
|
|
||||||
}
|
|
||||||
defer loggerB.Close()
|
|
||||||
|
|
||||||
wantA := filepath.Join(tempDir, fmt.Sprintf("%s-%d-%s.log", primaryLogPrefix(), os.Getpid(), taskA))
|
|
||||||
if loggerA.Path() != wantA {
|
|
||||||
t.Fatalf("loggerA path = %q, want %q", loggerA.Path(), wantA)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantB := filepath.Join(tempDir, fmt.Sprintf("%s-%d-%s.log", primaryLogPrefix(), os.Getpid(), taskB))
|
|
||||||
if loggerB.Path() != wantB {
|
|
||||||
t.Fatalf("loggerB path = %q, want %q", loggerB.Path(), wantB)
|
|
||||||
}
|
|
||||||
|
|
||||||
if loggerA.Path() == loggerB.Path() {
|
|
||||||
t.Fatalf("expected different log files, got %q", loggerA.Path())
|
|
||||||
}
|
|
||||||
|
|
||||||
loggerA.Info("from taskA")
|
|
||||||
loggerB.Info("from taskB")
|
|
||||||
loggerA.Flush()
|
|
||||||
loggerB.Flush()
|
|
||||||
|
|
||||||
dataA, err := os.ReadFile(loggerA.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read loggerA file: %v", err)
|
|
||||||
}
|
|
||||||
dataB, err := os.ReadFile(loggerB.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read loggerB file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(dataA), "from taskA") {
|
|
||||||
t.Fatalf("loggerA missing its message, got: %q", string(dataA))
|
|
||||||
}
|
|
||||||
if strings.Contains(string(dataA), "from taskB") {
|
|
||||||
t.Fatalf("loggerA contains loggerB message, got: %q", string(dataA))
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(dataB), "from taskB") {
|
|
||||||
t.Fatalf("loggerB missing its message, got: %q", string(dataB))
|
|
||||||
}
|
|
||||||
if strings.Contains(string(dataB), "from taskA") {
|
|
||||||
t.Fatalf("loggerB contains loggerA message, got: %q", string(dataB))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerWithSuffixReturnsErrorWhenTempDirMissing(t *testing.T) {
|
|
||||||
missingTempDir := filepath.Join(t.TempDir(), "does-not-exist")
|
|
||||||
setTempDirEnv(t, missingTempDir)
|
|
||||||
|
|
||||||
logger, err := NewLoggerWithSuffix("task-err")
|
|
||||||
if err == nil {
|
|
||||||
_ = logger.Close()
|
|
||||||
t.Fatalf("expected error, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ func compareCleanupStats(got, want CleanupStats) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCreatesFileWithPID(t *testing.T) {
|
func TestRunLoggerCreatesFileWithPID(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
t.Setenv("TMPDIR", tempDir)
|
t.Setenv("TMPDIR", tempDir)
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ func TestLoggerCreatesFileWithPID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerWritesLevels(t *testing.T) {
|
func TestRunLoggerWritesLevels(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
t.Setenv("TMPDIR", tempDir)
|
t.Setenv("TMPDIR", tempDir)
|
||||||
|
|
||||||
@@ -77,31 +77,7 @@ func TestLoggerWritesLevels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerDefaultIsTerminalCoverage(t *testing.T) {
|
func TestRunLoggerCloseRemovesFileAndStopsWorker(t *testing.T) {
|
||||||
oldStdin := os.Stdin
|
|
||||||
t.Cleanup(func() { os.Stdin = oldStdin })
|
|
||||||
|
|
||||||
f, err := os.CreateTemp(t.TempDir(), "stdin-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("os.CreateTemp() error = %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
|
|
||||||
os.Stdin = f
|
|
||||||
if got := defaultIsTerminal(); got {
|
|
||||||
t.Fatalf("defaultIsTerminal() = %v, want false for regular file", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
t.Fatalf("Close() error = %v", err)
|
|
||||||
}
|
|
||||||
os.Stdin = f
|
|
||||||
if got := defaultIsTerminal(); !got {
|
|
||||||
t.Fatalf("defaultIsTerminal() = %v, want true when Stat fails", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerCloseStopsWorkerAndKeepsFile(t *testing.T) {
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
t.Setenv("TMPDIR", tempDir)
|
t.Setenv("TMPDIR", tempDir)
|
||||||
|
|
||||||
@@ -118,11 +94,6 @@ func TestLoggerCloseStopsWorkerAndKeepsFile(t *testing.T) {
|
|||||||
if err := logger.Close(); err != nil {
|
if err := logger.Close(); err != nil {
|
||||||
t.Fatalf("Close() returned error: %v", err)
|
t.Fatalf("Close() returned error: %v", err)
|
||||||
}
|
}
|
||||||
if logger.file != nil {
|
|
||||||
if _, err := logger.file.Write([]byte("x")); err == nil {
|
|
||||||
t.Fatalf("expected file to be closed after Close()")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After recent changes, log file is kept for debugging - NOT removed
|
// After recent changes, log file is kept for debugging - NOT removed
|
||||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||||
@@ -145,7 +116,7 @@ func TestLoggerCloseStopsWorkerAndKeepsFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerConcurrentWritesSafe(t *testing.T) {
|
func TestRunLoggerConcurrentWritesSafe(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
t.Setenv("TMPDIR", tempDir)
|
t.Setenv("TMPDIR", tempDir)
|
||||||
|
|
||||||
@@ -194,7 +165,7 @@ func TestLoggerConcurrentWritesSafe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerTerminateProcessActive(t *testing.T) {
|
func TestRunLoggerTerminateProcessActive(t *testing.T) {
|
||||||
cmd := exec.Command("sleep", "5")
|
cmd := exec.Command("sleep", "5")
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
t.Skipf("cannot start sleep command: %v", err)
|
t.Skipf("cannot start sleep command: %v", err)
|
||||||
@@ -222,7 +193,7 @@ func TestLoggerTerminateProcessActive(t *testing.T) {
|
|||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerTerminateProcessNil(t *testing.T) {
|
func TestRunTerminateProcessNil(t *testing.T) {
|
||||||
if timer := terminateProcess(nil); timer != nil {
|
if timer := terminateProcess(nil); timer != nil {
|
||||||
t.Fatalf("terminateProcess(nil) should return nil timer")
|
t.Fatalf("terminateProcess(nil) should return nil timer")
|
||||||
}
|
}
|
||||||
@@ -231,7 +202,7 @@ func TestLoggerTerminateProcessNil(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsRemovesOrphans(t *testing.T) {
|
func TestRunCleanupOldLogsRemovesOrphans(t *testing.T) {
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
orphan1 := createTempLog(t, tempDir, "codex-wrapper-111.log")
|
orphan1 := createTempLog(t, tempDir, "codex-wrapper-111.log")
|
||||||
@@ -281,7 +252,7 @@ func TestLoggerCleanupOldLogsRemovesOrphans(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
|
func TestRunCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
invalid := []string{
|
invalid := []string{
|
||||||
@@ -339,7 +310,7 @@ func TestLoggerCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsHandlesGlobFailures(t *testing.T) {
|
func TestRunCleanupOldLogsHandlesGlobFailures(t *testing.T) {
|
||||||
stubProcessRunning(t, func(pid int) bool {
|
stubProcessRunning(t, func(pid int) bool {
|
||||||
t.Fatalf("process check should not run when glob fails")
|
t.Fatalf("process check should not run when glob fails")
|
||||||
return false
|
return false
|
||||||
@@ -365,7 +336,7 @@ func TestLoggerCleanupOldLogsHandlesGlobFailures(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
|
func TestRunCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
|
||||||
setTempDirEnv(t, t.TempDir())
|
setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
stubProcessRunning(t, func(int) bool {
|
stubProcessRunning(t, func(int) bool {
|
||||||
@@ -385,7 +356,7 @@ func TestLoggerCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
|
func TestRunCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
paths := []string{
|
paths := []string{
|
||||||
@@ -425,7 +396,7 @@ func TestLoggerCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
|
func TestRunCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
protected := createTempLog(t, tempDir, "codex-wrapper-6200.log")
|
protected := createTempLog(t, tempDir, "codex-wrapper-6200.log")
|
||||||
@@ -462,7 +433,7 @@ func TestLoggerCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsPerformanceBound(t *testing.T) {
|
func TestRunCleanupOldLogsPerformanceBound(t *testing.T) {
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
const fileCount = 400
|
const fileCount = 400
|
||||||
@@ -505,98 +476,17 @@ func TestLoggerCleanupOldLogsPerformanceBound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsCoverageSuite(t *testing.T) {
|
func TestRunCleanupOldLogsCoverageSuite(t *testing.T) {
|
||||||
TestBackendParseJSONStream_CoverageSuite(t)
|
TestBackendParseJSONStream_CoverageSuite(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse the existing coverage suite so the focused TestLogger run still exercises
|
// Reuse the existing coverage suite so the focused TestLogger run still exercises
|
||||||
// the rest of the codebase and keeps coverage high.
|
// the rest of the codebase and keeps coverage high.
|
||||||
func TestLoggerCoverageSuite(t *testing.T) {
|
func TestRunLoggerCoverageSuite(t *testing.T) {
|
||||||
suite := []struct {
|
TestBackendParseJSONStream_CoverageSuite(t)
|
||||||
name string
|
|
||||||
fn func(*testing.T)
|
|
||||||
}{
|
|
||||||
{"TestBackendParseJSONStream_CoverageSuite", TestBackendParseJSONStream_CoverageSuite},
|
|
||||||
{"TestVersionCoverageFullRun", TestVersionCoverageFullRun},
|
|
||||||
{"TestVersionMainWrapper", TestVersionMainWrapper},
|
|
||||||
|
|
||||||
{"TestExecutorHelperCoverage", TestExecutorHelperCoverage},
|
|
||||||
{"TestExecutorRunCodexTaskWithContext", TestExecutorRunCodexTaskWithContext},
|
|
||||||
{"TestExecutorParallelLogIsolation", TestExecutorParallelLogIsolation},
|
|
||||||
{"TestExecutorTaskLoggerContext", TestExecutorTaskLoggerContext},
|
|
||||||
{"TestExecutorExecuteConcurrentWithContextBranches", TestExecutorExecuteConcurrentWithContextBranches},
|
|
||||||
{"TestExecutorSignalAndTermination", TestExecutorSignalAndTermination},
|
|
||||||
{"TestExecutorCancelReasonAndCloseWithReason", TestExecutorCancelReasonAndCloseWithReason},
|
|
||||||
{"TestExecutorForceKillTimerStop", TestExecutorForceKillTimerStop},
|
|
||||||
{"TestExecutorForwardSignalsDefaults", TestExecutorForwardSignalsDefaults},
|
|
||||||
|
|
||||||
{"TestBackendParseArgs_NewMode", TestBackendParseArgs_NewMode},
|
|
||||||
{"TestBackendParseArgs_ResumeMode", TestBackendParseArgs_ResumeMode},
|
|
||||||
{"TestBackendParseArgs_BackendFlag", TestBackendParseArgs_BackendFlag},
|
|
||||||
{"TestBackendParseArgs_SkipPermissions", TestBackendParseArgs_SkipPermissions},
|
|
||||||
{"TestBackendParseBoolFlag", TestBackendParseBoolFlag},
|
|
||||||
{"TestBackendEnvFlagEnabled", TestBackendEnvFlagEnabled},
|
|
||||||
{"TestRunResolveTimeout", TestRunResolveTimeout},
|
|
||||||
{"TestRunIsTerminal", TestRunIsTerminal},
|
|
||||||
{"TestRunReadPipedTask", TestRunReadPipedTask},
|
|
||||||
{"TestTailBufferWrite", TestTailBufferWrite},
|
|
||||||
{"TestLogWriterWriteLimitsBuffer", TestLogWriterWriteLimitsBuffer},
|
|
||||||
{"TestLogWriterLogLine", TestLogWriterLogLine},
|
|
||||||
{"TestNewLogWriterDefaultMaxLen", TestNewLogWriterDefaultMaxLen},
|
|
||||||
{"TestNewLogWriterDefaultLimit", TestNewLogWriterDefaultLimit},
|
|
||||||
{"TestRunHello", TestRunHello},
|
|
||||||
{"TestRunGreet", TestRunGreet},
|
|
||||||
{"TestRunFarewell", TestRunFarewell},
|
|
||||||
{"TestRunFarewellEmpty", TestRunFarewellEmpty},
|
|
||||||
|
|
||||||
{"TestParallelParseConfig_Success", TestParallelParseConfig_Success},
|
|
||||||
{"TestParallelParseConfig_Backend", TestParallelParseConfig_Backend},
|
|
||||||
{"TestParallelParseConfig_InvalidFormat", TestParallelParseConfig_InvalidFormat},
|
|
||||||
{"TestParallelParseConfig_EmptyTasks", TestParallelParseConfig_EmptyTasks},
|
|
||||||
{"TestParallelParseConfig_MissingID", TestParallelParseConfig_MissingID},
|
|
||||||
{"TestParallelParseConfig_MissingTask", TestParallelParseConfig_MissingTask},
|
|
||||||
{"TestParallelParseConfig_DuplicateID", TestParallelParseConfig_DuplicateID},
|
|
||||||
{"TestParallelParseConfig_DelimiterFormat", TestParallelParseConfig_DelimiterFormat},
|
|
||||||
|
|
||||||
{"TestBackendSelectBackend", TestBackendSelectBackend},
|
|
||||||
{"TestBackendSelectBackend_Invalid", TestBackendSelectBackend_Invalid},
|
|
||||||
{"TestBackendSelectBackend_DefaultOnEmpty", TestBackendSelectBackend_DefaultOnEmpty},
|
|
||||||
{"TestBackendBuildArgs_CodexBackend", TestBackendBuildArgs_CodexBackend},
|
|
||||||
{"TestBackendBuildArgs_ClaudeBackend", TestBackendBuildArgs_ClaudeBackend},
|
|
||||||
{"TestClaudeBackendBuildArgs_OutputValidation", TestClaudeBackendBuildArgs_OutputValidation},
|
|
||||||
{"TestBackendBuildArgs_GeminiBackend", TestBackendBuildArgs_GeminiBackend},
|
|
||||||
{"TestGeminiBackendBuildArgs_OutputValidation", TestGeminiBackendBuildArgs_OutputValidation},
|
|
||||||
{"TestBackendNamesAndCommands", TestBackendNamesAndCommands},
|
|
||||||
|
|
||||||
{"TestBackendParseJSONStream", TestBackendParseJSONStream},
|
|
||||||
{"TestBackendParseJSONStream_ClaudeEvents", TestBackendParseJSONStream_ClaudeEvents},
|
|
||||||
{"TestBackendParseJSONStream_GeminiEvents", TestBackendParseJSONStream_GeminiEvents},
|
|
||||||
{"TestBackendParseJSONStreamWithWarn_InvalidLine", TestBackendParseJSONStreamWithWarn_InvalidLine},
|
|
||||||
{"TestBackendParseJSONStream_OnMessage", TestBackendParseJSONStream_OnMessage},
|
|
||||||
{"TestBackendParseJSONStream_ScannerError", TestBackendParseJSONStream_ScannerError},
|
|
||||||
{"TestBackendDiscardInvalidJSON", TestBackendDiscardInvalidJSON},
|
|
||||||
{"TestBackendDiscardInvalidJSONBuffer", TestBackendDiscardInvalidJSONBuffer},
|
|
||||||
|
|
||||||
{"TestCurrentWrapperNameFallsBackToExecutable", TestCurrentWrapperNameFallsBackToExecutable},
|
|
||||||
{"TestCurrentWrapperNameDetectsLegacyAliasSymlink", TestCurrentWrapperNameDetectsLegacyAliasSymlink},
|
|
||||||
|
|
||||||
{"TestIsProcessRunning", TestIsProcessRunning},
|
|
||||||
{"TestGetProcessStartTimeReadsProcStat", TestGetProcessStartTimeReadsProcStat},
|
|
||||||
{"TestGetProcessStartTimeInvalidData", TestGetProcessStartTimeInvalidData},
|
|
||||||
{"TestGetBootTimeParsesBtime", TestGetBootTimeParsesBtime},
|
|
||||||
{"TestGetBootTimeInvalidData", TestGetBootTimeInvalidData},
|
|
||||||
|
|
||||||
{"TestClaudeBuildArgs_ModesAndPermissions", TestClaudeBuildArgs_ModesAndPermissions},
|
|
||||||
{"TestClaudeBuildArgs_GeminiAndCodexModes", TestClaudeBuildArgs_GeminiAndCodexModes},
|
|
||||||
{"TestClaudeBuildArgs_BackendMetadata", TestClaudeBuildArgs_BackendMetadata},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range suite {
|
func TestRunCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
|
||||||
t.Run(tc.name, tc.fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
|
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
currentPID := os.Getpid()
|
currentPID := os.Getpid()
|
||||||
@@ -628,7 +518,7 @@ func TestLoggerCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerIsPIDReusedScenarios(t *testing.T) {
|
func TestIsPIDReusedScenarios(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -662,7 +552,7 @@ func TestLoggerIsPIDReusedScenarios(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerIsUnsafeFileSecurityChecks(t *testing.T) {
|
func TestIsUnsafeFileSecurityChecks(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
absTempDir, err := filepath.Abs(tempDir)
|
absTempDir, err := filepath.Abs(tempDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -711,7 +601,7 @@ func TestLoggerIsUnsafeFileSecurityChecks(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerPathAndRemove(t *testing.T) {
|
func TestRunLoggerPathAndRemove(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
path := filepath.Join(tempDir, "sample.log")
|
path := filepath.Join(tempDir, "sample.log")
|
||||||
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
||||||
@@ -738,19 +628,7 @@ func TestLoggerPathAndRemove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerTruncateBytesCoverage(t *testing.T) {
|
func TestRunLoggerInternalLog(t *testing.T) {
|
||||||
if got := truncateBytes([]byte("abc"), 3); got != "abc" {
|
|
||||||
t.Fatalf("truncateBytes() = %q, want %q", got, "abc")
|
|
||||||
}
|
|
||||||
if got := truncateBytes([]byte("abcd"), 3); got != "abc..." {
|
|
||||||
t.Fatalf("truncateBytes() = %q, want %q", got, "abc...")
|
|
||||||
}
|
|
||||||
if got := truncateBytes([]byte("abcd"), -1); got != "" {
|
|
||||||
t.Fatalf("truncateBytes() = %q, want empty string", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerInternalLog(t *testing.T) {
|
|
||||||
logger := &Logger{
|
logger := &Logger{
|
||||||
ch: make(chan logEntry, 1),
|
ch: make(chan logEntry, 1),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
@@ -775,7 +653,7 @@ func TestLoggerInternalLog(t *testing.T) {
|
|||||||
close(logger.done)
|
close(logger.done)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerParsePIDFromLog(t *testing.T) {
|
func TestRunParsePIDFromLog(t *testing.T) {
|
||||||
hugePID := strconv.FormatInt(math.MaxInt64, 10) + "0"
|
hugePID := strconv.FormatInt(math.MaxInt64, 10) + "0"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -891,7 +769,7 @@ func (f fakeFileInfo) ModTime() time.Time { return f.modTime }
|
|||||||
func (f fakeFileInfo) IsDir() bool { return false }
|
func (f fakeFileInfo) IsDir() bool { return false }
|
||||||
func (f fakeFileInfo) Sys() interface{} { return nil }
|
func (f fakeFileInfo) Sys() interface{} { return nil }
|
||||||
|
|
||||||
func TestLoggerExtractRecentErrors(t *testing.T) {
|
func TestExtractRecentErrors(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
@@ -968,21 +846,21 @@ func TestLoggerExtractRecentErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerExtractRecentErrorsNilLogger(t *testing.T) {
|
func TestExtractRecentErrorsNilLogger(t *testing.T) {
|
||||||
var logger *Logger
|
var logger *Logger
|
||||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||||
t.Fatalf("nil logger ExtractRecentErrors() should return nil, got %v", got)
|
t.Fatalf("nil logger ExtractRecentErrors() should return nil, got %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerExtractRecentErrorsEmptyPath(t *testing.T) {
|
func TestExtractRecentErrorsEmptyPath(t *testing.T) {
|
||||||
logger := &Logger{path: ""}
|
logger := &Logger{path: ""}
|
||||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||||
t.Fatalf("empty path ExtractRecentErrors() should return nil, got %v", got)
|
t.Fatalf("empty path ExtractRecentErrors() should return nil, got %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerExtractRecentErrorsFileNotExist(t *testing.T) {
|
func TestExtractRecentErrorsFileNotExist(t *testing.T) {
|
||||||
logger := &Logger{path: "/nonexistent/path/to/log.log"}
|
logger := &Logger{path: "/nonexistent/path/to/log.log"}
|
||||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||||
t.Fatalf("nonexistent file ExtractRecentErrors() should return nil, got %v", got)
|
t.Fatalf("nonexistent file ExtractRecentErrors() should return nil, got %v", got)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "5.2.4"
|
version = "5.2.3"
|
||||||
defaultWorkdir = "."
|
defaultWorkdir = "."
|
||||||
defaultTimeout = 7200 // seconds
|
defaultTimeout = 7200 // seconds
|
||||||
codexLogLineLimit = 1000
|
codexLogLineLimit = 1000
|
||||||
|
|||||||
@@ -426,11 +426,10 @@ ok-d`
|
|||||||
t.Fatalf("expected startup banner in stderr, got:\n%s", stderrOut)
|
t.Fatalf("expected startup banner in stderr, got:\n%s", stderrOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
// After parallel log isolation fix, each task has its own log file
|
|
||||||
expectedLines := map[string]struct{}{
|
expectedLines := map[string]struct{}{
|
||||||
fmt.Sprintf("Task a: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-a.log", os.Getpid()))): {},
|
fmt.Sprintf("Task a: Log: %s", expectedLog): {},
|
||||||
fmt.Sprintf("Task b: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-b.log", os.Getpid()))): {},
|
fmt.Sprintf("Task b: Log: %s", expectedLog): {},
|
||||||
fmt.Sprintf("Task d: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-d.log", os.Getpid()))): {},
|
fmt.Sprintf("Task d: Log: %s", expectedLog): {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(taskLines) != len(expectedLines) {
|
if len(taskLines) != len(expectedLines) {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ func resetTestHooks() {
|
|||||||
closeLogger()
|
closeLogger()
|
||||||
executablePathFn = os.Executable
|
executablePathFn = os.Executable
|
||||||
runTaskFn = runCodexTask
|
runTaskFn = runCodexTask
|
||||||
runCodexTaskFn = defaultRunCodexTaskFn
|
|
||||||
exitFn = os.Exit
|
exitFn = os.Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1378,7 +1377,7 @@ func TestBackendBuildArgs_ClaudeBackend(t *testing.T) {
|
|||||||
backend := ClaudeBackend{}
|
backend := ClaudeBackend{}
|
||||||
cfg := &Config{Mode: "new", WorkDir: defaultWorkdir}
|
cfg := &Config{Mode: "new", WorkDir: defaultWorkdir}
|
||||||
got := backend.BuildArgs(cfg, "todo")
|
got := backend.BuildArgs(cfg, "todo")
|
||||||
want := []string{"-p", "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose", "todo"}
|
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"}
|
||||||
if len(got) != len(want) {
|
if len(got) != len(want) {
|
||||||
t.Fatalf("length mismatch")
|
t.Fatalf("length mismatch")
|
||||||
}
|
}
|
||||||
@@ -1399,7 +1398,7 @@ func TestClaudeBackendBuildArgs_OutputValidation(t *testing.T) {
|
|||||||
target := "ensure-flags"
|
target := "ensure-flags"
|
||||||
|
|
||||||
args := backend.BuildArgs(cfg, target)
|
args := backend.BuildArgs(cfg, target)
|
||||||
expectedPrefix := []string{"-p", "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose"}
|
expectedPrefix := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose"}
|
||||||
|
|
||||||
if len(args) != len(expectedPrefix)+1 {
|
if len(args) != len(expectedPrefix)+1 {
|
||||||
t.Fatalf("args length=%d, want %d", len(args), len(expectedPrefix)+1)
|
t.Fatalf("args length=%d, want %d", len(args), len(expectedPrefix)+1)
|
||||||
@@ -2691,7 +2690,7 @@ func TestVersionFlag(t *testing.T) {
|
|||||||
t.Errorf("exit = %d, want 0", code)
|
t.Errorf("exit = %d, want 0", code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
want := "codeagent-wrapper version 5.2.4\n"
|
want := "codeagent-wrapper version 5.2.3\n"
|
||||||
if output != want {
|
if output != want {
|
||||||
t.Fatalf("output = %q, want %q", output, want)
|
t.Fatalf("output = %q, want %q", output, want)
|
||||||
}
|
}
|
||||||
@@ -2705,7 +2704,7 @@ func TestVersionShortFlag(t *testing.T) {
|
|||||||
t.Errorf("exit = %d, want 0", code)
|
t.Errorf("exit = %d, want 0", code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
want := "codeagent-wrapper version 5.2.4\n"
|
want := "codeagent-wrapper version 5.2.3\n"
|
||||||
if output != want {
|
if output != want {
|
||||||
t.Fatalf("output = %q, want %q", output, want)
|
t.Fatalf("output = %q, want %q", output, want)
|
||||||
}
|
}
|
||||||
@@ -2719,7 +2718,7 @@ func TestVersionLegacyAlias(t *testing.T) {
|
|||||||
t.Errorf("exit = %d, want 0", code)
|
t.Errorf("exit = %d, want 0", code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
want := "codex-wrapper version 5.2.4\n"
|
want := "codex-wrapper version 5.2.3\n"
|
||||||
if output != want {
|
if output != want {
|
||||||
t.Fatalf("output = %q, want %q", output, want)
|
t.Fatalf("output = %q, want %q", output, want)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user