You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/acp"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/project"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
||||
@@ -152,7 +153,7 @@ func TestHandler_Session_CreateAndWS_E2E(t *testing.T) {
|
||||
paStub := &stubProjectAccess{pj: &project.Project{ID: pid, Slug: "p-test"}}
|
||||
|
||||
svc := acp.NewSessionService(
|
||||
repo, wsStub, nil, nil, paStub, sup,
|
||||
repo, wsStub, nil, nil, paStub, nil, nil, sup,
|
||||
audit.NewPostgresRecorder(pool),
|
||||
acp.SessionServiceConfig{MaxActiveGlobal: 10, MaxActivePerUser: 5},
|
||||
nil, // mcpTokens — not exercised by handler e2e
|
||||
@@ -276,3 +277,140 @@ func TestHandler_Session_CreateAndWS_E2E(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSession_TurnCompletion_E2E drives a full session with an InitialPrompt
|
||||
// against the fake-acp-agent (which emits session/update chunks then a
|
||||
// session/prompt response with stopReason=end_turn). It asserts:
|
||||
// - an acp_turns row is created and completed with stop_reason=end_turn
|
||||
// - update_count == FAKE_ACP_PROMPT_UPDATES
|
||||
// - acp_sessions.last_stop_reason is set to end_turn
|
||||
// - a test TurnBus subscriber receives turn_completed + session_idle
|
||||
func TestSession_TurnCompletion_E2E(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("e2e: spawns subprocess + testcontainer pg")
|
||||
}
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
repo, pool := setupRepo(t)
|
||||
|
||||
uid := mustInsertUser(t, ctx, pool, "turn-e2e@local", false)
|
||||
pid, wsid := mustInsertProjectWorkspace(t, ctx, pool, uid)
|
||||
cwd := t.TempDir()
|
||||
_, err := pool.Exec(ctx, `UPDATE workspaces SET main_path = $1 WHERE id = $2`, cwd, wsid)
|
||||
require.NoError(t, err)
|
||||
|
||||
bin := fakeAgentBinary(t)
|
||||
enc := testEncryptor(t)
|
||||
|
||||
const wantUpdates = 4
|
||||
kind, err := repo.CreateAgentKind(ctx, &acp.AgentKind{
|
||||
ID: uuid.New(), Name: "turn-e2e-fake", DisplayName: "Fake",
|
||||
BinaryPath: bin, Args: []string{}, Enabled: true, CreatedBy: uid,
|
||||
// FAKE_ACP_PROMPT_UPDATES via encrypted env: emit N session/update before response.
|
||||
EncryptedEnv: mustEncryptEnv(t, enc, map[string]string{"FAKE_ACP_PROMPT_UPDATES": "4"}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
wsRow, err := workspace.NewPostgresRepository(pool).GetWorkspaceByID(ctx, wsid)
|
||||
require.NoError(t, err)
|
||||
|
||||
disp := notify.NewDispatcher(notifyRepoStub{}, slogDiscard())
|
||||
sup := acp.NewSupervisor(
|
||||
repo, audit.NewPostgresRecorder(pool), disp,
|
||||
nil, nil, nil, enc,
|
||||
acp.SupervisorConfig{
|
||||
SpawnTimeout: 5 * time.Second,
|
||||
KillGrace: 1 * time.Second,
|
||||
StderrBufferLines: 50,
|
||||
StderrTailBytes: 1000,
|
||||
EventMaxPayload: 65536,
|
||||
EventTruncateField: 4096,
|
||||
ShutdownGrace: 5 * time.Second,
|
||||
},
|
||||
slogDiscard(),
|
||||
)
|
||||
defer sup.ShutdownAll(context.Background())
|
||||
|
||||
// Test TurnBus subscriber records published events.
|
||||
bus := acp.NewTurnBus(slogDiscard())
|
||||
evCh := make(chan acp.TurnEvent, 8)
|
||||
bus.Subscribe("test", func(_ context.Context, ev acp.TurnEvent) { evCh <- ev })
|
||||
sup.SetTurnBus(bus)
|
||||
|
||||
wsStub := &stubWsService{wsRow: wsRow}
|
||||
paStub := &stubProjectAccess{pj: &project.Project{ID: pid, Slug: "p-turn"}}
|
||||
svc := acp.NewSessionService(
|
||||
repo, wsStub, nil, nil, paStub, nil, nil, sup,
|
||||
audit.NewPostgresRecorder(pool),
|
||||
acp.SessionServiceConfig{MaxActiveGlobal: 10, MaxActivePerUser: 5},
|
||||
nil,
|
||||
)
|
||||
permSvc := acp.NewPermissionService(repo, nil, 0, nil)
|
||||
sup.SetPermissionService(permSvc)
|
||||
permSvc.SetRelayLocator(sup)
|
||||
|
||||
prompt := "do the thing"
|
||||
branch := "main"
|
||||
sess, err := svc.Create(ctx, acp.Caller{UserID: uid}, acp.CreateSessionInput{
|
||||
WorkspaceID: wsid, AgentKindID: kind.ID, Branch: &branch, InitialPrompt: &prompt,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Poll for the turn to complete.
|
||||
var completedTurn *acp.Turn
|
||||
deadline := time.After(15 * time.Second)
|
||||
for completedTurn == nil {
|
||||
ts, _ := repo.ListTurnsBySession(ctx, sess.ID)
|
||||
for _, tn := range ts {
|
||||
if tn.Status == acp.TurnCompleted {
|
||||
completedTurn = tn
|
||||
}
|
||||
}
|
||||
if completedTurn != nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatalf("turn never completed; turns=%+v", ts)
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, completedTurn.StopReason)
|
||||
assert.Equal(t, "end_turn", *completedTurn.StopReason)
|
||||
assert.Equal(t, wantUpdates, completedTurn.UpdateCount)
|
||||
|
||||
got, _ := repo.GetSessionByID(ctx, sess.ID)
|
||||
require.NotNil(t, got.LastStopReason)
|
||||
assert.Equal(t, "end_turn", *got.LastStopReason)
|
||||
|
||||
// Collect bus events: expect a turn_completed and a session_idle.
|
||||
var sawCompleted, sawIdle bool
|
||||
evDeadline := time.After(3 * time.Second)
|
||||
for !(sawCompleted && sawIdle) {
|
||||
select {
|
||||
case ev := <-evCh:
|
||||
switch ev.Kind {
|
||||
case acp.TurnCompletedEvent:
|
||||
sawCompleted = true
|
||||
assert.Equal(t, acp.StopEndTurn, ev.StopReason)
|
||||
case acp.SessionIdleEvent:
|
||||
sawIdle = true
|
||||
}
|
||||
case <-evDeadline:
|
||||
t.Fatalf("did not receive both turn_completed and session_idle (completed=%v idle=%v)", sawCompleted, sawIdle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mustEncryptEnv marshals env to JSON and encrypts it (mirrors acp.encryptEnv,
|
||||
// which is unexported), so the supervisor can decrypt it back into the spawn env.
|
||||
func mustEncryptEnv(t *testing.T, enc *crypto.Encryptor, env map[string]string) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(env)
|
||||
require.NoError(t, err)
|
||||
out, err := enc.Encrypt(b)
|
||||
require.NoError(t, err)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user