Skip to content

Commit b6a8c25

Browse files
Merge pull request #1379 from oriontech-me/fix/issue-1376-mcp-starter-projects-cleanup
fix(mcp): delete the flow and the project this spec leaks every run (#1376)
2 parents b8a7481 + cd5492d commit b6a8c25

1 file changed

Lines changed: 89 additions & 1 deletion

File tree

tests/tests-automations/regression/mcp/server/mcp-server-starter-projects.spec.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { Page } from "@playwright/test";
22
import { expect, test } from "../../../../fixtures/fixtures";
33
import { awaitBootstrapTest } from "../../../../helpers/other/await-bootstrap-test";
4+
import { getAuthToken } from "../../../../helpers/auth/get-auth-token";
45
import { cleanOldFolders } from "../../../../helpers/filesystem/clean-old-folders";
56
import { createProjectThroughSidebar } from "../../../../helpers/flows/create-project-through-sidebar";
67
import { deleteProject } from "../../../../helpers/flows/delete-project";
8+
import { trackCreatedFlows } from "../../../../helpers/flows/track-created-flows";
9+
import { unmountEditorForCleanup } from "../../../../helpers/flows/unmount-editor-for-cleanup";
710
import { navigateSettingsPages } from "../../../../helpers/ui/go-to-settings";
811
import { openProjectOptions } from "../../../../helpers/ui/project-sidebar";
912

@@ -59,6 +62,87 @@ const removeLeftoverRenamedProject = async (page: Page) => {
5962
}
6063
};
6164

65+
// Ids of what a test created, deleted id-scoped in afterEach (#1376) — never a
66+
// global sweep, which wipes what other workers are building (#515).
67+
//
68+
// Measured on `1.12.0.dev20` over three consecutive runs of this file, counting
69+
// `GET /api/v1/flows/?get_all=true` and `GET /api/v1/projects/` around each:
70+
//
71+
// flows 34 -> 35 -> 36 -> 37 (+1 every run, unbounded)
72+
// projects 1 -> 2 -> 2 -> 2 (+1, then flat)
73+
//
74+
// The two halves fail differently, and the second is the one worth naming. Test
75+
// 2 opens the Basic Prompting template, which creates a flow nothing deletes —
76+
// that leak is monotonic and visible. Test 1 creates TWO projects and only ever
77+
// deletes the one it renames, and that leak reads as zero from run 2 onwards
78+
// only because `cleanOldFolders` at the top of test 1 sweeps the PREVIOUS run's
79+
// leftover. It is not absent, it is absorbed — and #1363 is the incident where
80+
// that absorption stopped working: with the sweep silently deleting nothing,
81+
// `New Project (N)` accumulated inside a single test's own retries.
82+
//
83+
// So the cleanup below is not redundant with `cleanOldFolders`. That helper
84+
// guards against OTHER runs' leftovers; this one stops the file from producing
85+
// them.
86+
const createdProjectIds = new Set<string>();
87+
88+
// Flows go through the SHARED tracker (#1108), not a hand-rolled listener. The
89+
// block that would be written here was copied into 51 spec files and drifted on
90+
// four axes; two of them decide whether this file's leak is actually fixed.
91+
// **One of the 51 settles its in-flight body reads** — the id lands a tick after
92+
// the `201`, so a teardown that snapshots immediately drops it and the flow leaks
93+
// anyway. And `cleanup` resolves the bearer itself and **never throws out of
94+
// teardown**: `getAuthToken` throws once its retry budget is spent (a backend
95+
// wedged at teardown, #1077), and a throw in an `afterEach` is a hook error that
96+
// fails an otherwise-green test — the opposite of what cleanup is for. It also
97+
// leaves the editor before deleting (#1288) and reports failures instead of
98+
// swallowing them (#1012).
99+
let flows: ReturnType<typeof trackCreatedFlows>;
100+
101+
test.beforeEach(({ page }) => {
102+
flows = trackCreatedFlows(page);
103+
});
104+
105+
test.afterEach(async ({ page, request }) => {
106+
await flows.cleanup(request);
107+
108+
const projectIds = [...createdProjectIds];
109+
createdProjectIds.clear();
110+
if (projectIds.length === 0) return;
111+
112+
// The tracker unmounts only when it has flows to delete, and test 1 has none
113+
// while still holding projects — so the navigation is done here too. Deleting a
114+
// project under a mounted home view makes it refetch a folder that is already
115+
// gone, which the fixture logs as `🚨 Backend Error` (#1023). Idempotent: when
116+
// the tracker already navigated, this is a second `about:blank`.
117+
await unmountEditorForCleanup(page);
118+
119+
// Same contract as the tracker's own bearer, for the same reason (#1086/#1077):
120+
// caught so a wedged backend cannot turn teardown into a hook error, and NAMED
121+
// rather than degraded silently to an empty token — otherwise the 401s below
122+
// would read as the projects' fault.
123+
let options: { headers: Record<string, string> } | undefined;
124+
try {
125+
const bearer = await getAuthToken(request);
126+
options = bearer ? { headers: { Authorization: bearer } } : undefined;
127+
} catch (error) {
128+
console.warn(
129+
`⚠️ cleanup: no auth token — the project deletes below run on the browser ` +
130+
`session alone, so a 401 here is THAT and not the project (#1086/#1077): ${error}`,
131+
);
132+
}
133+
134+
// `deleteProject` treats 404 — the project test 1 already deleted through the
135+
// UI — as the desired end state, and retries the 500 the endpoint answers
136+
// under contention (#965). Reported, never swallowed: a failed cleanup must not
137+
// fail the hook and mask the assertion that already ran, but it must not be
138+
// silent either (#1012).
139+
for (const id of projectIds) {
140+
await deleteProject(request, id, options).catch((error) => {
141+
console.warn(`⚠️ Orphan project left behind (${id}): ${error}`);
142+
});
143+
}
144+
});
145+
62146
test(
63147
"user must be able to see starter projects for mcp servers",
64148
{ tag: ["@stable", "@release", "@workspace", "@components", "@mcp"] },
@@ -85,7 +169,11 @@ test(
85169
// project. Both are needed to address the sidebar entry and its kebab: the
86170
// nightly keys those testids on the project id, 1.11.x on its name (#1363).
87171
const firstProject = await createProjectThroughSidebar(page);
88-
await createProjectThroughSidebar(page);
172+
const secondProject = await createProjectThroughSidebar(page);
173+
// Both, not just the renamed one: the second is the project this file used
174+
// to leave behind on every run (#1376).
175+
createdProjectIds.add(firstProject.id);
176+
createdProjectIds.add(secondProject.id);
89177

90178
await navigateSettingsPages(page, "Settings", "MCP Servers");
91179

0 commit comments

Comments
 (0)