Skip to content

Commit 084e69b

Browse files
Merge pull request #1339 from oriontech-me/fix/issue-1336-auto-save-off-reopen-by-id
fix(flow-functionality): re-open the flow under test by id, not by name (#1336)
2 parents a4f47c5 + 2206b34 commit 084e69b

2 files changed

Lines changed: 146 additions & 34 deletions

File tree

docs/flow-functionality/auto-save-off.md

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Manual Save With Auto-Save Disabled — §12.2 View and Edit Flow
22

3-
**Last validated:** Langflow 1.11.x
3+
**Last validated:** Langflow 1.12.x
44

55
---
66

@@ -43,25 +43,34 @@ across a full exit/re-open. `@components` — drives a canvas component.
4343
Setup: mock `/api/v1/config` to `auto_saving: false`, bootstrap the app
4444
(`awaitBootstrapTest`), open a blank flow. Every flow this page creates is
4545
captured from its `POST /api/v1/flows → 201` response and deleted id-scoped in
46-
`afterEach`.
46+
`afterEach`. The **id of the flow under test** is read from the URL right after
47+
the blank-flow navigation (never before it — the bootstrap parks the page on a
48+
placeholder flow Langflow then deletes) and cross-checked against the ids this
49+
page created; every re-open below is pinned to that id.
50+
51+
Every exit asserts the editor was actually left (the URL leaves `/flow/…`)
52+
before anything else runs.
4753

4854
1. Add a **Chat Input** component to the canvas (sidebar search → hover entry →
4955
`add-component-button-chat-input`)
5056
2. Assert the on-canvas **`save-flow-button`** is enabled (auto-save off ⇒ manual
5157
save is available)
5258
3. Leave via the back button (`icon-ChevronLeft`); the unsaved-changes dialog
53-
("Unsaved changes will be permanently lost.") appears — click **Exit Anyway**
54-
4. Re-open the flow (via the flow card's open button); assert the canvas has
55-
**0** nodes (`div-generic-node` count = 0) — the edit was discarded
59+
("Unsaved changes will be permanently lost.") appears — click **Exit Anyway**;
60+
assert the editor was left
61+
4. Re-open the flow **by id** (`/flow/<id>`, waiting for its
62+
`GET /api/v1/flows/<id>` to resolve and the canvas to mount); assert the
63+
canvas has **0** nodes (`div-generic-node` count = 0) — the edit was discarded
5664
5. Add the Chat Input component again (hover the sidebar entry →
5765
`add-component-button-chat-input`)
58-
6. Leave via the back button; click **Save And Exit**
66+
6. Leave via the back button; click **Save And Exit**; assert the editor was left
5967
7. Re-open the flow; assert **`title-Chat Input`** is visible — the edit persisted
6068
8. Add a **Chat Output** component (hover the sidebar entry →
6169
`add-component-button-chat-output`), click **`save-flow-button`** (the
6270
on-canvas manual save), leave via the back button. The exit-guard dialog is
6371
timing-dependent here — if the manual save settled, the exit is clean;
64-
otherwise **Save And Exit** appears and is clicked. Either path persists.
72+
otherwise **Save And Exit** appears and is clicked. Either path persists, and
73+
either way the editor must be left
6574
9. Re-open the flow; assert both `title-Chat Input` and `title-Chat Output` are
6675
visible and `div-generic-node` count = **2**
6776

@@ -98,9 +107,8 @@ fails if either save did not persist (see Notes on the hardening).
98107
search. Adds use the draggable wrapper hover → add button (the sidebar row is
99108
briefly `pointer-events-none`; dragging it is unreliable).
100109
- `data-testid="title-Chat Input"` / `div-generic-node` — node presence on canvas.
101-
- `data-testid="list-card"` / `flow-name-div` / `list-card-open-button`
102-
re-open a flow from the list (the `/flows` a11y refactor made `flow-name-div`
103-
`pointer-events-none`; open via the card overlay button — Langflow #13891).
110+
- `GET /api/v1/flows/{id}` and the `/flow/{id}` route — the re-open path (see the
111+
#1336 note below for why this is not the flows-list card).
104112
- No API key — the Chat Input / Chat Output components are added to the graph,
105113
never executed.
106114

@@ -116,6 +124,33 @@ fails if either save did not persist (see Notes on the hardening).
116124

117125
## Notes *(optional)*
118126

127+
- **#1336 (the re-open opened the WRONG flow).** Recurrent flake on the
128+
2026-07-22 and 2026-08-06 dailies: `locator.click: Timeout 45000ms exceeded`
129+
re-opening the just-created flow's card. Not a product regression, and not a
130+
tight wait either — the spec was **driving another worker's flow**. It clicked
131+
the first `list-card` whose name contained "New Flow", and Langflow names every
132+
blank flow "New Flow"/"New Flow (N)", so under `fullyParallel` the list holds
133+
one per worker. Proved on nightly 1.12.0.dev18 by logging the page's own
134+
network: the page created ids `8e767306` and `ee8e0ab9`, and the re-open landed
135+
on `164b3c19` — an id it never created. When that flow's real owner ran its
136+
id-scoped cleanup, the save `PATCH /api/v1/flows/{id}` came back **404**, so the
137+
editor never navigated back to the list and the next re-open burned its 45 s on
138+
a card that had no reason to exist. That is also the CI artifact's state: the
139+
failure screenshot is the *canvas*, not the list, with the flow saved and the
140+
same two 404s in the advisory log. Reproduced **3/8 at `--workers=4`**;
141+
**8/8 green** after the fix under the identical burst.
142+
Two further findings shaped the fix. The card cannot be selected by id either:
143+
the list is **paginated at 12 and ordered by `updated_at DESC`**, and under load
144+
this test's own card is routinely off page 1 (measured: 12 of 12 slots taken by
145+
fresher flows) — so the old spec's "success" depended on *some* other worker's
146+
"New Flow" being on top, which also made the discard assertion (`count === 0`)
147+
vacuous whenever it opened a stranger's fresh blank flow. And the id must be
148+
read **after** the blank-flow navigation: `awaitBootstrapTest` reaches the
149+
templates modal through "New Flow", which parks the page on a placeholder flow
150+
that Langflow deletes as soon as the modal navigates elsewhere (#490/#681
151+
again, from the other side). The re-open is therefore by URL, and each exit now
152+
asserts the editor was left — verified by forcing the save PATCH to 500, which
153+
now fails at the exit step instead of 45 s later on an unrelated locator.
119154
- **#790 (load-collateral, critical clicks hardened).** On load-degraded /
120155
guard-tripped dailies (2026-07-15/16) the spec failed with
121156
`locator.click: Timeout 20000ms exceeded` on a manual-save click target. Not a

tests/tests-automations/regression/flow-functionality/auto-save-off.spec.ts

Lines changed: 101 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,87 @@ test.afterEach(async ({ request }) => {
3939
}
4040
});
4141

42-
// The /flows a11y refactor (Langflow #13891) makes `flow-name-div`
43-
// `pointer-events-none`; open the flow via the card's overlay button.
44-
async function reopenNewFlow(page: Page): Promise<void> {
45-
// Explicit timeout above the 20s default actionTimeout: this heavy spec (two
46-
// bootstraps + repeated exit/re-open cycles) blew the default card-open click
47-
// under CI saturation on load-degraded dailies (#790). The extra headroom
48-
// absorbs transient load without masking a real regression.
49-
await page
50-
.getByTestId("list-card")
51-
.filter({
52-
has: page.getByTestId("flow-name-div").filter({ hasText: "New Flow" }),
53-
})
54-
.getByTestId("list-card-open-button")
55-
.first()
56-
.click({ timeout: 45000 });
42+
/**
43+
* The id of the flow currently open in the editor, cross-checked against the
44+
* flows THIS page created.
45+
*
46+
* The cross-check is the point: an id in the URL that this page never created
47+
* means the spec is driving somebody else's flow — the state #1336 spent two
48+
* dailies failing on, and one better named here than six steps later as a
49+
* 45 s timeout.
50+
*/
51+
async function editorFlowId(page: Page): Promise<string> {
52+
const id = new URL(page.url()).pathname.match(/\/flow\/([0-9a-f-]{36})/)?.[1];
53+
// The POST body is parsed asynchronously, so the entry can land a tick after
54+
// the navigation; poll briefly rather than racing it.
55+
await expect
56+
.poll(() => createdFlowIds.includes(id ?? ""), { timeout: 10000 })
57+
.toBe(true);
58+
return id!;
5759
}
5860

59-
// Quarantined: recurrent flake on the daily (2026-07-22, 2026-08-06, same
60-
// signature) — reopening the just-created flow through its
61-
// `list-card-open-button` times out at 45 s. Tracked in #1336; lifting the
62-
// quarantine (remove `test.fixme` + restore `@stable`) is a deliverable there.
63-
test.fixme(
61+
/**
62+
* Leaving the canvas is itself part of the contract under test, so assert it
63+
* instead of inferring it from whatever the next step happens to find. When the
64+
* save behind "Save And Exit" fails, the editor simply stays put — under the
65+
* old spec that surfaced 45 s later as a card-click timeout on the flows list
66+
* (#1336), which named neither the step nor the cause. Verified by forcing the
67+
* save PATCH to 500: the failure now lands on this call.
68+
*/
69+
async function expectLeftEditor(page: Page): Promise<void> {
70+
await page.waitForURL((url) => !url.pathname.startsWith("/flow/"), {
71+
timeout: 30000,
72+
});
73+
}
74+
75+
/**
76+
* Re-open THIS test's flow by id and wait until its graph has been applied to
77+
* the canvas.
78+
*
79+
* Why by id and not through the flows-list card (#1336). The spec used to click
80+
* the first `list-card` whose name contained "New Flow". Langflow names every
81+
* blank flow "New Flow"/"New Flow (N)", so under `fullyParallel` the list holds
82+
* one per worker — and the flows list is **paginated at 12, ordered by
83+
* `updated_at DESC`**. Both halves bite:
84+
*
85+
* - the card that filter resolved was routinely ANOTHER worker's flow (proved
86+
* on nightly 1.12.0.dev18: the page created ids `8e767306` and `ee8e0ab9`
87+
* and the re-open landed on `164b3c19`, which it never created). When that
88+
* worker's own id-scoped cleanup deleted it mid-test, the save PATCH came
89+
* back 404, the editor never navigated back to the list, and the next
90+
* re-open waited out its 45 s — the exact 2026-07-22 / 2026-08-06 daily
91+
* signature, reproduced 3/8 at `--workers=4`;
92+
* - this test's own card is frequently NOT on page 1 at all (measured: 12 of
93+
* 12 slots taken by fresher flows), so no card-based selector — not even the
94+
* id-scoped `flow-name-<uuid>` testid the card carries — can find it.
95+
*
96+
* Opening by URL removes both. The trade-off is deliberate: what this spec
97+
* validates is server-side persistence across an exit/re-open, and a full
98+
* reload proves that more strictly than an SPA route change. The
99+
* "open a flow from its list card" path stays covered by the specs whose
100+
* subject it is (`bulk-actions`, `mcp-server`).
101+
*/
102+
async function reopenFlow(page: Page, flowId: string): Promise<void> {
103+
// The graph is applied to the canvas only after this GET resolves; waiting on
104+
// it is what keeps the `div-generic-node` counts below from reading an empty
105+
// canvas that simply had not rendered yet.
106+
const flowLoaded = page.waitForResponse(
107+
(resp) =>
108+
new URL(resp.url()).pathname === `/api/v1/flows/${flowId}` &&
109+
resp.request().method() === "GET" &&
110+
resp.status() === 200,
111+
{ timeout: 45000 },
112+
);
113+
await page.goto(`/flow/${flowId}`);
114+
await flowLoaded;
115+
await page.waitForSelector('[data-testid="canvas_controls_dropdown"]', {
116+
timeout: 45000,
117+
});
118+
}
119+
120+
test(
64121
"user should be able to manually save a flow when the auto_save is off",
65-
{ tag: ["@release", "@api", "@database", "@components"] },
122+
{ tag: ["@stable", "@release", "@api", "@database", "@components"] },
66123
async ({ page }) => {
67124
trackCreatedFlows(page);
68125

@@ -88,7 +145,24 @@ test.fixme(
88145
timeout: 5000,
89146
});
90147

148+
// `awaitBootstrapTest` reaches the templates modal through the "New Flow"
149+
// entry point, which already parked the page on a freshly created
150+
// PLACEHOLDER flow — and Langflow deletes that placeholder as soon as the
151+
// modal navigates elsewhere. So the id has to be read after the blank-flow
152+
// navigation, never from the URL standing before it (#490/#681).
153+
const placeholderUrl = page.url();
91154
await page.getByTestId("blank-flow").click();
155+
await page.waitForURL(
156+
(url) =>
157+
/\/flow\/[0-9a-f-]{36}/.test(url.pathname) &&
158+
url.toString() !== placeholderUrl,
159+
{ timeout: 30000 },
160+
);
161+
162+
// Resolve the flow under test once, before any edit: every re-open below is
163+
// pinned to this id, so the spec can never drive a parallel worker's
164+
// identically-named "New Flow" (#1336).
165+
const flowUnderTest = await editorFlowId(page);
92166

93167
await page.getByTestId("sidebar-search-input").click();
94168
await page.getByTestId("sidebar-search-input").fill("chat input");
@@ -131,8 +205,9 @@ test.fixme(
131205
page.getByText("Unsaved changes will be permanently lost."),
132206
).toBeVisible({ timeout: 10000 });
133207
await page.getByText("Exit Anyway", { exact: true }).click();
208+
await expectLeftEditor(page);
134209

135-
await reopenNewFlow(page);
210+
await reopenFlow(page, flowUnderTest);
136211

137212
await page.waitForSelector('[data-testid="sidebar-search-input"]', {
138213
timeout: 5000,
@@ -167,8 +242,9 @@ test.fixme(
167242
const saveAndExit = page.getByText("Save And Exit", { exact: true }).last();
168243
await expect(saveAndExit).toBeVisible({ timeout: 10000 });
169244
await saveAndExit.click();
245+
await expectLeftEditor(page);
170246

171-
await reopenNewFlow(page);
247+
await reopenFlow(page, flowUnderTest);
172248

173249
await page.waitForSelector("text=loading", {
174250
state: "hidden",
@@ -219,8 +295,9 @@ test.fixme(
219295
) {
220296
await saveAndExit2.click();
221297
}
298+
await expectLeftEditor(page);
222299

223-
await reopenNewFlow(page);
300+
await reopenFlow(page, flowUnderTest);
224301

225302
await page.waitForSelector('[data-testid="sidebar-search-input"]', {
226303
timeout: 5000,

0 commit comments

Comments
 (0)