You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -234,6 +234,86 @@ func TestPgRepo_Event_OrderAndPurge(t *testing.T) {
|
||||
assert.Equal(t, int64(3), n)
|
||||
}
|
||||
|
||||
func TestPgRepo_Turn_Lifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
repo, pool := setupRepo(t)
|
||||
|
||||
uid := mustInsertUser(t, ctx, pool, "u6@local", false)
|
||||
pid, wsid := mustInsertProjectWorkspace(t, ctx, pool, uid)
|
||||
k, _ := repo.CreateAgentKind(ctx, &acp.AgentKind{
|
||||
ID: uuid.New(), Name: "kind6", DisplayName: "x",
|
||||
BinaryPath: "x", Enabled: true, CreatedBy: uid,
|
||||
})
|
||||
sess, _ := repo.InsertSession(ctx, &acp.Session{
|
||||
ID: uuid.New(), WorkspaceID: wsid, ProjectID: pid,
|
||||
AgentKindID: k.ID, UserID: uid,
|
||||
Branch: "main", CwdPath: "/tmp", IsMainWorktree: true, Status: acp.SessionRunning,
|
||||
})
|
||||
|
||||
// 新 session 无回合
|
||||
latest, err := repo.GetLatestTurnBySession(ctx, sess.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, latest)
|
||||
|
||||
// 插入回合 0
|
||||
pidStr := "client-init"
|
||||
turn, err := repo.InsertTurn(ctx, &acp.Turn{
|
||||
SessionID: sess.ID, TurnIndex: 0, PromptRequestID: &pidStr, Status: acp.TurnInProgress,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, turn.TurnIndex)
|
||||
assert.Equal(t, acp.TurnInProgress, turn.Status)
|
||||
|
||||
// IncrementTurnUpdateCount ×2(开放回合)
|
||||
require.NoError(t, repo.IncrementTurnUpdateCount(ctx, sess.ID, 0))
|
||||
require.NoError(t, repo.IncrementTurnUpdateCount(ctx, sess.ID, 0))
|
||||
|
||||
// 完成回合:MarkTurnCompleted 用内存计数覆盖 update_count
|
||||
require.NoError(t, repo.MarkTurnCompleted(ctx, sess.ID, 0, "end_turn", 5))
|
||||
require.NoError(t, repo.UpdateSessionLastStopReason(ctx, sess.ID, "end_turn"))
|
||||
|
||||
ts, err := repo.ListTurnsBySession(ctx, sess.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ts, 1)
|
||||
assert.Equal(t, acp.TurnCompleted, ts[0].Status)
|
||||
require.NotNil(t, ts[0].StopReason)
|
||||
assert.Equal(t, "end_turn", *ts[0].StopReason)
|
||||
assert.Equal(t, 5, ts[0].UpdateCount)
|
||||
require.NotNil(t, ts[0].CompletedAt)
|
||||
|
||||
// last_stop_reason round-trips on session SELECT
|
||||
got, _ := repo.GetSessionByID(ctx, sess.ID)
|
||||
require.NotNil(t, got.LastStopReason)
|
||||
assert.Equal(t, "end_turn", *got.LastStopReason)
|
||||
|
||||
// GetLatestTurnBySession 返回 index 最大的回合
|
||||
latest, err = repo.GetLatestTurnBySession(ctx, sess.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, latest)
|
||||
assert.Equal(t, 0, latest.TurnIndex)
|
||||
|
||||
// UNIQUE(session_id, turn_index) 约束
|
||||
_, err = repo.InsertTurn(ctx, &acp.Turn{SessionID: sess.ID, TurnIndex: 0, Status: acp.TurnInProgress})
|
||||
require.Error(t, err, "duplicate (session_id, turn_index) must violate UNIQUE")
|
||||
|
||||
// 插入并中止回合 1
|
||||
_, err = repo.InsertTurn(ctx, &acp.Turn{SessionID: sess.ID, TurnIndex: 1, Status: acp.TurnInProgress})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, repo.MarkTurnAborted(ctx, sess.ID, 1))
|
||||
latest, _ = repo.GetLatestTurnBySession(ctx, sess.ID)
|
||||
assert.Equal(t, 1, latest.TurnIndex)
|
||||
assert.Equal(t, acp.TurnAborted, latest.Status)
|
||||
|
||||
// Purge before now+1m → 全删
|
||||
purged, err := repo.PurgeTurnsBefore(ctx, time.Now().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), purged)
|
||||
// 幂等
|
||||
purged2, _ := repo.PurgeTurnsBefore(ctx, time.Now().Add(time.Minute))
|
||||
assert.Equal(t, int64(0), purged2)
|
||||
}
|
||||
|
||||
// ===== test helpers =====
|
||||
|
||||
func mustInsertUser(t *testing.T, ctx context.Context, pool *pgxpool.Pool, email string, admin bool) uuid.UUID {
|
||||
@@ -259,3 +339,141 @@ func mustInsertProjectWorkspace(t *testing.T, ctx context.Context, pool *pgxpool
|
||||
require.NoError(t, err)
|
||||
return pid, wsid
|
||||
}
|
||||
|
||||
// TestPgRepo_UsagePersist_BumpsDashboardColumns 验证 AddSessionUsageTotals 同时
|
||||
// 把 dashboard rollup 列(cost_usd / tokens_total)与运行时累加器对齐。
|
||||
func TestPgRepo_UsagePersist_BumpsDashboardColumns(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
repo, pool := setupRepo(t)
|
||||
|
||||
uid := mustInsertUser(t, ctx, pool, "dash-usage@local", false)
|
||||
pid, wsid := mustInsertProjectWorkspace(t, ctx, pool, uid)
|
||||
k, _ := repo.CreateAgentKind(ctx, &acp.AgentKind{
|
||||
ID: uuid.New(), Name: "kind-dash-usage", DisplayName: "x",
|
||||
BinaryPath: "x", Enabled: true, CreatedBy: uid,
|
||||
})
|
||||
sess, _ := repo.InsertSession(ctx, &acp.Session{
|
||||
ID: uuid.New(), WorkspaceID: wsid, ProjectID: pid,
|
||||
AgentKindID: k.ID, UserID: uid,
|
||||
Branch: "main", CwdPath: "/tmp", IsMainWorktree: true, Status: acp.SessionRunning,
|
||||
})
|
||||
|
||||
// 两次回合累加。
|
||||
_, _, _, _, err := repo.AddSessionUsageTotals(ctx, sess.ID, 100, 50, 10, 0.30)
|
||||
require.NoError(t, err)
|
||||
_, _, _, totCost, err := repo.AddSessionUsageTotals(ctx, sess.ID, 200, 40, 0, 0.20)
|
||||
require.NoError(t, err)
|
||||
assert.InDelta(t, 0.50, totCost, 1e-9)
|
||||
|
||||
got, err := repo.GetSessionByID(ctx, sess.ID)
|
||||
require.NoError(t, err)
|
||||
assert.InDelta(t, 0.50, got.TotalCostUSD, 1e-9)
|
||||
// 运行时累加器 totals。
|
||||
assert.Equal(t, int64(300), got.PromptTokens)
|
||||
assert.Equal(t, int64(90), got.CompletionTokens)
|
||||
assert.Equal(t, int64(10), got.ThinkingTokens)
|
||||
|
||||
// dashboard 汇总应读到与运行时一致的 cost / tokens。
|
||||
rollup, err := repo.SessionRollup(ctx, acp.DashboardFilter{
|
||||
UserID: &uid, From: time.Now().Add(-time.Hour), To: time.Now().Add(time.Hour),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), rollup.Total)
|
||||
assert.Equal(t, int64(1), rollup.Active)
|
||||
assert.InDelta(t, 0.50, rollup.TotalCostUSD, 1e-9)
|
||||
assert.Equal(t, int64(400), rollup.TotalTokens) // 100+50+10 + 200+40+0
|
||||
}
|
||||
|
||||
// TestPgRepo_Dashboard_RollupTimelineByDay 覆盖三个 dashboard 查询:
|
||||
// SessionRollup(按状态计数 + 成本 + scope)、SessionsByDay、SessionTimeline。
|
||||
func TestPgRepo_Dashboard_RollupTimelineByDay(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
repo, pool := setupRepo(t)
|
||||
|
||||
owner := mustInsertUser(t, ctx, pool, "dash-owner@local", false)
|
||||
other := mustInsertUser(t, ctx, pool, "dash-other@local", false)
|
||||
pid, wsid := mustInsertProjectWorkspace(t, ctx, pool, owner)
|
||||
k, _ := repo.CreateAgentKind(ctx, &acp.AgentKind{
|
||||
ID: uuid.New(), Name: "kind-dash", DisplayName: "x",
|
||||
BinaryPath: "x", Enabled: true, CreatedBy: owner,
|
||||
})
|
||||
|
||||
mk := func(u uuid.UUID, status acp.SessionStatus) *acp.Session {
|
||||
s, err := repo.InsertSession(ctx, &acp.Session{
|
||||
ID: uuid.New(), WorkspaceID: wsid, ProjectID: pid,
|
||||
AgentKindID: k.ID, UserID: u,
|
||||
Branch: "b", CwdPath: "/tmp", IsMainWorktree: false, Status: status,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
exited := mk(owner, acp.SessionRunning)
|
||||
crashed := mk(owner, acp.SessionRunning)
|
||||
mk(other, acp.SessionRunning) // 另一用户:owner-scope 下不计入
|
||||
|
||||
// owner 的两个会话各累加成本并结束。
|
||||
_, _, _, _, err := repo.AddSessionUsageTotals(ctx, exited.ID, 100, 100, 0, 1.00)
|
||||
require.NoError(t, err)
|
||||
_, _, _, _, err = repo.AddSessionUsageTotals(ctx, crashed.ID, 50, 50, 0, 0.50)
|
||||
require.NoError(t, err)
|
||||
ec := int32(0)
|
||||
require.NoError(t, repo.UpdateSessionFinished(ctx, exited.ID, acp.SessionExited, &ec, nil))
|
||||
require.NoError(t, repo.UpdateSessionFinished(ctx, crashed.ID, acp.SessionCrashed, &ec, nil))
|
||||
|
||||
from := time.Now().Add(-time.Hour)
|
||||
to := time.Now().Add(time.Hour)
|
||||
|
||||
// owner scope:只统计 owner 的 2 个会话。
|
||||
ownerScope := acp.DashboardFilter{UserID: &owner, From: from, To: to}
|
||||
rollup, err := repo.SessionRollup(ctx, ownerScope)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), rollup.Total)
|
||||
assert.Equal(t, int64(1), rollup.Succeeded) // exited
|
||||
assert.Equal(t, int64(1), rollup.Crashed)
|
||||
assert.Equal(t, int64(0), rollup.Active)
|
||||
assert.InDelta(t, 1.50, rollup.TotalCostUSD, 1e-9)
|
||||
assert.True(t, rollup.AvgDurationSeconds >= 0)
|
||||
|
||||
// admin scope(UserID nil):跨用户 3 个会话。
|
||||
adminRollup, err := repo.SessionRollup(ctx, acp.DashboardFilter{From: from, To: to})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), adminRollup.Total)
|
||||
|
||||
// SessionsByDay:owner scope 当天 1 行,total=2。
|
||||
byDay, err := repo.SessionsByDay(ctx, ownerScope)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, byDay, 1)
|
||||
assert.Equal(t, int64(2), byDay[0].Total)
|
||||
assert.Equal(t, int64(1), byDay[0].Succeeded)
|
||||
assert.InDelta(t, 1.50, byDay[0].TotalCostUSD, 1e-9)
|
||||
|
||||
// SessionTimeline:插入两类事件,按桶聚合。
|
||||
method := "session/update"
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err := repo.InsertEvent(ctx, &acp.Event{
|
||||
SessionID: exited.ID, Direction: acp.DirectionOut, RPCKind: acp.RPCNotification,
|
||||
Method: &method, Payload: []byte(`{}`), PayloadSize: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
reqMethod := "session/prompt"
|
||||
_, err = repo.InsertEvent(ctx, &acp.Event{
|
||||
SessionID: exited.ID, Direction: acp.DirectionOut, RPCKind: acp.RPCRequest,
|
||||
Method: &reqMethod, Payload: []byte(`{}`), PayloadSize: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
timeline, err := repo.SessionTimeline(ctx, exited.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, timeline, 2) // (out,notification,session/update) + (out,request,session/prompt)
|
||||
total := int64(0)
|
||||
for _, b := range timeline {
|
||||
total += b.EventCount
|
||||
assert.False(t, b.FirstAt.IsZero())
|
||||
assert.False(t, b.LastAt.IsZero())
|
||||
}
|
||||
assert.Equal(t, int64(4), total)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user