Claude Code + Codex + Gemini 多模型协作工作流系统 ## 核心功能 - 6阶段开发工作流 (/ccg:dev) - 智能路由:前端→Gemini,后端→Codex - Prompt 增强 (Auggie MCP prompt-enhancer) - 双模型代码审查和技术分析 ## 命令列表 - /ccg:dev - 完整开发工作流 - /ccg:frontend - 前端任务 - /ccg:backend - 后端任务 - /ccg:review - 代码审查 - /ccg:analyze - 技术分析 - /ccg:enhance - Prompt 增强 - /ccg:commit - 智能提交 - /ccg:rollback - 交互式回滚 - /ccg:clean-branches - 清理分支 - /ccg:worktree - Worktree 管理 - /ccg:init - 项目初始化 ## 特性 - 预编译二进制(无需 Go 环境) - 专家系统提示词(Codex/Gemini) - 一键安装脚本
218 lines
5.1 KiB
Go
218 lines
5.1 KiB
Go
//go:build unix || darwin || linux
|
|
// +build unix darwin linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIsProcessRunning(t *testing.T) {
|
|
t.Run("current process", func(t *testing.T) {
|
|
if !isProcessRunning(os.Getpid()) {
|
|
t.Fatalf("expected current process (pid=%d) to be running", os.Getpid())
|
|
}
|
|
})
|
|
|
|
t.Run("fake pid", func(t *testing.T) {
|
|
const nonexistentPID = 1 << 30
|
|
if isProcessRunning(nonexistentPID) {
|
|
t.Fatalf("expected pid %d to be reported as not running", nonexistentPID)
|
|
}
|
|
})
|
|
|
|
t.Run("terminated process", func(t *testing.T) {
|
|
pid := exitedProcessPID(t)
|
|
if isProcessRunning(pid) {
|
|
t.Fatalf("expected exited child process (pid=%d) to be reported as not running", pid)
|
|
}
|
|
})
|
|
|
|
t.Run("boundary values", func(t *testing.T) {
|
|
if isProcessRunning(0) {
|
|
t.Fatalf("pid 0 should never be treated as running")
|
|
}
|
|
if isProcessRunning(-42) {
|
|
t.Fatalf("negative pid should never be treated as running")
|
|
}
|
|
})
|
|
|
|
t.Run("find process error", func(t *testing.T) {
|
|
original := findProcess
|
|
defer func() { findProcess = original }()
|
|
|
|
mockErr := errors.New("findProcess failure")
|
|
findProcess = func(pid int) (*os.Process, error) {
|
|
return nil, mockErr
|
|
}
|
|
|
|
if isProcessRunning(1234) {
|
|
t.Fatalf("expected false when os.FindProcess fails")
|
|
}
|
|
})
|
|
}
|
|
|
|
func exitedProcessPID(t *testing.T) int {
|
|
t.Helper()
|
|
|
|
var cmd *exec.Cmd
|
|
if runtime.GOOS == "windows" {
|
|
cmd = exec.Command("cmd", "/c", "exit 0")
|
|
} else {
|
|
cmd = exec.Command("sh", "-c", "exit 0")
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("failed to start helper process: %v", err)
|
|
}
|
|
pid := cmd.Process.Pid
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
t.Fatalf("helper process did not exit cleanly: %v", err)
|
|
}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
return pid
|
|
}
|
|
|
|
func TestRunProcessCheckSmoke(t *testing.T) {
|
|
t.Run("current process", func(t *testing.T) {
|
|
if !isProcessRunning(os.Getpid()) {
|
|
t.Fatalf("expected current process (pid=%d) to be running", os.Getpid())
|
|
}
|
|
})
|
|
|
|
t.Run("fake pid", func(t *testing.T) {
|
|
const nonexistentPID = 1 << 30
|
|
if isProcessRunning(nonexistentPID) {
|
|
t.Fatalf("expected pid %d to be reported as not running", nonexistentPID)
|
|
}
|
|
})
|
|
|
|
t.Run("boundary values", func(t *testing.T) {
|
|
if isProcessRunning(0) {
|
|
t.Fatalf("pid 0 should never be treated as running")
|
|
}
|
|
if isProcessRunning(-42) {
|
|
t.Fatalf("negative pid should never be treated as running")
|
|
}
|
|
})
|
|
|
|
t.Run("find process error", func(t *testing.T) {
|
|
original := findProcess
|
|
defer func() { findProcess = original }()
|
|
|
|
mockErr := errors.New("findProcess failure")
|
|
findProcess = func(pid int) (*os.Process, error) {
|
|
return nil, mockErr
|
|
}
|
|
|
|
if isProcessRunning(1234) {
|
|
t.Fatalf("expected false when os.FindProcess fails")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetProcessStartTimeReadsProcStat(t *testing.T) {
|
|
pid := 4321
|
|
boot := time.Unix(1_710_000_000, 0)
|
|
startTicks := uint64(4500)
|
|
|
|
statFields := make([]string, 25)
|
|
for i := range statFields {
|
|
statFields[i] = strconv.Itoa(i + 1)
|
|
}
|
|
statFields[19] = strconv.FormatUint(startTicks, 10)
|
|
statContent := fmt.Sprintf("%d (%s) %s", pid, "cmd with space", strings.Join(statFields, " "))
|
|
|
|
stubReadFile(t, func(path string) ([]byte, error) {
|
|
switch path {
|
|
case fmt.Sprintf("/proc/%d/stat", pid):
|
|
return []byte(statContent), nil
|
|
case "/proc/stat":
|
|
return []byte(fmt.Sprintf("cpu 0 0 0 0\nbtime %d\n", boot.Unix())), nil
|
|
default:
|
|
return nil, os.ErrNotExist
|
|
}
|
|
})
|
|
|
|
got := getProcessStartTime(pid)
|
|
want := boot.Add(time.Duration(startTicks/100) * time.Second)
|
|
if !got.Equal(want) {
|
|
t.Fatalf("getProcessStartTime() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGetProcessStartTimeInvalidData(t *testing.T) {
|
|
pid := 99
|
|
stubReadFile(t, func(path string) ([]byte, error) {
|
|
switch path {
|
|
case fmt.Sprintf("/proc/%d/stat", pid):
|
|
return []byte("garbage"), nil
|
|
case "/proc/stat":
|
|
return []byte("btime not-a-number\n"), nil
|
|
default:
|
|
return nil, os.ErrNotExist
|
|
}
|
|
})
|
|
|
|
if got := getProcessStartTime(pid); !got.IsZero() {
|
|
t.Fatalf("invalid /proc data should return zero time, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestGetBootTimeParsesBtime(t *testing.T) {
|
|
const bootSec = 1_711_111_111
|
|
stubReadFile(t, func(path string) ([]byte, error) {
|
|
if path != "/proc/stat" {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
content := fmt.Sprintf("intr 0\nbtime %d\n", bootSec)
|
|
return []byte(content), nil
|
|
})
|
|
|
|
got := getBootTime()
|
|
want := time.Unix(bootSec, 0)
|
|
if !got.Equal(want) {
|
|
t.Fatalf("getBootTime() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGetBootTimeInvalidData(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
content string
|
|
}{
|
|
{"missing", "cpu 0 0 0 0"},
|
|
{"malformed", "btime abc"},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
stubReadFile(t, func(string) ([]byte, error) {
|
|
return []byte(tt.content), nil
|
|
})
|
|
if got := getBootTime(); !got.IsZero() {
|
|
t.Fatalf("getBootTime() unexpected value for %s: %v", tt.name, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func stubReadFile(t *testing.T, fn func(string) ([]byte, error)) {
|
|
t.Helper()
|
|
original := readFileFn
|
|
readFileFn = fn
|
|
t.Cleanup(func() {
|
|
readFileFn = original
|
|
})
|
|
}
|