Let a node's failure outlive the run that followed it
A node's error cleared the moment it ran again, so a failure that genuinely fired an alert could leave no trace on the canvas by the time anyone looked. The engine records it now — on the node's status, so it survives a reload and every client agrees — and reading the traceback is what clears it. The seam is the event bus, which is where every failing path already meets: a queued live run, an explicit run, a preview, and a single triggered node all publish `node_error`, while the controller's own observer would have seen only one of them. That was half the confusion. The other half: clicking a failed neuron on Home often landed on a flow where everything looked fine. Nodes merge into one neuron by instance key — every InfluxDB node pointing at the same bucket is one neuron — and the click went to whichever flow contributed a member first, not the one that failed. It now goes to the failing member and selects it, and the canvas marks a failing node rather than leaving it to the dot alone. The inject node emitted one payload to every port it declared, whatever their types, so an inject on a bool port carrying the text "true" raised at publish time. Each port gets its own field now, typed and parsed by that port's dtype, and remembers what it last sent. A port that is renamed carries its value with it; one that is removed takes its value with it. An inject written before this keeps emitting exactly what it did. The derived-cron chip also appeared on the delay node, where `interval` is a rate limit and a schedule derived from it means nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -19,6 +19,9 @@ const PRINTING_NODE = `def process():
|
||||
const BROKEN_NODE = `def process(reading):
|
||||
raise RuntimeError("downstream blew up")
|
||||
`
|
||||
const FIXED_NODE = `def process(reading):
|
||||
print(f"logged {reading}")
|
||||
`
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
await deleteAll(browser, [`/flows/${flowName}`])
|
||||
@@ -61,11 +64,9 @@ test.beforeAll(async ({ browser }) => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test("the dashboard lists flows and can stop one", async ({ page }) => {
|
||||
test("the home page lists flows and can stop one", async ({ page }) => {
|
||||
await page.goto("/")
|
||||
const row = page
|
||||
.getByTestId("dashboard-flow-row")
|
||||
.filter({ hasText: "Runtime" })
|
||||
const row = page.getByTestId("home-flow-row").filter({ hasText: "Runtime" })
|
||||
await expect(row).toBeVisible()
|
||||
await expect(row).toContainText("Running")
|
||||
|
||||
@@ -117,6 +118,33 @@ test("the logs panel shows what a node printed and why one failed", async ({
|
||||
await expect(panel).toBeHidden()
|
||||
})
|
||||
|
||||
test("a node's failure outlives its next good run", async ({ page }) => {
|
||||
await page.goto(`/flows/${flowName}`)
|
||||
await page.waitForSelector(".react-flow__node")
|
||||
|
||||
const logger = page.locator(".react-flow__node").filter({ hasText: "logger" })
|
||||
await page.getByTestId("run-flow").click()
|
||||
await expect(logger.getByTestId("node-traceback")).toBeVisible()
|
||||
|
||||
// Fix it the way the editor does, and put it on the engine: publishing
|
||||
// rebuilds every node, which is exactly what must not wipe the record.
|
||||
await api(page, `/flows/${flowName}/nodes/logger/source`, {
|
||||
method: "PUT",
|
||||
data: { code: FIXED_NODE },
|
||||
})
|
||||
const detail = await (await api(page, `/flows/${flowName}`)).json()
|
||||
await api(page, `/flows/${flowName}/publish`, {
|
||||
method: "POST",
|
||||
data: { version: detail.definition.version },
|
||||
})
|
||||
|
||||
await page.getByTestId("run-flow").click()
|
||||
// The dot says how the *last* run went, and the traceback button says the
|
||||
// node failed at some point since anyone looked. Both at once is the point.
|
||||
await expect(logger.getByLabel("Last run succeeded")).toBeVisible()
|
||||
await expect(logger.getByTestId("node-traceback")).toBeVisible()
|
||||
})
|
||||
|
||||
test("a flow can be paused and let go again", async ({ page }) => {
|
||||
await page.goto(`/flows/${flowName}`)
|
||||
await page.waitForSelector(".react-flow__node")
|
||||
|
||||
Reference in New Issue
Block a user