fix: hide start agent button during preparation and fix auto-start race condition on step move (#516)
This commit is contained in:
@@ -548,6 +548,19 @@ func (s *Service) autoStartStepPrompt(
|
||||
// Record a user message so the auto-start prompt is visible in chat history.
|
||||
s.recordAutoStartMessage(ctx, taskID, sessionID, prompt, planMode)
|
||||
|
||||
// If the session is in CREATED state, the agent was never started (e.g. workspace-only
|
||||
// preparation from a blocked auto-start). PromptTask will reject CREATED sessions,
|
||||
// so use StartCreatedSession which properly launches the agent on the prepared workspace.
|
||||
// Pass skipMessageRecord=true since recordAutoStartMessage above already recorded it.
|
||||
if session.State == models.TaskSessionStateCreated {
|
||||
s.logger.Info("auto-start: session is CREATED, launching agent via StartCreatedSession",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("session_id", sessionID),
|
||||
zap.String("step_name", stepName))
|
||||
_, err := s.StartCreatedSession(ctx, taskID, sessionID, session.AgentProfileID, prompt, true, planMode, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
const maxRetryAttempts = 5
|
||||
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
||||
_, err := s.PromptTask(ctx, taskID, sessionID, prompt, "", planMode, nil)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Message, TaskSessionState, TaskState } from "@/lib/types/http";
|
||||
import type { ToolCallMetadata } from "@/components/task/chat/types";
|
||||
import { launchSession } from "@/lib/services/session-launch-service";
|
||||
import { buildStartCreatedRequest } from "@/lib/services/session-launch-helpers";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { ChatMessage } from "@/components/task/chat/messages/chat-message";
|
||||
import { PermissionRequestMessage } from "@/components/task/chat/messages/permission-request-message";
|
||||
import { StatusMessage } from "@/components/task/chat/messages/status-message";
|
||||
@@ -39,6 +40,10 @@ type AdapterContext = {
|
||||
|
||||
function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; sessionId: string }) {
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const prepareStatus = useAppStore(
|
||||
(state) => state.prepareProgress.bySessionId[sessionId]?.status ?? null,
|
||||
);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
setIsStarting(true);
|
||||
try {
|
||||
@@ -51,6 +56,9 @@ function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; ses
|
||||
}
|
||||
}, [taskId, sessionId]);
|
||||
|
||||
// Hide while environment is being prepared
|
||||
if (prepareStatus === "preparing") return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end mt-1.5">
|
||||
<Button
|
||||
@@ -59,6 +67,7 @@ function TaskDescriptionStartButton({ taskId, sessionId }: { taskId: string; ses
|
||||
className="cursor-pointer gap-1.5"
|
||||
onClick={handleStart}
|
||||
disabled={isStarting}
|
||||
data-testid="task-description-start-button"
|
||||
>
|
||||
<IconPlayerPlay className="h-3.5 w-3.5" />
|
||||
{isStarting ? "Starting…" : "Start agent"}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { KanbanPage } from "../../pages/kanban-page";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
test.describe("PR watcher auto-start on move", () => {
|
||||
/**
|
||||
* Race condition: When a task created from a PR watcher sits on a step
|
||||
* WITHOUT auto_start_agent, the user opens the task (creating a CREATED
|
||||
* session with workspace-only preparation), then moves the task to a step
|
||||
* WITH auto_start_agent on the kanban board.
|
||||
*
|
||||
* BUG: The on_enter auto_start_agent action calls PromptTask which rejects
|
||||
* CREATED sessions, causing the prompt to be queued instead of starting
|
||||
* the agent. The queued prompt is never consumed because no agent is running.
|
||||
*
|
||||
* EXPECTED: The agent starts immediately on the prepared workspace.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create workflow: Waiting (no auto_start) → Review (auto_start) → Done
|
||||
* 2. Create task as PR watcher would (metadata.agent_profile_id)
|
||||
* 3. Open task → session auto-prepares (CREATED, workspace-only)
|
||||
* 4. Go back to kanban
|
||||
* 5. Move task from Waiting to Review via API
|
||||
* 6. Open the task
|
||||
* 7. Assert: agent starts and completes (NOT queued)
|
||||
*/
|
||||
test("starts agent immediately when moving task with CREATED session to auto-start step", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
// --- Create workflow: Waiting → Review (auto_start) → Done ---
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "PR Move Workflow");
|
||||
|
||||
const waitingStep = await apiClient.createWorkflowStep(workflow.id, "Waiting", 0);
|
||||
const reviewStep = await apiClient.createWorkflowStep(workflow.id, "Review", 1);
|
||||
const doneStep = await apiClient.createWorkflowStep(workflow.id, "Done", 2);
|
||||
|
||||
// Configure Review step: auto_start + move to Done on turn complete
|
||||
await apiClient.updateWorkflowStep(reviewStep.id, {
|
||||
prompt: '/e2e:message("review complete")\n{{task_prompt}}',
|
||||
events: {
|
||||
on_enter: [{ type: "auto_start_agent" }],
|
||||
on_turn_complete: [{ type: "move_to_step", config: { step_id: doneStep.id } }],
|
||||
},
|
||||
});
|
||||
|
||||
await apiClient.saveUserSettings({
|
||||
workspace_id: seedData.workspaceId,
|
||||
workflow_filter_id: workflow.id,
|
||||
enable_preview_on_click: false,
|
||||
});
|
||||
|
||||
// --- Create task as PR watcher would ---
|
||||
const task = await apiClient.createTask(seedData.workspaceId, "PR #55: Refactor auth", {
|
||||
description: "Review the auth refactoring changes",
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: waitingStep.id,
|
||||
repositories: [
|
||||
{
|
||||
repository_id: seedData.repositoryId,
|
||||
base_branch: "main",
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
agent_profile_id: seedData.agentProfileId,
|
||||
pr_number: 55,
|
||||
pr_branch: "refactor/auth",
|
||||
pr_repo: "testorg/testrepo",
|
||||
pr_author: "contributor",
|
||||
},
|
||||
});
|
||||
|
||||
// --- Navigate to kanban and verify task in Waiting ---
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
|
||||
const card = kanban.taskCardInColumn("PR #55: Refactor auth", waitingStep.id);
|
||||
await expect(card).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// --- Open task → creates CREATED session (workspace-only preparation) ---
|
||||
await card.click();
|
||||
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
|
||||
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
// Wait for the session to be created (auto-prepare fires from useAutoStartSession)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
return sessions.length > 0 && sessions[0]?.state === "CREATED";
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for session to be created in CREATED state" },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// --- Go back to kanban ---
|
||||
await kanban.goto();
|
||||
await expect(kanban.taskCardByTitle("PR #55: Refactor auth")).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// --- Move task from Waiting to Review (triggers on_enter: auto_start_agent) ---
|
||||
await apiClient.moveTask(task.id, workflow.id, reviewStep.id);
|
||||
|
||||
// --- Task should move through Review to Done after agent completes ---
|
||||
// If the bug is present, the prompt would be queued and the agent never starts,
|
||||
// so the task would stay in Review indefinitely.
|
||||
const cardInDone = kanban.taskCardInColumn("PR #55: Refactor auth", doneStep.id);
|
||||
await expect(cardInDone).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
// --- Navigate to session and verify agent completed (not stuck with queued prompt) ---
|
||||
await cardInDone.click();
|
||||
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
|
||||
|
||||
const sessionAfter = new SessionPage(testPage);
|
||||
await sessionAfter.waitForLoad();
|
||||
|
||||
// The agent should have completed — idle input visible
|
||||
await expect(sessionAfter.idleInput()).toBeVisible({ timeout: 30_000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { KanbanPage } from "../../pages/kanban-page";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
test.describe("PR watcher start button timing", () => {
|
||||
/**
|
||||
* When a task is created from a PR watcher on a workflow step WITHOUT
|
||||
* auto_start_agent, opening the task triggers environment preparation
|
||||
* (workspace-only, no agent). The "Start agent" button should appear
|
||||
* after preparation completes and clicking it should start the agent.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create workflow step without auto_start_agent
|
||||
* 2. Create a task as the PR watcher would (with metadata.agent_profile_id)
|
||||
* 3. Navigate to task — auto-start fires, backend downgrades to prepare
|
||||
* 4. Wait for CREATED session to exist (env preparing)
|
||||
* 5. Reload page so SSR picks up the session and renders it
|
||||
* 6. Assert: "Start agent" button becomes visible
|
||||
* 7. Click the button — agent starts and completes
|
||||
*/
|
||||
test("start button appears after environment preparation and starts agent", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
// --- Create a workflow step WITHOUT auto_start_agent ---
|
||||
const reviewStep = await apiClient.createWorkflowStep(seedData.workflowId, "Review", 0);
|
||||
|
||||
await apiClient.saveUserSettings({
|
||||
workspace_id: seedData.workspaceId,
|
||||
workflow_filter_id: seedData.workflowId,
|
||||
enable_preview_on_click: false,
|
||||
});
|
||||
|
||||
// --- Create a task as the PR watcher would ---
|
||||
const task = await apiClient.createTask(seedData.workspaceId, "PR #42: Add feature", {
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: reviewStep.id,
|
||||
repositories: [
|
||||
{
|
||||
repository_id: seedData.repositoryId,
|
||||
base_branch: "main",
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
agent_profile_id: seedData.agentProfileId,
|
||||
pr_number: 42,
|
||||
pr_branch: "feature/add-feature",
|
||||
pr_repo: "testorg/testrepo",
|
||||
pr_author: "contributor",
|
||||
},
|
||||
});
|
||||
|
||||
// --- Navigate to kanban and click the task ---
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
|
||||
const card = kanban.taskCardByTitle("PR #42: Add feature");
|
||||
await expect(card).toBeVisible({ timeout: 15_000 });
|
||||
await card.click();
|
||||
|
||||
// Wait for navigation to session view
|
||||
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
|
||||
|
||||
// --- Wait for auto-start to create a CREATED session ---
|
||||
// useAutoStartSession fires with autoStart=true, backend downgrades to
|
||||
// prepare (step lacks auto_start_agent). Session stays in CREATED state.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
return sessions.length > 0;
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for session to be created" },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// --- Reload so SSR picks up the session and renders the chat ---
|
||||
// After auto-start creates the CREATED session, the frontend doesn't
|
||||
// automatically set it as the active session. Reloading ensures SSR
|
||||
// resolves the session and the chat panel renders with the task description.
|
||||
await testPage.reload();
|
||||
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
// --- Assert: "Start agent" button becomes visible after prep completes ---
|
||||
const startButton = session.chat.getByTestId("task-description-start-button");
|
||||
await expect(startButton).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
// --- Click the button to start the agent ---
|
||||
await startButton.click();
|
||||
|
||||
// Agent should start and eventually complete (mock agent with simple message)
|
||||
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user