fix: user workflow deadlock (#677)
This commit is contained in:
@@ -71,6 +71,39 @@ function createKeyEventHandler(options: TerminalKeyHandlerOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer WebGL addon loading to the next animation frame so the initial
|
||||
* synchronous work (Terminal + FitAddon + open) stays within the browser's
|
||||
* frame budget.
|
||||
*
|
||||
* Skips WebGL on Firefox: its canvas fingerprinting protection silently
|
||||
* poisons readback data, corrupting the glyph texture atlas.
|
||||
*/
|
||||
function deferWebGLAddon(refs: Pick<TerminalInitOptions, "xtermRef" | "webglAddonRef">) {
|
||||
const isFirefox = typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent);
|
||||
if (isFirefox) {
|
||||
log("Skipping WebGL addon on Firefox (canvas fingerprinting protection)");
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const term = refs.xtermRef.current;
|
||||
if (!term || refs.webglAddonRef.current) return;
|
||||
try {
|
||||
const webglAddon = new WebglAddon();
|
||||
webglAddon.onContextLoss(() => {
|
||||
log("WebGL context lost");
|
||||
webglAddon.dispose();
|
||||
refs.webglAddonRef.current = null;
|
||||
});
|
||||
term.loadAddon(webglAddon);
|
||||
refs.webglAddonRef.current = webglAddon;
|
||||
log("WebGL addon loaded");
|
||||
} catch (e) {
|
||||
log("WebGL failed, using canvas:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initTerminalInstance(
|
||||
termContainer: HTMLDivElement,
|
||||
refs: Pick<
|
||||
@@ -121,26 +154,7 @@ function initTerminalInstance(
|
||||
}
|
||||
refs.xtermRef.current = terminal;
|
||||
refs.fitAddonRef.current = fitAddon;
|
||||
// Defer WebGL addon loading to the next animation frame so the initial
|
||||
// synchronous work (Terminal + FitAddon + open) stays within the browser's
|
||||
// frame budget and avoids "Violation: 'setTimeout' handler took Xms" warnings.
|
||||
requestAnimationFrame(() => {
|
||||
const term = refs.xtermRef.current;
|
||||
if (!term || refs.webglAddonRef.current) return;
|
||||
try {
|
||||
const webglAddon = new WebglAddon();
|
||||
webglAddon.onContextLoss(() => {
|
||||
log("WebGL context lost");
|
||||
webglAddon.dispose();
|
||||
refs.webglAddonRef.current = null;
|
||||
});
|
||||
term.loadAddon(webglAddon);
|
||||
refs.webglAddonRef.current = webglAddon;
|
||||
log("WebGL addon loaded");
|
||||
} catch (e) {
|
||||
log("WebGL failed, using canvas:", e);
|
||||
}
|
||||
});
|
||||
deferWebGLAddon(refs);
|
||||
exposeBufferReader(termContainer, terminal);
|
||||
const handleResize = () => {
|
||||
const rect = termContainer.getBoundingClientRect();
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { backendFixture, type BackendContext } from "./backend";
|
||||
import { ApiClient } from "../helpers/api-client";
|
||||
import { PrAssetCapture } from "../helpers/pr-asset-capture";
|
||||
import { makeGitEnv } from "../helpers/git-helper";
|
||||
import type { WorkflowStep } from "../../lib/types/http";
|
||||
|
||||
export type SeedData = {
|
||||
@@ -48,14 +49,7 @@ export const test = backendFixture.extend<
|
||||
// This ensures discoveryRoots() allows the path for branch listing.
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
fs.mkdirSync(repoDir, { recursive: true });
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
};
|
||||
const gitEnv = makeGitEnv(backend.tmpDir);
|
||||
execSync("git init -b main", { cwd: repoDir, env: gitEnv });
|
||||
execSync('git commit --allow-empty -m "init"', { cwd: repoDir, env: gitEnv });
|
||||
const repo = await apiClient.createRepository(workspace.id, repoDir);
|
||||
|
||||
@@ -62,9 +62,22 @@ export class GitHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip GIT_CONFIG_* environment variables that can inject global git hooks
|
||||
* into fresh git repos, breaking E2E test setup.
|
||||
*/
|
||||
function stripGitConfigOverrides(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const clean: NodeJS.ProcessEnv = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (/^GIT_CONFIG_(KEY|VALUE|COUNT|PARAMETERS|GLOBAL|SYSTEM)/i.test(key)) continue;
|
||||
clean[key] = value;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
export function makeGitEnv(tmpDir: string): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
...stripGitConfigOverrides(process.env),
|
||||
HOME: tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
import { makeGitEnv } from "../../helpers/git-helper";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
@@ -47,14 +48,7 @@ test.describe("Changes panel focus behavior", () => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
};
|
||||
const gitEnv = makeGitEnv(backend.tmpDir);
|
||||
const git = new GitHelper(repoDir, gitEnv);
|
||||
|
||||
// Create a task and wait for the agent to be ready
|
||||
@@ -118,14 +112,7 @@ test.describe("Changes panel focus behavior", () => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
};
|
||||
const gitEnv = makeGitEnv(backend.tmpDir);
|
||||
const git = new GitHelper(repoDir, gitEnv);
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import type { ApiClient } from "../../helpers/api-client";
|
||||
import { makeGitEnv } from "../../helpers/git-helper";
|
||||
import { KanbanPage } from "../../pages/kanban-page";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
import type { Page } from "@playwright/test";
|
||||
@@ -114,14 +115,7 @@ test.describe("VCS split button context", () => {
|
||||
|
||||
// Set up git with a remote so we can get "ahead" status
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
};
|
||||
const gitEnv = makeGitEnv(backend.tmpDir);
|
||||
const git = new GitHelper(repoDir, gitEnv);
|
||||
git.setupRemote(backend.tmpDir);
|
||||
|
||||
@@ -179,14 +173,7 @@ test.describe("VCS split button context", () => {
|
||||
|
||||
// Set up git with a remote
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
};
|
||||
const gitEnv = makeGitEnv(backend.tmpDir);
|
||||
const git = new GitHelper(repoDir, gitEnv);
|
||||
|
||||
// Only set up remote if not already configured
|
||||
@@ -251,14 +238,7 @@ test.describe("VCS split button context", () => {
|
||||
|
||||
// Create an uncommitted file
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const git = new GitHelper(repoDir, {
|
||||
...process.env,
|
||||
HOME: backend.tmpDir,
|
||||
GIT_AUTHOR_NAME: "E2E Test",
|
||||
GIT_AUTHOR_EMAIL: "e2e@test.local",
|
||||
GIT_COMMITTER_NAME: "E2E Test",
|
||||
GIT_COMMITTER_EMAIL: "e2e@test.local",
|
||||
});
|
||||
const git = new GitHelper(repoDir, makeGitEnv(backend.tmpDir));
|
||||
git.createFile("uncommitted.txt", "uncommitted content");
|
||||
|
||||
// Even with open PR, uncommitted files → "Commit" takes priority
|
||||
|
||||
@@ -292,12 +292,19 @@ test.describe("Workflow agent profile switching", () => {
|
||||
// Move to Step2 — should auto-launch agent despite no auto_start_agent
|
||||
await apiClient.moveTask(task.id, workflow.id, step2.id);
|
||||
|
||||
// Poll for a session with profileB that has completed at least one turn
|
||||
const finalSessions = await pollSessions(apiClient, task.id, 2, 30_000);
|
||||
const step2Session = finalSessions.find((s) => s.agent_profile_id === profileB.id);
|
||||
expect(step2Session).toBeDefined();
|
||||
// The session should have progressed past CREATED (agent was launched)
|
||||
expect(step2Session!.state).not.toBe("CREATED");
|
||||
// Poll until a session with profileB progresses past CREATED (agent launched).
|
||||
// The agent launch is async (goroutine), so we need to wait for state progression.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
const step2Session = sessions.find((s) => s.agent_profile_id === profileB.id);
|
||||
if (!step2Session || step2Session.state === "CREATED") return "CREATED";
|
||||
return step2Session.state;
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for step2 agent to launch past CREATED" },
|
||||
)
|
||||
.not.toBe("CREATED");
|
||||
});
|
||||
|
||||
test("new session inherits task_environment_id from old session", async ({
|
||||
@@ -357,6 +364,315 @@ test.describe("Workflow agent profile switching", () => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies the StartCreatedSession fix: when a task is created in a step with an
|
||||
* agent profile override, the session tab shows the step's agent profile name
|
||||
* (not the workspace default).
|
||||
*/
|
||||
test("initial task creation shows step's agent profile in session tab", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const { profileA } = await createProfiles(apiClient);
|
||||
|
||||
// Create workflow with Step1 (profileA, auto_start, is_start)
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Initial Profile Tab");
|
||||
const step1 = await apiClient.createWorkflowStep(workflow.id, "Step1", 0, {
|
||||
is_start_step: true,
|
||||
});
|
||||
await apiClient.createWorkflowStep(workflow.id, "Done", 1);
|
||||
|
||||
await apiClient.updateWorkflowStep(step1.id, {
|
||||
agent_profile_id: profileA.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
|
||||
// Create task — should use Step1's profileA, not the workspace default
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Initial Profile Task",
|
||||
profileA.id,
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: step1.id,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
// Navigate to task
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.chat).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The session tab should show "Profile A" (the step's agent profile name)
|
||||
const sessionTab = testPage.locator('[data-testid^="session-tab-"]').first();
|
||||
await expect(sessionTab).toBeVisible({ timeout: 30_000 });
|
||||
await expect(sessionTab).toContainText("Profile A", { timeout: 10_000 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies the switchSessionForStep WS event fix: when a task is manually moved
|
||||
* to a step with a different agent profile, the new session tab becomes the
|
||||
* active dockview tab.
|
||||
*/
|
||||
test("manual step move switches active session tab to new agent", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const { profileA, profileB } = await createProfiles(apiClient);
|
||||
|
||||
// Step1 (profileA, auto_start, is_start) → Step2 (profileB, auto_start)
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Active Tab Switch");
|
||||
const step1 = await apiClient.createWorkflowStep(workflow.id, "Step1", 0, {
|
||||
is_start_step: true,
|
||||
});
|
||||
const step2 = await apiClient.createWorkflowStep(workflow.id, "Step2", 1);
|
||||
await apiClient.createWorkflowStep(workflow.id, "Done", 2);
|
||||
|
||||
await apiClient.updateWorkflowStep(step1.id, {
|
||||
agent_profile_id: profileA.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
await apiClient.updateWorkflowStep(step2.id, {
|
||||
agent_profile_id: profileB.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Tab Switch Task",
|
||||
profileA.id,
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: step1.id,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
// Navigate and wait for first session
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.chat).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Wait for Profile A tab to appear and be active
|
||||
const profileATab = session.sessionTabByText("Profile A");
|
||||
await expect(profileATab).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Wait for agent to be ready (WAITING_FOR_INPUT)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
return sessions.some((s) => s.state === "WAITING_FOR_INPUT");
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for agent to be ready" },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// Move to Step2 — triggers new session with profileB
|
||||
await apiClient.moveTask(task.id, workflow.id, step2.id);
|
||||
|
||||
// The new "Profile B" tab should appear and become the active dockview tab
|
||||
const activeProfileBTab = testPage.locator(
|
||||
'.dv-tab.dv-active-tab [data-testid^="session-tab-"]',
|
||||
);
|
||||
await expect(activeProfileBTab).toContainText("Profile B", { timeout: 30_000 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that an on_turn_complete cascade that switches agent profiles
|
||||
* also switches the active dockview session tab.
|
||||
*/
|
||||
test("on_turn_complete cascade switches active session tab to new agent", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const { profileA, profileB } = await createProfiles(apiClient);
|
||||
|
||||
// Step1 (profileA, auto_start, move_to_next) → Step2 (profileB, auto_start)
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Cascade Tab Switch");
|
||||
const inbox = await apiClient.createWorkflowStep(workflow.id, "Inbox", 0);
|
||||
const step1 = await apiClient.createWorkflowStep(workflow.id, "Step1", 1);
|
||||
const step2 = await apiClient.createWorkflowStep(workflow.id, "Step2", 2);
|
||||
await apiClient.createWorkflowStep(workflow.id, "Done", 3);
|
||||
|
||||
await apiClient.updateWorkflowStep(step1.id, {
|
||||
agent_profile_id: profileA.id,
|
||||
prompt: 'e2e:delay(1000)\ne2e:message("step1 done")',
|
||||
events: {
|
||||
on_enter: [{ type: "auto_start_agent" }],
|
||||
on_turn_complete: [{ type: "move_to_next" }],
|
||||
},
|
||||
});
|
||||
await apiClient.updateWorkflowStep(step2.id, {
|
||||
agent_profile_id: profileB.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
|
||||
const task = await apiClient.createTask(seedData.workspaceId, "Cascade Tab Task", {
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: inbox.id,
|
||||
agent_profile_id: profileA.id,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
});
|
||||
|
||||
// Navigate to task page first so WS is connected
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.chat).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Move to Step1 → auto_start with profileA → on_turn_complete → Step2 (profileB)
|
||||
await apiClient.moveTask(task.id, workflow.id, step1.id);
|
||||
|
||||
// After the cascade, the active tab should be "Profile B" (the final step's agent)
|
||||
const activeProfileBTab = testPage.locator(
|
||||
'.dv-tab.dv-active-tab [data-testid^="session-tab-"]',
|
||||
);
|
||||
await expect(activeProfileBTab).toContainText("Profile B", { timeout: 45_000 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that after a manual step move, the new session tab shows
|
||||
* the primary star icon (★) without needing a page reload.
|
||||
* Covers the fix: switchSessionForStep uses SetPrimarySession (with WS
|
||||
* broadcast) instead of the raw repo.SetSessionPrimary.
|
||||
*/
|
||||
test("primary star icon appears on new session tab after step move", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const { profileA, profileB } = await createProfiles(apiClient);
|
||||
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Primary Star Test");
|
||||
const step1 = await apiClient.createWorkflowStep(workflow.id, "Step1", 0, {
|
||||
is_start_step: true,
|
||||
});
|
||||
const step2 = await apiClient.createWorkflowStep(workflow.id, "Step2", 1);
|
||||
await apiClient.createWorkflowStep(workflow.id, "Done", 2);
|
||||
|
||||
await apiClient.updateWorkflowStep(step1.id, {
|
||||
agent_profile_id: profileA.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
await apiClient.updateWorkflowStep(step2.id, {
|
||||
agent_profile_id: profileB.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Primary Star Task",
|
||||
profileA.id,
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: step1.id,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.chat).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Wait for Profile A session to be ready
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
return sessions.some((s) => s.state === "WAITING_FOR_INPUT");
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for agent to be ready" },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// Move to Step2
|
||||
await apiClient.moveTask(task.id, workflow.id, step2.id);
|
||||
|
||||
// The Profile B tab should become active
|
||||
const activeTab = testPage.locator('.dv-tab.dv-active-tab [data-testid^="session-tab-"]');
|
||||
await expect(activeTab).toContainText("Profile B", { timeout: 30_000 });
|
||||
|
||||
// The star icon should be visible inside the active tab (no reload needed)
|
||||
const starIcon = activeTab.locator("svg.tabler-icon-star");
|
||||
await expect(starIcon).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that moving to a step with NO agent_profile override reverts
|
||||
* to the task's original (default) agent profile from task metadata.
|
||||
* Covers the fix: maybySwitchSessionForProfile falls back to
|
||||
* task.Metadata["agent_profile_id"] when the step has no override.
|
||||
*/
|
||||
test("moving to step without agent override reverts to task default agent", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const { profileA, profileB } = await createProfiles(apiClient);
|
||||
|
||||
// Step1 (profileB, auto_start) → Step2 (NO override, auto_start)
|
||||
// Task created with profileA → Step1 overrides to profileB → Step2 should revert to profileA
|
||||
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Revert Agent Test");
|
||||
const step1 = await apiClient.createWorkflowStep(workflow.id, "Step1", 0, {
|
||||
is_start_step: true,
|
||||
});
|
||||
const step2 = await apiClient.createWorkflowStep(workflow.id, "Step2", 1);
|
||||
await apiClient.createWorkflowStep(workflow.id, "Done", 2);
|
||||
|
||||
await apiClient.updateWorkflowStep(step1.id, {
|
||||
agent_profile_id: profileB.id,
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
// Step2 has NO agent_profile_id — should revert to task default (profileA)
|
||||
await apiClient.updateWorkflowStep(step2.id, {
|
||||
events: { on_enter: [{ type: "auto_start_agent" }] },
|
||||
});
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Revert Agent Task",
|
||||
profileA.id,
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
workflow_step_id: step1.id,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.chat).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Wait for Profile B session (Step1 override) to be ready
|
||||
const profileBTab = session.sessionTabByText("Profile B");
|
||||
await expect(profileBTab).toBeVisible({ timeout: 30_000 });
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { sessions } = await apiClient.listTaskSessions(task.id);
|
||||
return sessions.some((s) => s.state === "WAITING_FOR_INPUT");
|
||||
},
|
||||
{ timeout: 30_000, message: "Waiting for agent to be ready" },
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// Move to Step2 (no override) — should revert to profileA
|
||||
await apiClient.moveTask(task.id, workflow.id, step2.id);
|
||||
|
||||
// The active tab should show Profile A (the task's default agent)
|
||||
const activeTab = testPage.locator('.dv-tab.dv-active-tab [data-testid^="session-tab-"]');
|
||||
await expect(activeTab).toContainText("Profile A", { timeout: 30_000 });
|
||||
});
|
||||
|
||||
test("reset context checkbox is disabled when step has agent profile override", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
Reference in New Issue
Block a user