422 lines
16 KiB
Go
422 lines
16 KiB
Go
package runtime
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func TestStoreMissingSQLiteReturnsEmptySnapshot(t *testing.T) {
|
|
root := t.TempDir()
|
|
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 || len(snapshot.Goals) != 0 {
|
|
t.Fatalf("expected empty snapshot, got %#v", snapshot)
|
|
}
|
|
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)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "state_5.sqlite")); !os.IsNotExist(err) {
|
|
t.Fatalf("state sqlite was created or stat failed unexpectedly: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "goals_1.sqlite")); !os.IsNotExist(err) {
|
|
t.Fatalf("goals sqlite was created or stat failed unexpectedly: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreReadsThreadsEdgesAndGoalsFromReadonlySQLite(t *testing.T) {
|
|
root := t.TempDir()
|
|
createRuntimeSQLite(t, root)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if len(snapshot.Threads) != 2 {
|
|
t.Fatalf("threads = %#v", snapshot.Threads)
|
|
}
|
|
if snapshot.Threads[0].ID != "thread-a" || snapshot.Threads[0].Role != "analyst" {
|
|
t.Fatalf("unexpected first thread: %#v", snapshot.Threads[0])
|
|
}
|
|
if len(snapshot.SpawnEdges) != 1 || snapshot.SpawnEdges[0].FromThreadID != "thread-a" || snapshot.SpawnEdges[0].ToThreadID != "thread-b" || snapshot.SpawnEdges[0].Reason != "handoff" {
|
|
t.Fatalf("unexpected edges: %#v", snapshot.SpawnEdges)
|
|
}
|
|
if len(snapshot.Goals) != 1 || snapshot.Goals[0].ThreadID != "thread-b" || snapshot.Goals[0].Status != "in_progress" {
|
|
t.Fatalf("unexpected goals: %#v", snapshot.Goals)
|
|
}
|
|
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 TestStoreReadsCurrentCodexRuntimeSchema(t *testing.T) {
|
|
root := t.TempDir()
|
|
stateDB := openWritableSQLite(t, filepath.Join(root, "state_5.sqlite"))
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
rollout_path TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
source TEXT NOT NULL,
|
|
model_provider TEXT NOT NULL,
|
|
cwd TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
sandbox_policy TEXT NOT NULL,
|
|
approval_mode TEXT NOT NULL,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
has_user_event INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
archived_at INTEGER,
|
|
git_sha TEXT,
|
|
git_branch TEXT,
|
|
git_origin_url TEXT,
|
|
cli_version TEXT NOT NULL DEFAULT '',
|
|
first_user_message TEXT NOT NULL DEFAULT '',
|
|
agent_nickname TEXT,
|
|
agent_role TEXT,
|
|
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
model TEXT,
|
|
reasoning_effort TEXT,
|
|
agent_path TEXT,
|
|
created_at_ms INTEGER,
|
|
updated_at_ms INTEGER,
|
|
thread_source TEXT,
|
|
preview TEXT NOT NULL DEFAULT ''
|
|
)`)
|
|
execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges (
|
|
parent_thread_id TEXT NOT NULL,
|
|
child_thread_id TEXT NOT NULL PRIMARY KEY,
|
|
status TEXT NOT NULL
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (
|
|
id, rollout_path, created_at, updated_at, source, model_provider, cwd, title, sandbox_policy, approval_mode,
|
|
cli_version, agent_nickname, agent_role, agent_path, created_at_ms, updated_at_ms, thread_source, preview
|
|
) VALUES
|
|
('thread-a', '/tmp/a.jsonl', 1, 2, 'codex', 'openai', '/repo/a', '主线程', 'workspace-write', 'on-request',
|
|
'0.0.1', '主控', '智能体编排者', '/agents/orchestrator.toml', 1000, 2000, 'cli', '监管项目流程'),
|
|
('thread-b', '/tmp/b.jsonl', 3, 4, 'codex', 'openai', '/repo/b', '审查线程', 'workspace-write', 'on-request',
|
|
'0.0.1', '审查员', '代码审查员', '/agents/reviewer.toml', 3000, 4000, 'cli', '审查实现')`)
|
|
execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (parent_thread_id, child_thread_id, status) VALUES
|
|
('thread-a', 'thread-b', 'spawned')`)
|
|
|
|
goalsDB := openWritableSQLite(t, filepath.Join(root, "goals_1.sqlite"))
|
|
defer goalsDB.Close()
|
|
execSQL(t, goalsDB, `CREATE TABLE thread_goals (
|
|
thread_id TEXT PRIMARY KEY NOT NULL,
|
|
goal_id TEXT NOT NULL,
|
|
objective TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
token_budget INTEGER,
|
|
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
time_used_seconds INTEGER NOT NULL DEFAULT 0,
|
|
created_at_ms INTEGER NOT NULL,
|
|
updated_at_ms INTEGER NOT NULL
|
|
)`)
|
|
execSQL(t, goalsDB, `INSERT INTO thread_goals (
|
|
thread_id, goal_id, objective, status, token_budget, tokens_used, time_used_seconds, created_at_ms, updated_at_ms
|
|
) VALUES
|
|
('thread-a', 'goal-a', '管理当前项目的智能体流程', 'active', 10000, 1200, 60, 1000, 2000)`)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if len(snapshot.Threads) != 2 {
|
|
t.Fatalf("threads = %#v", snapshot.Threads)
|
|
}
|
|
first := snapshot.Threads[0]
|
|
if first.ID != "thread-a" || first.CWD != "/repo/a" || first.Title != "主线程" {
|
|
t.Fatalf("unexpected first thread identity: %#v", first)
|
|
}
|
|
if first.AgentNickname != "主控" || first.AgentRole != "智能体编排者" || first.Role != "智能体编排者" {
|
|
t.Fatalf("unexpected first thread agent fields: %#v", first)
|
|
}
|
|
if first.AgentPath != "/agents/orchestrator.toml" || first.Preview != "监管项目流程" || first.CreatedAt != "1000" || first.UpdatedAt != "2000" {
|
|
t.Fatalf("unexpected first thread metadata: %#v", first)
|
|
}
|
|
if len(snapshot.SpawnEdges) != 1 || snapshot.SpawnEdges[0].FromThreadID != "thread-a" || snapshot.SpawnEdges[0].ToThreadID != "thread-b" || snapshot.SpawnEdges[0].Reason != "spawned" {
|
|
t.Fatalf("unexpected current-schema edges: %#v", snapshot.SpawnEdges)
|
|
}
|
|
if len(snapshot.Goals) != 1 || snapshot.Goals[0].ThreadID != "thread-a" || snapshot.Goals[0].Goal != "管理当前项目的智能体流程" || snapshot.Goals[0].Status != "active" || snapshot.Goals[0].UpdatedAt != "2000" {
|
|
t.Fatalf("unexpected current-schema goals: %#v", snapshot.Goals)
|
|
}
|
|
}
|
|
|
|
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 != "medium" || 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 != "medium" || snapshot.Source.Kind != "sqlite_partial" {
|
|
t.Fatalf("unexpected aggregate source evidence: %#v", snapshot.Source)
|
|
}
|
|
}
|
|
|
|
func TestStoreToleratesMissingOptionalRuntimeColumns(t *testing.T) {
|
|
root := t.TempDir()
|
|
stateDB := openWritableSQLite(t, filepath.Join(root, "state_5.sqlite"))
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
role TEXT,
|
|
status TEXT,
|
|
created_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges (
|
|
from_thread_id TEXT,
|
|
to_thread_id TEXT,
|
|
created_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (id, role, status, created_at) VALUES
|
|
('thread-a', 'analyst', 'done', '2026-05-25T01:00:00Z')`)
|
|
execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (from_thread_id, to_thread_id, created_at) VALUES
|
|
('thread-a', 'thread-b', '2026-05-25T01:06:00Z')`)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if len(snapshot.Threads) != 1 || snapshot.Threads[0].UpdatedAt != "" {
|
|
t.Fatalf("unexpected threads for missing updated_at: %#v", snapshot.Threads)
|
|
}
|
|
if len(snapshot.SpawnEdges) != 1 || snapshot.SpawnEdges[0].Reason != "" {
|
|
t.Fatalf("unexpected edges for missing reason: %#v", snapshot.SpawnEdges)
|
|
}
|
|
}
|
|
|
|
func TestStoreToleratesNullRuntimeValues(t *testing.T) {
|
|
root := t.TempDir()
|
|
stateDB := openWritableSQLite(t, filepath.Join(root, "state_5.sqlite"))
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
role TEXT,
|
|
status TEXT,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges (
|
|
from_thread_id TEXT,
|
|
to_thread_id TEXT,
|
|
reason TEXT,
|
|
created_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (id, role, status, created_at, updated_at) VALUES
|
|
('thread-a', NULL, NULL, NULL, NULL)`)
|
|
execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (from_thread_id, to_thread_id, reason, created_at) VALUES
|
|
('thread-a', 'thread-b', NULL, NULL)`)
|
|
goalsDB := openWritableSQLite(t, filepath.Join(root, "goals_1.sqlite"))
|
|
defer goalsDB.Close()
|
|
execSQL(t, goalsDB, `CREATE TABLE thread_goals (
|
|
thread_id TEXT,
|
|
goal TEXT,
|
|
status TEXT,
|
|
updated_at TEXT
|
|
)`)
|
|
execSQL(t, goalsDB, `INSERT INTO thread_goals (thread_id, goal, status, updated_at) VALUES
|
|
('thread-a', NULL, NULL, NULL)`)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if snapshot.Threads[0].Role != "" || snapshot.Threads[0].Status != "" || snapshot.Threads[0].CreatedAt != "" || snapshot.Threads[0].UpdatedAt != "" {
|
|
t.Fatalf("NULL thread fields were not converted to empty strings: %#v", snapshot.Threads[0])
|
|
}
|
|
if snapshot.SpawnEdges[0].Reason != "" || snapshot.SpawnEdges[0].CreatedAt != "" {
|
|
t.Fatalf("NULL edge fields were not converted to empty strings: %#v", snapshot.SpawnEdges[0])
|
|
}
|
|
if snapshot.Goals[0].Goal != "" || snapshot.Goals[0].Status != "" || snapshot.Goals[0].UpdatedAt != "" {
|
|
t.Fatalf("NULL goal fields were not converted to empty strings: %#v", snapshot.Goals[0])
|
|
}
|
|
}
|
|
|
|
func TestStoreCastsNumericRuntimeValuesToText(t *testing.T) {
|
|
root := t.TempDir()
|
|
stateDB := openWritableSQLite(t, filepath.Join(root, "state_5.sqlite"))
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
id INTEGER PRIMARY KEY,
|
|
role INTEGER,
|
|
status INTEGER,
|
|
created_at INTEGER,
|
|
updated_at INTEGER
|
|
)`)
|
|
execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges (
|
|
from_thread_id INTEGER,
|
|
to_thread_id INTEGER,
|
|
reason INTEGER,
|
|
created_at INTEGER
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (id, role, status, created_at, updated_at) VALUES
|
|
(7, 8, 9, 10, 11)`)
|
|
execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (from_thread_id, to_thread_id, reason, created_at) VALUES
|
|
(7, 12, 13, 14)`)
|
|
goalsDB := openWritableSQLite(t, filepath.Join(root, "goals_1.sqlite"))
|
|
defer goalsDB.Close()
|
|
execSQL(t, goalsDB, `CREATE TABLE thread_goals (
|
|
thread_id INTEGER,
|
|
goal INTEGER,
|
|
status INTEGER,
|
|
updated_at INTEGER
|
|
)`)
|
|
execSQL(t, goalsDB, `INSERT INTO thread_goals (thread_id, goal, status, updated_at) VALUES
|
|
(7, 15, 16, 17)`)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if snapshot.Threads[0].ID != "7" || snapshot.Threads[0].Role != "8" || snapshot.Threads[0].UpdatedAt != "11" {
|
|
t.Fatalf("numeric thread fields were not cast to text: %#v", snapshot.Threads[0])
|
|
}
|
|
if snapshot.SpawnEdges[0].FromThreadID != "7" || snapshot.SpawnEdges[0].Reason != "13" {
|
|
t.Fatalf("numeric edge fields were not cast to text: %#v", snapshot.SpawnEdges[0])
|
|
}
|
|
if snapshot.Goals[0].ThreadID != "7" || snapshot.Goals[0].Goal != "15" {
|
|
t.Fatalf("numeric goal fields were not cast to text: %#v", snapshot.Goals[0])
|
|
}
|
|
}
|
|
|
|
func TestStoreReturnsEmptyTableEvidenceWhenCriticalColumnMissing(t *testing.T) {
|
|
root := t.TempDir()
|
|
stateDB := openWritableSQLite(t, filepath.Join(root, "state_5.sqlite"))
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
role TEXT,
|
|
status TEXT,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (role, status, created_at, updated_at) VALUES
|
|
('analyst', 'done', '2026-05-25T01:00:00Z', '2026-05-25T01:05:00Z')`)
|
|
|
|
snapshot, err := Store{CodexHome: root}.Snapshot()
|
|
if err != nil {
|
|
t.Fatalf("Snapshot returned error: %v", err)
|
|
}
|
|
if len(snapshot.Threads) != 0 {
|
|
t.Fatalf("threads = %#v, want empty when critical id column is missing", snapshot.Threads)
|
|
}
|
|
if source := snapshot.Sources["threads"]; source.Kind != "sqlite_schema_drift" || source.Confidence != "low" {
|
|
t.Fatalf("unexpected threads schema source: %#v", source)
|
|
}
|
|
}
|
|
|
|
func createRuntimeSQLite(t *testing.T, root string) {
|
|
t.Helper()
|
|
statePath := filepath.Join(root, "state_5.sqlite")
|
|
goalsPath := filepath.Join(root, "goals_1.sqlite")
|
|
stateDB := openWritableSQLite(t, statePath)
|
|
defer stateDB.Close()
|
|
execSQL(t, stateDB, `CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
role TEXT,
|
|
status TEXT,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges (
|
|
from_thread_id TEXT,
|
|
to_thread_id TEXT,
|
|
reason TEXT,
|
|
created_at TEXT
|
|
)`)
|
|
execSQL(t, stateDB, `INSERT INTO threads (id, role, status, created_at, updated_at) VALUES
|
|
('thread-a', 'analyst', 'done', '2026-05-25T01:00:00Z', '2026-05-25T01:05:00Z'),
|
|
('thread-b', 'operator', 'running', '2026-05-25T01:06:00Z', '2026-05-25T01:07:00Z')`)
|
|
execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (from_thread_id, to_thread_id, reason, created_at) VALUES
|
|
('thread-a', 'thread-b', 'handoff', '2026-05-25T01:06:00Z')`)
|
|
|
|
goalsDB := openWritableSQLite(t, goalsPath)
|
|
defer goalsDB.Close()
|
|
execSQL(t, goalsDB, `CREATE TABLE thread_goals (
|
|
thread_id TEXT,
|
|
goal TEXT,
|
|
status TEXT,
|
|
updated_at TEXT
|
|
)`)
|
|
execSQL(t, goalsDB, `INSERT INTO thread_goals (thread_id, goal, status, updated_at) VALUES
|
|
('thread-b', 'ship phase 3', 'in_progress', '2026-05-25T01:08:00Z')`)
|
|
}
|
|
|
|
func openWritableSQLite(t *testing.T, path string) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func execSQL(t *testing.T, db *sql.DB, query string) {
|
|
t.Helper()
|
|
if _, err := db.Exec(query); err != nil {
|
|
t.Fatalf("exec %q: %v", query, err)
|
|
}
|
|
}
|