Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

444 lines
16 KiB
Go

package db
import (
"path/filepath"
"strings"
"testing"
)
func TestAgentInvocations_InsertAndReadBack(t *testing.T) {
d, _, run := openSessionTestDB(t)
inv := AgentInvocation{
RunID: run.ID,
StepName: "review",
Round: 2,
Purpose: "review-fix",
Agent: "codex",
Model: "gpt-5.2-codex",
SessionMode: InvocationModeResumed,
SessionKey: "abcd1234abcd1234",
StartedAt: 1_700_000_000,
CompletedAt: 1_700_000_090,
DurationMS: 90_000,
ExitStatus: "ok",
InputTokens: 1000,
OutputTokens: 200,
CacheReadTokens: 800,
CacheCreationTokens: intPtr(50),
}
if _, err := d.InsertAgentInvocation(inv); err != nil {
t.Fatalf("insert: %v", err)
}
got, err := d.GetAgentInvocationsByRun(run.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if len(got) != 1 {
t.Fatalf("got %d invocations, want 1", len(got))
}
back := got[0]
if back.Purpose != "review-fix" || back.Round != 2 || back.SessionMode != InvocationModeResumed ||
back.DurationMS != 90_000 || back.InputTokens != 1000 || back.CacheReadTokens != 800 || back.Model != "gpt-5.2-codex" {
t.Fatalf("readback mismatch: %+v", back)
}
if back.CacheCreationTokens == nil || *back.CacheCreationTokens != 50 {
t.Fatalf("cache creation readback = %v, want 50", back.CacheCreationTokens)
}
}
func intPtr(v int) *int { return &v }
// TestAgentInvocations_NullableFidelityFieldsRoundTrip proves the session-
// fidelity columns survive an insert/read cycle both when populated and when
// left unknown (NULL), so missing data reads back as nil rather than zero.
func TestAgentInvocations_NullableFidelityFieldsRoundTrip(t *testing.T) {
d, _, run := openSessionTestDB(t)
// Fully populated row.
full := AgentInvocation{
RunID: run.ID, StepName: "review", Round: 2, Purpose: "review", Agent: "codex",
Model: "gpt-5.6-sol", ModelProvider: strPtr("openai"),
SessionMode: InvocationModeFallback, SessionKey: "key1", FallbackReason: strPtr(FallbackReasonExit),
StartedAt: 1, CompletedAt: 2, DurationMS: 5000, SubprocessWaitMS: int64Ptr(1200),
ExitStatus: "ok", InputTokens: 2500, OutputTokens: 250, CacheReadTokens: 1800,
CacheCreationTokens: intPtr(0), FreshInputTokens: intPtr(700), ReasoningTokens: intPtr(9),
DeltaInputTokens: intPtr(1500), DeltaOutputTokens: intPtr(150), DeltaCacheReadTokens: intPtr(1200),
ModelRoundtrips: intPtr(4), ToolCalls: intPtr(3),
ToolWaitCalls: intPtr(0), ToolTestLintCalls: intPtr(1), ToolEditCalls: intPtr(1),
ToolReadCalls: intPtr(1), ToolGitCalls: intPtr(0), ToolOtherCalls: intPtr(0),
WorkloadFiles: intPtr(4), WorkloadLines: intPtr(120), FindingCount: intPtr(2),
}
if _, err := d.InsertAgentInvocation(full); err != nil {
t.Fatalf("insert full: %v", err)
}
// Minimal row: every nullable field unknown.
minimal := AgentInvocation{
RunID: run.ID, StepName: "test", Round: 1, Purpose: "test-evidence", Agent: "codex",
SessionMode: InvocationModeCold, StartedAt: 3, CompletedAt: 4, DurationMS: 10, ExitStatus: "ok",
}
if _, err := d.InsertAgentInvocation(minimal); err != nil {
t.Fatalf("insert minimal: %v", err)
}
got, err := d.GetAgentInvocationsByRun(run.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if len(got) != 2 {
t.Fatalf("got %d rows, want 2", len(got))
}
f := got[0]
if f.ModelProvider == nil || *f.ModelProvider != "openai" ||
f.FallbackReason == nil || *f.FallbackReason != FallbackReasonExit ||
f.SubprocessWaitMS == nil || *f.SubprocessWaitMS != 1200 ||
f.CacheCreationTokens == nil || *f.CacheCreationTokens != 0 ||
f.DeltaInputTokens == nil || *f.DeltaInputTokens != 1500 ||
f.ToolTestLintCalls == nil || *f.ToolTestLintCalls != 1 ||
f.FindingCount == nil || *f.FindingCount != 2 {
t.Fatalf("full row lost a fidelity field: %+v", f)
}
m := got[1]
for name, isNil := range map[string]bool{
"ModelProvider": m.ModelProvider == nil,
"FallbackReason": m.FallbackReason == nil,
"SubprocessWaitMS": m.SubprocessWaitMS == nil,
"CacheCreation": m.CacheCreationTokens == nil,
"FreshInput": m.FreshInputTokens == nil,
"DeltaInput": m.DeltaInputTokens == nil,
"ModelRoundtrips": m.ModelRoundtrips == nil,
"ToolCalls": m.ToolCalls == nil,
"WorkloadFiles": m.WorkloadFiles == nil,
"FindingCount": m.FindingCount == nil,
} {
if !isNil {
t.Fatalf("minimal row %s should read back as unknown (nil)", name)
}
}
}
func strPtr(s string) *string { return &s }
func int64Ptr(v int64) *int64 { return &v }
// TestAgentInvocations_PrivacySafeShape guards the privacy boundary: the
// table has no column that could hold prompts, outputs, or diffs, and the
// session identity is stored only as a fingerprint column.
func TestAgentInvocations_PrivacySafeShape(t *testing.T) {
d, _, _ := openSessionTestDB(t)
rows, err := d.sql.Query(`SELECT name FROM pragma_table_info('agent_invocations')`)
if err != nil {
t.Fatalf("table info: %v", err)
}
defer rows.Close()
var columns []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
t.Fatal(err)
}
columns = append(columns, name)
}
for _, col := range columns {
lower := strings.ToLower(col)
if strings.HasSuffix(lower, "_tokens") {
continue // token counts are numeric usage data, not content
}
for _, forbidden := range []string{"prompt", "output", "diff", "transcript", "secret", "credential", "text", "content"} {
if strings.Contains(lower, forbidden) {
t.Fatalf("agent_invocations column %q could hold %s content", col, forbidden)
}
}
}
}
func TestAgentInvocations_HasRunTimelineIndex(t *testing.T) {
d, _, _ := openSessionTestDB(t)
rows, err := d.sql.Query(`SELECT name FROM pragma_index_list('agent_invocations')`)
if err != nil {
t.Fatalf("index list: %v", err)
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
t.Fatal(err)
}
if name == "idx_agent_invocations_run_started_id" {
return
}
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
t.Fatal("agent invocations must have a run timeline index")
}
func TestAgentInvocationAggregatesAndRunSummary(t *testing.T) {
d, _, run := openSessionTestDB(t)
seed := []AgentInvocation{
{RunID: run.ID, StepName: "review", Round: 1, Purpose: "review", Agent: "codex", SessionMode: InvocationModeStarted, StartedAt: 1, CompletedAt: 2, DurationMS: 100, ExitStatus: "ok", InputTokens: 10, OutputTokens: 5},
{RunID: run.ID, StepName: "review", Round: 2, Purpose: "review", Agent: "codex", SessionMode: InvocationModeResumed, StartedAt: 3, CompletedAt: 4, DurationMS: 50, ExitStatus: "ok", InputTokens: 10, OutputTokens: 5},
{RunID: run.ID, StepName: "review", Round: 2, Purpose: "review-fix", Agent: "codex", SessionMode: InvocationModeFallback, StartedAt: 5, CompletedAt: 6, DurationMS: 70, ExitStatus: "error", FailureCategory: "exit"},
}
for _, inv := range seed {
if _, err := d.InsertAgentInvocation(inv); err != nil {
t.Fatalf("insert: %v", err)
}
}
aggregates, err := d.AgentInvocationAggregates()
if err != nil {
t.Fatalf("aggregates: %v", err)
}
byPurpose := map[string]AgentInvocationAggregate{}
for _, a := range aggregates {
byPurpose[a.Purpose] = a
}
review := byPurpose["review"]
if review.Count != 2 || review.TotalDurationMS != 150 || review.AvgDurationMS != 75 ||
review.Started != 1 || review.Resumed != 1 || review.InputTokens != 20 {
t.Fatalf("review aggregate = %+v", review)
}
fix := byPurpose["review-fix"]
if fix.Count != 1 || fix.Fallback != 1 || fix.Errors != 1 {
t.Fatalf("review-fix aggregate = %+v", fix)
}
summary, err := d.AgentInvocationSummaryForRun(run.ID)
if err != nil {
t.Fatalf("run summary: %v", err)
}
if summary.Count != 3 || summary.Resumed != 1 || summary.Fallback != 1 || summary.TotalDurationMS != 220 {
t.Fatalf("run summary = %+v", summary)
}
}
func TestAgentInvocationAggregatesPreserveUnknownMetrics(t *testing.T) {
d, _, run := openSessionTestDB(t)
inv := AgentInvocation{
RunID: run.ID, StepName: "review", Round: 1, Purpose: "review", Agent: "codex",
SessionMode: InvocationModeCold, StartedAt: 1, CompletedAt: 2, DurationMS: 10, ExitStatus: "ok",
}
if _, err := d.InsertAgentInvocation(inv); err != nil {
t.Fatal(err)
}
aggregates, err := d.AgentInvocationAggregates()
if err != nil {
t.Fatal(err)
}
if len(aggregates) != 1 {
t.Fatalf("got %d aggregates, want 1", len(aggregates))
}
a := aggregates[0]
if a.SubprocessWaitMS != nil || a.CacheCreationTokens != nil ||
a.FreshInputTokens != nil || a.ReasoningTokens != nil ||
a.ModelRoundtrips != nil || a.ToolCalls != nil {
t.Fatalf("unknown aggregate metrics became recorded values: %+v", a)
}
}
func TestAgentInvocationAggregatesHidePartialMetrics(t *testing.T) {
d, _, run := openSessionTestDB(t)
for _, inv := range []AgentInvocation{
{RunID: run.ID, StepName: "review", Round: 1, Purpose: "review", Agent: "codex", SessionMode: InvocationModeCold, StartedAt: 1, CompletedAt: 2, DurationMS: 10, ExitStatus: "ok", FreshInputTokens: intPtr(3)},
{RunID: run.ID, StepName: "review", Round: 2, Purpose: "review", Agent: "codex", SessionMode: InvocationModeCold, StartedAt: 3, CompletedAt: 4, DurationMS: 10, ExitStatus: "ok"},
} {
if _, err := d.InsertAgentInvocation(inv); err != nil {
t.Fatal(err)
}
}
aggregates, err := d.AgentInvocationAggregates()
if err != nil {
t.Fatal(err)
}
if aggregates[0].FreshInputTokens != nil {
t.Fatalf("partial fresh input = %v, want nil", *aggregates[0].FreshInputTokens)
}
}
func TestAddRunParkedDurationAccumulates(t *testing.T) {
d, _, run := openSessionTestDB(t)
if err := d.AddRunParkedDuration(run.ID, 1500); err != nil {
t.Fatalf("add: %v", err)
}
if err := d.AddRunParkedDuration(run.ID, 500); err != nil {
t.Fatalf("add again: %v", err)
}
if err := d.AddRunParkedDuration(run.ID, 0); err != nil {
t.Fatalf("zero add: %v", err)
}
got, err := d.GetRun(run.ID)
if err != nil {
t.Fatalf("get run: %v", err)
}
if got.ParkedMS != 2000 {
t.Fatalf("ParkedMS = %d, want 2000", got.ParkedMS)
}
}
func TestCompleteRunAwaitingAgentAccumulatesParkedDuration(t *testing.T) {
d, _, run := openSessionTestDB(t)
if err := d.SetRunAwaitingAgent(run.ID); err != nil {
t.Fatalf("set awaiting: %v", err)
}
if err := d.CompleteRunAwaitingAgent(run.ID, 1500); err != nil {
t.Fatalf("complete awaiting: %v", err)
}
got, err := d.GetRun(run.ID)
if err != nil {
t.Fatalf("get run: %v", err)
}
if got.AwaitingAgentSince != nil {
t.Fatal("AwaitingAgentSince must be cleared after completion")
}
if got.ParkedMS != 1500 {
t.Fatalf("ParkedMS = %d, want 1500", got.ParkedMS)
}
}
// TestRecoverStaleRunsAccumulatesParkedTime proves a crash while parked does
// not lose the parked evidence: recovery folds the live awaiting marker into
// the run's parked total.
func TestRecoverStaleRunsAccumulatesParkedTime(t *testing.T) {
d, _, run := openSessionTestDB(t)
if err := d.UpdateRunStatus(run.ID, "running"); err != nil {
t.Fatalf("mark running: %v", err)
}
// Park the run 60 seconds in the past.
past := now() - 60
if _, err := d.sql.Exec(`UPDATE runs SET awaiting_agent_since = ? WHERE id = ?`, past, run.ID); err != nil {
t.Fatalf("seed awaiting: %v", err)
}
if _, err := d.RecoverStaleRuns("daemon crashed during execution"); err != nil {
t.Fatalf("recover: %v", err)
}
got, err := d.GetRun(run.ID)
if err != nil {
t.Fatalf("get run: %v", err)
}
if got.AwaitingAgentSince != nil {
t.Fatal("awaiting marker must be cleared by recovery")
}
if got.ParkedMS < 59_000 || got.ParkedMS > 62_000 {
t.Fatalf("ParkedMS = %d, want ~60000 accumulated from the crashed park", got.ParkedMS)
}
}
// TestOpenMigratesAgentInvocationsAndParkedMS proves databases created before
// the performance-telemetry schema gain the table and column on reopen.
func TestOpenMigratesAgentInvocationsAndParkedMS(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.sqlite")
d, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
if _, err := d.sql.Exec(`DROP TABLE agent_invocations`); err != nil {
t.Fatalf("drop table: %v", err)
}
// Simulate a pre-parked_ms runs table by rebuilding it without the column.
if _, err := d.sql.Exec(`ALTER TABLE runs DROP COLUMN parked_ms`); err != nil {
t.Fatalf("drop column: %v", err)
}
if err := d.Close(); err != nil {
t.Fatalf("close: %v", err)
}
d, err = Open(path)
if err != nil {
t.Fatalf("reopen: %v", err)
}
defer d.Close()
repo, err := d.InsertRepo("/tmp/repo", "https://github.com/test/repo", "main")
if err != nil {
t.Fatalf("insert repo: %v", err)
}
run, err := d.InsertRun(repo.ID, "b", "h", "b")
if err != nil {
t.Fatalf("insert run: %v", err)
}
if err := d.AddRunParkedDuration(run.ID, 10); err != nil {
t.Fatalf("parked_ms column missing after migration: %v", err)
}
if _, err := d.InsertAgentInvocation(AgentInvocation{RunID: run.ID, StepName: "review", Round: 1, Purpose: "review", Agent: "codex", SessionMode: InvocationModeCold, StartedAt: 1, CompletedAt: 2, DurationMS: 1, ExitStatus: "ok"}); err != nil {
t.Fatalf("agent_invocations table missing after migration: %v", err)
}
}
// TestOpenMigratesSessionFidelityColumns proves a database whose
// agent_invocations table predates the session-fidelity columns gains them on
// reopen, and that pre-existing rows read those columns back as unknown (nil)
// rather than a fabricated zero.
func TestOpenMigratesSessionFidelityColumns(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.sqlite")
d, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
repo, err := d.InsertRepo("/tmp/repo", "https://github.com/test/repo", "main")
if err != nil {
t.Fatalf("insert repo: %v", err)
}
run, err := d.InsertRun(repo.ID, "b", "h", "b")
if err != nil {
t.Fatalf("insert run: %v", err)
}
// Simulate a pre-fidelity table by dropping the new columns, then insert a
// legacy row that has no fidelity data.
for _, col := range []string{"model_provider", "fallback_reason", "subprocess_wait_ms",
"fresh_input_tokens", "reasoning_tokens", "model_roundtrips", "tool_calls", "finding_count"} {
if _, err := d.sql.Exec(`ALTER TABLE agent_invocations DROP COLUMN ` + col); err != nil {
t.Fatalf("drop %s: %v", col, err)
}
}
if _, err := d.sql.Exec(`INSERT INTO agent_invocations
(id, run_id, step_name, round, purpose, agent, model, session_mode, session_key, exit_status, failure_category, started_at, completed_at, duration_ms, input_tokens, output_tokens, cache_read_tokens)
VALUES ('legacy1', ?, 'review', 1, 'review', 'codex', '', 'started', '', 'ok', '', 1, 2, 100, 500, 20, 300)`, run.ID); err != nil {
t.Fatalf("insert legacy row: %v", err)
}
if err := d.Close(); err != nil {
t.Fatalf("close: %v", err)
}
d, err = Open(path)
if err != nil {
t.Fatalf("reopen: %v", err)
}
defer d.Close()
got, err := d.GetAgentInvocationsByRun(run.ID)
if err != nil {
t.Fatalf("get after migration: %v", err)
}
if len(got) != 1 {
t.Fatalf("got %d rows, want 1", len(got))
}
legacy := got[0]
if legacy.InputTokens != 500 {
t.Fatalf("legacy input tokens = %d, want 500", legacy.InputTokens)
}
if legacy.ModelProvider != nil || legacy.SubprocessWaitMS != nil ||
legacy.ModelRoundtrips != nil || legacy.ToolCalls != nil || legacy.FindingCount != nil {
t.Fatalf("legacy row must read new columns as unknown, got %+v", legacy)
}
// The migrated table now accepts the new fields.
if _, err := d.InsertAgentInvocation(AgentInvocation{
RunID: run.ID, StepName: "review", Round: 2, Purpose: "review", Agent: "codex",
SessionMode: InvocationModeResumed, StartedAt: 3, CompletedAt: 4, DurationMS: 1, ExitStatus: "ok",
ModelRoundtrips: intPtr(3), ToolCalls: intPtr(2), SubprocessWaitMS: int64Ptr(500),
}); err != nil {
t.Fatalf("insert after migration: %v", err)
}
}