tests/e2e/flows-api-lifecycle.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import {
user1Headers,
seedState,
resetState,
getTestState,
} from "./helpers/flows-api-fixtures";
test.beforeEach(async ({ request }) => {
await resetState(request);
});
test("POST /api/flows/:id/duplicate creates an inactive unlinked copy", async ({
request,
}) => {
await seedState(request, {
flows: [
{
id: "flow-1",
user_id: "user-1",
installation_id: 101,
name: "Original Flow",
description: "desc",
notes: "notes",
source_kind: "github",
status: "active",
draft_graph: {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
published_version_id: "version-1",
created_at: "2026-03-28T10:00:00.000Z",
updated_at: "2026-03-28T10:00:00.000Z",
},
],
});
const response = await request.post("/api/flows/flow-1/duplicate", {
headers: user1Headers,
});
expect(response.status()).toBe(201);
const payload = await response.json();
expect(payload.name).toBe("Original Flow Copy");
expect(payload.status).toBe("inactive");
expect(payload.published_version_id).toBeNull();
});
test("DELETE /api/flows/:id deletes the flow and leaves trigger-origin automations untouched", async ({
request,
}) => {
await seedState(request, {
flows: [
{
id: "flow-1",
user_id: "user-1",
installation_id: 101,
name: "Flow",
description: null,
notes: null,
source_kind: "github",
status: "active",
draft_graph: {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
published_version_id: "version-1",
created_at: "2026-03-28T10:00:00.000Z",
updated_at: "2026-03-28T10:00:00.000Z",
},
],
flowVersions: [
{
id: "version-1",
flow_id: "flow-1",
version_number: 1,
graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
created_at: "2026-03-28T10:00:00.000Z",
},
],
triggers: [
{
id: "trigger-1",
user_id: "user-1",
installation_id: 101,
agent_id: "agent-1",
event: "mention",
is_default: true,
enabled: true,
},
],
});
const response = await request.delete("/api/flows/flow-1", {
headers: user1Headers,
});
expect(response.ok()).toBeTruthy();
const state = await getTestState(request);
expect(state.flows).toHaveLength(0);
expect(state.triggers).toHaveLength(1);
});
test("DELETE /api/flows/:id fails cleanly without mutating trigger-origin automations", async ({
request,
}) => {
await seedState(request, {
flows: [
{
id: "flow-1",
user_id: "user-1",
installation_id: 101,
name: "Flow",
description: null,
notes: null,
source_kind: "github",
status: "active",
draft_graph: {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
published_version_id: "version-1",
created_at: "2026-03-28T10:00:00.000Z",
updated_at: "2026-03-28T10:00:00.000Z",
},
],
flowVersions: [
{
id: "version-1",
flow_id: "flow-1",
version_number: 1,
graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
created_at: "2026-03-28T10:00:00.000Z",
},
],
triggers: [
{
id: "trigger-1",
user_id: "user-1",
installation_id: 101,
agent_id: "agent-1",
event: "mention",
is_default: true,
enabled: true,
},
],
faults: {
failNextFlowDelete: "flow delete failed",
},
});
const response = await request.delete("/api/flows/flow-1", {
headers: user1Headers,
});
expect(response.status()).toBe(500);
expect(await response.json()).toMatchObject({
code: "FLOW_DELETE_SYNC_FAILED",
error: "flow delete failed",
});
const state = await getTestState(request);
expect(state.flows).toHaveLength(1);
expect(state.triggers).toHaveLength(1);
expect(state.triggers[0].id).toBe("trigger-1");
});
test("POST /api/flows/:id/assistant validates request and response graphs", async ({
request,
}) => {
await seedState(request, {
flows: [
{
id: "flow-1",
user_id: "user-1",
installation_id: 101,
name: "Flow",
description: null,
notes: null,
source_kind: "github",
status: "inactive",
draft_graph: {
nodes: [
{
id: "start",
type: "start",
position: { x: 0, y: 0 },
data: { label: "@mention", event: "mention", isDefault: true },
},
{
id: "agent-1",
type: "agent",
position: { x: 100, y: 0 },
data: {
label: "Agent",
agentId: "agent-1",
modelOverride: "minimax/minimax-m2.5",
},
},
{
id: "end",
type: "end",
position: { x: 200, y: 0 },
data: { label: "Done" },
},
],
edges: [
{ id: "e1", source: "start", target: "agent-1" },
{ id: "e2", source: "agent-1", target: "end" },
],
viewport: { x: 0, y: 0, zoom: 1 },
},
published_version_id: null,
created_at: "2026-03-28T10:00:00.000Z",
updated_at: "2026-03-28T10:00:00.000Z",
},
],
assistant: {
nextResult: {
summary: "Added a parallel reviewer",
graph: {
nodes: [
{
id: "start",
type: "start",
position: { x: 0, y: 0 },
data: { label: "@mention", event: "mention", isDefault: true },
},
{
id: "agent-1",
type: "agent",
position: { x: 100, y: 0 },
data: {
label: "Agent",
agentId: "agent-1",
modelOverride: "minimax/minimax-m2.5",
},
},
{
id: "end",
type: "end",
position: { x: 200, y: 0 },
data: { label: "Done" },
},
],
edges: [
{ id: "e1", source: "start", target: "agent-1" },
{ id: "e2", source: "agent-1", target: "end" },
],
viewport: { x: 0, y: 0, zoom: 1 },
},
},
},
});
const successResponse = await request.post("/api/flows/flow-1/assistant", {
headers: user1Headers,
data: { message: "Keep it simple" },
});
expect(successResponse.ok()).toBeTruthy();
expect(await successResponse.json()).toMatchObject({
summary: "Added a parallel reviewer",
});
const missingMessage = await request.post("/api/flows/flow-1/assistant", {
headers: user1Headers,
data: { message: "" },
});
expect(missingMessage.status()).toBe(400);
await seedState(request, {
flows: [
{
id: "flow-1",
user_id: "user-1",
installation_id: 101,
name: "Flow",
description: null,
notes: null,
source_kind: "github",
status: "inactive",
draft_graph: {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
published_version_id: null,
created_at: "2026-03-28T10:00:00.000Z",
updated_at: "2026-03-28T10:00:00.000Z",
},
],
assistant: {
nextResult: {
summary: "Broken graph",
graph: {
nodes: [
{
id: "start",
type: "start",
position: { x: 0, y: 0 },
data: { label: "@mention", event: "mention", isDefault: true },
},
],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
},
},
});
const invalidGraph = await request.post("/api/flows/flow-1/assistant", {
headers: user1Headers,
data: { message: "Break it" },
});
expect(invalidGraph.status()).toBe(400);
expect(await invalidGraph.json()).toMatchObject({
code: "FLOW_ASSISTANT_INVALID_GRAPH",
error: "Assistant produced an invalid flow graph",
});
});