You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -21,6 +21,9 @@ type fakeACPBridge struct {
|
||||
sessions []ACPSession
|
||||
perms []ACPPermission
|
||||
terminated []uuid.UUID
|
||||
turns map[uuid.UUID][]ACPTurn
|
||||
dashboard *ACPRunDashboard
|
||||
dashIn ACPRunDashboardInput
|
||||
}
|
||||
|
||||
func (f *fakeACPBridge) ListAgentKinds(_ context.Context, _ uuid.UUID, _ bool) ([]ACPAgentKind, error) {
|
||||
@@ -70,6 +73,21 @@ func (f *fakeACPBridge) ListPendingPermissions(_ context.Context, _ uuid.UUID, _
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (f *fakeACPBridge) ListPendingApprovals(_ context.Context, _ uuid.UUID, _ bool) ([]ACPPermission, error) {
|
||||
out := make([]ACPPermission, 0, len(f.perms))
|
||||
out = append(out, f.perms...)
|
||||
return out, nil
|
||||
}
|
||||
func (f *fakeACPBridge) ListSessionTurns(_ context.Context, _ uuid.UUID, _ bool, sessionID uuid.UUID) ([]ACPTurn, error) {
|
||||
return f.turns[sessionID], nil
|
||||
}
|
||||
func (f *fakeACPBridge) GetRunDashboard(_ context.Context, _ uuid.UUID, _ bool, in ACPRunDashboardInput) (*ACPRunDashboard, error) {
|
||||
f.dashIn = in
|
||||
if f.dashboard != nil {
|
||||
return f.dashboard, nil
|
||||
}
|
||||
return &ACPRunDashboard{From: time.Now().Add(-24 * time.Hour), To: time.Now()}, nil
|
||||
}
|
||||
|
||||
func acpTestDeps() (ServerDeps, *fakeACPBridge) {
|
||||
deps := testDeps()
|
||||
@@ -216,3 +234,152 @@ func TestListPendingPermissions(t *testing.T) {
|
||||
assert.Equal(t, "fs/write", out.Permissions[0].ToolName)
|
||||
assert.Equal(t, "pending", out.Permissions[0].Status)
|
||||
}
|
||||
|
||||
func TestListPendingApprovals(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
sidA := uuid.New()
|
||||
sidB := uuid.New()
|
||||
bridge.perms = []ACPPermission{
|
||||
{ID: uuid.New(), SessionID: sidA, AgentRequestID: "r1", ToolName: "fs/write", Status: "pending", CreatedAt: time.Now()},
|
||||
{ID: uuid.New(), SessionID: sidB, AgentRequestID: "r2", ToolName: "shell/exec", Status: "pending", CreatedAt: time.Now()},
|
||||
}
|
||||
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_pending_approvals", map[string]any{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
var out listPendingPermissionsOut
|
||||
unmarshalStructured(t, result, &out)
|
||||
// Cross-session: both pending requests returned regardless of session.
|
||||
require.Len(t, out.Permissions, 2)
|
||||
}
|
||||
|
||||
func TestGetACPSession_IncludesLastStopReason(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
sid := uuid.New()
|
||||
reason := "end_turn"
|
||||
bridge.sessions = []ACPSession{{
|
||||
ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
|
||||
AgentKindID: uuid.New(), UserID: testUID, Status: "running",
|
||||
LastStopReason: &reason, StartedAt: time.Now(),
|
||||
}}
|
||||
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_acp_session", map[string]any{
|
||||
"session_id": sid.String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
var out acpSessionDetail
|
||||
unmarshalStructured(t, result, &out)
|
||||
assert.Equal(t, "end_turn", out.LastStopReason)
|
||||
}
|
||||
|
||||
func TestGetACPSessionTurns(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
sid := uuid.New()
|
||||
bridge.sessions = []ACPSession{{
|
||||
ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
|
||||
AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
|
||||
}}
|
||||
stop := "end_turn"
|
||||
completed := time.Now()
|
||||
bridge.turns = map[uuid.UUID][]ACPTurn{
|
||||
sid: {
|
||||
{TurnIndex: 0, Status: "completed", StopReason: &stop, UpdateCount: 3, StartedAt: time.Now(), CompletedAt: &completed},
|
||||
{TurnIndex: 1, Status: "in_progress", UpdateCount: 0, StartedAt: time.Now()},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_acp_session_turns", map[string]any{
|
||||
"session_id": sid.String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
var out getACPSessionTurnsOut
|
||||
unmarshalStructured(t, result, &out)
|
||||
require.Len(t, out.Turns, 2)
|
||||
assert.Equal(t, 0, out.Turns[0].TurnIndex)
|
||||
assert.Equal(t, "completed", out.Turns[0].Status)
|
||||
assert.Equal(t, "end_turn", out.Turns[0].StopReason)
|
||||
assert.Equal(t, 3, out.Turns[0].UpdateCount)
|
||||
assert.Equal(t, "in_progress", out.Turns[1].Status)
|
||||
assert.Empty(t, out.Turns[1].StopReason)
|
||||
}
|
||||
|
||||
func TestGetACPSessionTurns_CrossProjectDenied(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
sid := uuid.New()
|
||||
otherProject := uuid.New()
|
||||
bridge.sessions = []ACPSession{{
|
||||
ID: sid, WorkspaceID: testWSID, ProjectID: otherProject,
|
||||
AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
|
||||
}}
|
||||
|
||||
// caller scope 限定到 testPID,session 属于 otherProject → 拒绝
|
||||
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
|
||||
"get_acp_session_turns", map[string]any{"session_id": sid.String()})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.IsError, "cross-project access must be denied")
|
||||
}
|
||||
|
||||
func TestGetRunDashboard(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
day := time.Now().UTC()
|
||||
bridge.dashboard = &ACPRunDashboard{
|
||||
From: day.Add(-24 * time.Hour), To: day,
|
||||
Total: 6, Succeeded: 4, Crashed: 2, Active: 0,
|
||||
SuccessRate: 4.0 / 6.0, TotalCostUSD: 2.25, TotalTokens: 1500,
|
||||
AvgDurationSeconds: 42.5,
|
||||
ByDay: []ACPRunDayRow{
|
||||
{Day: day, Total: 6, Succeeded: 4, Crashed: 2, TotalCostUSD: 2.25},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError, "body=%v", result)
|
||||
|
||||
var out getRunDashboardOut
|
||||
unmarshalStructured(t, result, &out)
|
||||
assert.Equal(t, int64(6), out.Total)
|
||||
assert.Equal(t, int64(4), out.Succeeded)
|
||||
assert.Equal(t, int64(2), out.Crashed)
|
||||
assert.InDelta(t, 4.0/6.0, out.SuccessRate, 1e-9)
|
||||
assert.InDelta(t, 2.25, out.TotalCostUSD, 1e-9)
|
||||
assert.Equal(t, int64(1500), out.TotalTokens)
|
||||
assert.InDelta(t, 42.5, out.AvgDurationSeconds, 1e-9)
|
||||
require.Len(t, out.ByDay, 1)
|
||||
assert.Equal(t, int64(6), out.ByDay[0].Total)
|
||||
}
|
||||
|
||||
func TestGetRunDashboard_PassesFilters(t *testing.T) {
|
||||
deps, bridge := acpTestDeps()
|
||||
pid := uuid.New()
|
||||
reqID := uuid.New()
|
||||
from := time.Now().Add(-72 * time.Hour).UTC().Truncate(time.Second)
|
||||
to := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{
|
||||
"project_id": pid.String(),
|
||||
"requirement_id": reqID.String(),
|
||||
"from": from.Format(time.RFC3339),
|
||||
"to": to.Format(time.RFC3339),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError, "body=%v", result)
|
||||
|
||||
require.NotNil(t, bridge.dashIn.ProjectID)
|
||||
assert.Equal(t, pid, *bridge.dashIn.ProjectID)
|
||||
require.NotNil(t, bridge.dashIn.RequirementID)
|
||||
assert.Equal(t, reqID, *bridge.dashIn.RequirementID)
|
||||
assert.True(t, from.Equal(bridge.dashIn.From))
|
||||
assert.True(t, to.Equal(bridge.dashIn.To))
|
||||
}
|
||||
|
||||
func TestGetRunDashboard_BadFilter(t *testing.T) {
|
||||
deps, _ := acpTestDeps()
|
||||
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{
|
||||
"project_id": "not-a-uuid",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.IsError, "invalid project_id must error")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user