fix: restore go compatibility for runtime models
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
- 后端监听地址:默认 `127.0.0.1:18083`
|
||||
- 前端开发地址:默认 `127.0.0.1:13083`
|
||||
- `WorkspaceRoot`: 默认当前进程工作目录,用于读取 `task_plan.md` 等计划文件证据。
|
||||
- Go module 目标:`go 1.22`
|
||||
- SQLite 驱动:`modernc.org/sqlite v1.35.0`
|
||||
|
||||
## Runbook
|
||||
|
||||
@@ -57,7 +59,8 @@ curl http://127.0.0.1:18083/api/workflow/events
|
||||
|
||||
- 不读取 `.codex/auth.json`。
|
||||
- 不写入 Codex SQLite。
|
||||
- SQLite 通过纯 Go `modernc.org/sqlite` 和 `mode=ro&immutable=1` 打开;缺失 SQLite 返回空列表和低置信度来源说明。
|
||||
- SQLite 通过纯 Go `modernc.org/sqlite v1.35.0` 和 `mode=ro&immutable=1` 打开;缺失 SQLite 返回空列表和低置信度来源说明。
|
||||
- 运行线程 API 返回聚合 `source` 和分数据源 `sources`;仅 `state_5.sqlite` 或仅 `goals_1.sqlite` 缺失时,聚合来源为 `sqlite_partial`,缺失的一侧为 `sqlite_missing` / `low`。
|
||||
- `.codex/agents/*.toml` 写回必须先备份。
|
||||
- 当前 `/api/agents` 只读列出 `.codex/agents` 直属 `.toml` 文件,读取前通过 Codex home 边界和 agent TOML 专用 resolver;坏 TOML 以单条 `invalid` 状态返回,不导致服务崩溃。
|
||||
- 当前 `/api/projects` 只读解析 `.codex/config.toml` 中的 `[projects."..."]`,展示路径、显示名、信任等级和目录存在性。
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
- 工作流显示使用动态事件流/DAG,不写死阶段或 agent 顺序。
|
||||
- 所有推断状态必须显示来源和置信度。
|
||||
- UI 全中文,技术缩写保留英文。
|
||||
- Phase 3 使用纯 Go `modernc.org/sqlite` 读取 SQLite,避免 CGO 运行时依赖。
|
||||
- SQLite 文件不存在时返回空列表和 `sqlite_missing`/`low` 来源证据,不返回 500。
|
||||
- Phase 3 使用纯 Go `modernc.org/sqlite v1.35.0` 读取 SQLite,避免 CGO 运行时依赖,并保持项目 `go 1.22` 兼容。
|
||||
- SQLite 文件不存在时返回空列表和 `sqlite_missing`/`low` 来源证据;仅 state 或仅 goals 缺失时返回 `sqlite_partial` 聚合证据,并在 `sources.state` / `sources.goals` 分别标注缺失或只读来源。
|
||||
- 动态工作流事件从 threads、spawn edges、goals、`task_plan.md` 证据构建,不假设固定角色顺序。
|
||||
|
||||
13
go.mod
13
go.mod
@@ -1,17 +1,18 @@
|
||||
module codex-agent-manager
|
||||
|
||||
go 1.25.0
|
||||
go 1.22
|
||||
|
||||
require modernc.org/sqlite v1.50.1
|
||||
require modernc.org/sqlite v1.35.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -1,50 +1,46 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
|
||||
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -5,8 +5,11 @@ type Snapshot struct {
|
||||
SpawnEdges []SpawnEdge `json:"spawnEdges"`
|
||||
Goals []Goal `json:"goals"`
|
||||
Source SourceEvidence `json:"source"`
|
||||
Sources SourceMap `json:"sources"`
|
||||
}
|
||||
|
||||
type SourceMap map[string]SourceEvidence
|
||||
|
||||
type Thread struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
|
||||
45
internal/runtime/module_test.go
Normal file
45
internal/runtime/module_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestModuleKeepsGo122Compatibility(t *testing.T) {
|
||||
if runtime.Version() == "" {
|
||||
t.Fatal("runtime version unavailable")
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsLine(string(data), "go 1.22") {
|
||||
t.Fatalf("go.mod must keep Go 1.22 compatibility, got:\n%s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func containsLine(input string, want string) bool {
|
||||
for _, line := range splitLines(input) {
|
||||
if line == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitLines(input string) []string {
|
||||
var lines []string
|
||||
start := 0
|
||||
for i, char := range input {
|
||||
if char == '\n' {
|
||||
lines = append(lines, input[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start <= len(input) {
|
||||
lines = append(lines, input[start:])
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -27,24 +27,16 @@ func (s Store) Snapshot() (Snapshot, error) {
|
||||
}
|
||||
stateExists := fileExists(statePath)
|
||||
goalsExists := fileExists(goalsPath)
|
||||
if !stateExists && !goalsExists {
|
||||
return Snapshot{
|
||||
Threads: []Thread{},
|
||||
SpawnEdges: []SpawnEdge{},
|
||||
Goals: []Goal{},
|
||||
Source: SourceEvidence{
|
||||
Kind: "sqlite_missing",
|
||||
Confidence: "low",
|
||||
Message: "Codex SQLite files were not found; returning an empty read-only snapshot.",
|
||||
},
|
||||
}, nil
|
||||
sources := SourceMap{
|
||||
"state": sqliteSource("state", statePath, stateExists),
|
||||
"goals": sqliteSource("goals", goalsPath, goalsExists),
|
||||
}
|
||||
|
||||
snapshot := Snapshot{
|
||||
Threads: []Thread{},
|
||||
SpawnEdges: []SpawnEdge{},
|
||||
Goals: []Goal{},
|
||||
Source: SourceEvidence{Kind: "sqlite_readonly", Path: statePath, Confidence: "high"},
|
||||
Source: aggregateSource(sources),
|
||||
Sources: sources,
|
||||
}
|
||||
if stateExists {
|
||||
db, err := openReadonlySQLite(statePath)
|
||||
@@ -75,6 +67,38 @@ func (s Store) Snapshot() (Snapshot, error) {
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func sqliteSource(name string, path string, exists bool) SourceEvidence {
|
||||
if exists {
|
||||
return SourceEvidence{Kind: "sqlite_readonly", Path: path, Confidence: "high"}
|
||||
}
|
||||
return SourceEvidence{
|
||||
Kind: "sqlite_missing",
|
||||
Path: path,
|
||||
Confidence: "low",
|
||||
Message: name + " SQLite file was not found; returning empty data for that source.",
|
||||
}
|
||||
}
|
||||
|
||||
func aggregateSource(sources SourceMap) SourceEvidence {
|
||||
state := sources["state"]
|
||||
goals := sources["goals"]
|
||||
if state.Kind == "sqlite_readonly" && goals.Kind == "sqlite_readonly" {
|
||||
return SourceEvidence{Kind: "sqlite_readonly", Confidence: "high"}
|
||||
}
|
||||
if state.Kind == "sqlite_missing" && goals.Kind == "sqlite_missing" {
|
||||
return SourceEvidence{
|
||||
Kind: "sqlite_missing",
|
||||
Confidence: "low",
|
||||
Message: "Codex SQLite files were not found; returning an empty read-only snapshot.",
|
||||
}
|
||||
}
|
||||
return SourceEvidence{
|
||||
Kind: "sqlite_partial",
|
||||
Confidence: "partial",
|
||||
Message: "One Codex SQLite source is missing; available data was read from existing sources only.",
|
||||
}
|
||||
}
|
||||
|
||||
func openReadonlySQLite(path string) (*sql.DB, error) {
|
||||
uri := url.URL{Scheme: "file", Path: path}
|
||||
query := uri.Query()
|
||||
|
||||
@@ -2,6 +2,7 @@ package runtime
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -19,6 +20,12 @@ func TestStoreMissingSQLiteReturnsEmptySnapshot(t *testing.T) {
|
||||
if snapshot.Source.Confidence != "low" || snapshot.Source.Kind != "sqlite_missing" {
|
||||
t.Fatalf("unexpected source evidence: %#v", snapshot.Source)
|
||||
}
|
||||
if source := snapshot.Sources["state"]; source.Kind != "sqlite_missing" || source.Confidence != "low" {
|
||||
t.Fatalf("unexpected state source evidence: %#v", source)
|
||||
}
|
||||
if source := snapshot.Sources["goals"]; source.Kind != "sqlite_missing" || source.Confidence != "low" {
|
||||
t.Fatalf("unexpected goals source evidence: %#v", source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreReadsThreadsEdgesAndGoalsFromReadonlySQLite(t *testing.T) {
|
||||
@@ -44,6 +51,68 @@ func TestStoreReadsThreadsEdgesAndGoalsFromReadonlySQLite(t *testing.T) {
|
||||
if snapshot.Source.Confidence != "high" || snapshot.Source.Kind != "sqlite_readonly" {
|
||||
t.Fatalf("unexpected source evidence: %#v", snapshot.Source)
|
||||
}
|
||||
if source := snapshot.Sources["state"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" {
|
||||
t.Fatalf("unexpected state source evidence: %#v", source)
|
||||
}
|
||||
if source := snapshot.Sources["goals"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" {
|
||||
t.Fatalf("unexpected goals source evidence: %#v", source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreMarksStateMissingWhenOnlyGoalsSQLiteExists(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createRuntimeSQLite(t, root)
|
||||
if err := os.Remove(filepath.Join(root, "state_5.sqlite")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
snapshot, err := Store{CodexHome: root}.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("Snapshot returned error: %v", err)
|
||||
}
|
||||
if len(snapshot.Threads) != 0 || len(snapshot.SpawnEdges) != 0 {
|
||||
t.Fatalf("expected no state-backed data, got threads=%#v edges=%#v", snapshot.Threads, snapshot.SpawnEdges)
|
||||
}
|
||||
if len(snapshot.Goals) != 1 {
|
||||
t.Fatalf("goals = %#v, want one goal from goals DB", snapshot.Goals)
|
||||
}
|
||||
if source := snapshot.Sources["state"]; source.Kind != "sqlite_missing" || source.Confidence != "low" {
|
||||
t.Fatalf("unexpected state source evidence: %#v", source)
|
||||
}
|
||||
if source := snapshot.Sources["goals"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" {
|
||||
t.Fatalf("unexpected goals source evidence: %#v", source)
|
||||
}
|
||||
if snapshot.Source.Confidence != "partial" || snapshot.Source.Kind != "sqlite_partial" {
|
||||
t.Fatalf("unexpected aggregate source evidence: %#v", snapshot.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreMarksGoalsMissingWhenOnlyStateSQLiteExists(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createRuntimeSQLite(t, root)
|
||||
if err := os.Remove(filepath.Join(root, "goals_1.sqlite")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
snapshot, err := Store{CodexHome: root}.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("Snapshot returned error: %v", err)
|
||||
}
|
||||
if len(snapshot.Threads) != 2 || len(snapshot.SpawnEdges) != 1 {
|
||||
t.Fatalf("expected state-backed data, got threads=%#v edges=%#v", snapshot.Threads, snapshot.SpawnEdges)
|
||||
}
|
||||
if len(snapshot.Goals) != 0 {
|
||||
t.Fatalf("goals = %#v, want no goals", snapshot.Goals)
|
||||
}
|
||||
if source := snapshot.Sources["state"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" {
|
||||
t.Fatalf("unexpected state source evidence: %#v", source)
|
||||
}
|
||||
if source := snapshot.Sources["goals"]; source.Kind != "sqlite_missing" || source.Confidence != "low" {
|
||||
t.Fatalf("unexpected goals source evidence: %#v", source)
|
||||
}
|
||||
if snapshot.Source.Confidence != "partial" || snapshot.Source.Kind != "sqlite_partial" {
|
||||
t.Fatalf("unexpected aggregate source evidence: %#v", snapshot.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func createRuntimeSQLite(t *testing.T, root string) {
|
||||
|
||||
@@ -60,10 +60,11 @@ func New(cfg app.Config) http.Handler {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": snapshot.Threads,
|
||||
"edges": snapshot.SpawnEdges,
|
||||
"goals": snapshot.Goals,
|
||||
"source": snapshot.Source,
|
||||
"items": snapshot.Threads,
|
||||
"edges": snapshot.SpawnEdges,
|
||||
"goals": snapshot.Goals,
|
||||
"source": snapshot.Source,
|
||||
"sources": snapshot.Sources,
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/workflow/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"codex-agent-manager/internal/app"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestAgentsEndpointReturnsAgentItems(t *testing.T) {
|
||||
@@ -117,6 +120,48 @@ func TestRuntimeThreadsEndpointReturnsEmptyWhenSQLiteMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeThreadsEndpointReturnsPartialSourceEvidence(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
db, err := sql.Open("sqlite", filepath.Join(root, "goals_1.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(`CREATE TABLE thread_goals (thread_id TEXT, goal TEXT, status TEXT, updated_at TEXT)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/runtime/threads", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
New(app.Config{CodexHome: root, HTTPAddr: "127.0.0.1:0"}).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Source struct {
|
||||
Kind string `json:"kind"`
|
||||
Confidence string `json:"confidence"`
|
||||
} `json:"source"`
|
||||
Sources map[string]struct {
|
||||
Kind string `json:"kind"`
|
||||
Confidence string `json:"confidence"`
|
||||
} `json:"sources"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("invalid json: %v", err)
|
||||
}
|
||||
if body.Source.Kind != "sqlite_partial" || body.Source.Confidence != "partial" {
|
||||
t.Fatalf("unexpected aggregate source: %#v", body.Source)
|
||||
}
|
||||
if body.Sources["state"].Kind != "sqlite_missing" || body.Sources["state"].Confidence != "low" {
|
||||
t.Fatalf("unexpected state source: %#v", body.Sources["state"])
|
||||
}
|
||||
if body.Sources["goals"].Kind != "sqlite_readonly" || body.Sources["goals"].Confidence != "high" {
|
||||
t.Fatalf("unexpected goals source: %#v", body.Sources["goals"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowEventsEndpointReturnsEvents(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "task_plan.md"), []byte("| 3 | in_progress | Runtime model |\n"), 0o644); err != nil {
|
||||
|
||||
15
progress.md
15
progress.md
@@ -16,6 +16,7 @@
|
||||
| 2026-05-25 | 2 | spec review | 复审未通过:`agents -> .` 目录 symlink 可读取 root `config.toml` | coding agent 按 blocking 范围修复 |
|
||||
| 2026-05-25 | 2 | coding agent | TDD 修复 symlinked `agents` 目录边界 | 完成;提交 `fix: reject symlinked agents directory` |
|
||||
| 2026-05-25 | 3 | coding agent | TDD 实现项目配置、运行线程和动态工作流只读模型 | 完成;新增 `/api/projects`、`/api/runtime/threads`、`/api/workflow/events` |
|
||||
| 2026-05-25 | 3 | spec review | 规格审查未通过:SQLite 依赖提升 Go 下限到 1.25;单侧 DB 缺失来源证据不足 | coding agent 按 blocking 范围修复 |
|
||||
|
||||
## Test Results
|
||||
|
||||
@@ -72,6 +73,18 @@
|
||||
| 2026-05-25 | `go test ./...` | PASS | 全量 Go 测试通过 |
|
||||
| 2026-05-25 | `git diff --check` | PASS | Phase 3 whitespace 检查通过 |
|
||||
| 2026-05-25 | `go test -count=1 ./...` | PASS | Phase 3 非缓存全量 Go 测试通过 |
|
||||
| 2026-05-25 | `go test ./internal/runtime` | FAIL | TDD 红灯:新增单侧 SQLite 缺失测试后 `Snapshot.Sources` 未定义 |
|
||||
| 2026-05-25 | `go test ./internal/runtime` | FAIL | TDD 红灯:`go.mod` 仍为 `go 1.25.0` |
|
||||
| 2026-05-25 | `go list -m -versions modernc.org/sqlite` | PASS_WITH_ESCALATION | 检查可用 SQLite 驱动版本 |
|
||||
| 2026-05-25 | `go list -m -json modernc.org/sqlite@v1.35.0` | PASS | 确认该版本 `GoVersion` 为 `1.21`,可用于 Go 1.22 目标 |
|
||||
| 2026-05-25 | `go get modernc.org/sqlite@v1.35.0` | PASS_WITH_ESCALATION | 降级 SQLite 驱动版本 |
|
||||
| 2026-05-25 | `go mod tidy` | PASS_WITH_ESCALATION | 移除 Go 1.25 间接依赖 pin,恢复 `go 1.22` |
|
||||
| 2026-05-25 | `go test ./internal/server` | FAIL | TDD 红灯:runtime API 未返回分数据源 `sources` |
|
||||
| 2026-05-25 | `go test ./internal/runtime` | PASS | 单侧 state/goals 缺失和 Go 1.22 module 兼容测试通过 |
|
||||
| 2026-05-25 | `go test ./internal/server` | PASS | runtime API 返回 `source` 和分数据源 `sources` |
|
||||
| 2026-05-25 | `git diff --check` | PASS | Phase 3 review fix whitespace 检查通过 |
|
||||
| 2026-05-25 | `go test ./...` | PASS | Phase 3 review fix 全量 Go 测试通过 |
|
||||
| 2026-05-25 | `go test -count=1 ./...` | PASS | Phase 3 review fix 非缓存全量 Go 测试通过 |
|
||||
|
||||
## Bug Loop
|
||||
|
||||
@@ -86,3 +99,5 @@
|
||||
| 2 | Agent symlink 只校验最终路径在 Codex home 内,可读取 root `config.toml` | 在 agent store 层拒绝 `.toml` symlink,避免读取非 agent TOML 内容 | `go test ./internal/agents` PASS |
|
||||
| 2 | `agents` 目录 symlink 会让枚举逻辑读取 Codex home root 的 `.toml` 文件 | 在 `Store.List` 对 lexical `CodexHome/agents` 先 `Lstat`,发现 symlink 直接返回 forbidden error | `go test ./internal/agents` PASS |
|
||||
| 3 | runtime 测试初次失败于未使用的 `os` import | 删除测试中不再使用的 import | `go test ./internal/runtime` PASS |
|
||||
| 3 | `modernc.org/sqlite v1.50.1` 将 module 最低版本提升到 Go 1.25 | 降级到 `modernc.org/sqlite v1.35.0`,清理高版本间接依赖,并恢复 `go 1.22` | `go test ./internal/runtime` PASS |
|
||||
| 3 | 单侧 SQLite 缺失时聚合来源仍可能显示整体 high confidence | 增加 `Snapshot.Sources`,按 `state` / `goals` 分别记录 `sqlite_missing` 或 `sqlite_readonly`,聚合来源使用 `sqlite_partial` | `go test ./internal/runtime` PASS |
|
||||
|
||||
Reference in New Issue
Block a user