From a15466ff543a6811f6f111bae9ec21ec01bc6388 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 16 Aug 2026 01:53:28 -0700 Subject: [PATCH] [eric] workflows: run rows carry their workflow's name; the title join failed for every deleted workflow (ENG-307) --- backend/apps/help/changelog.py | 1 + backend/apps/workflows/models.py | 2 + backend/apps/workflows/storage.py | 4 ++ .../tests/test_run_rows_keep_their_name.py | 48 +++++++++++++++++++ .../src/app/pages/Workflows/app/HomeView.tsx | 2 +- frontend/src/shared/state/workflowsSlice.ts | 1 + 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_run_rows_keep_their_name.py diff --git a/backend/apps/help/changelog.py b/backend/apps/help/changelog.py index 9d626928..04ddbf2a 100644 --- a/backend/apps/help/changelog.py +++ b/backend/apps/help/changelog.py @@ -89,6 +89,7 @@ P_RELEASES: List[ReleaseNote] = [ "The app opens seconds faster on machines with a crowded system temp folder. File uploads moved into OpenSwarm's own folder, so startup no longer pays a toll that grew with years of temp-file clutter.", "Clicking a chat in the sidebar or history now frames the whole card. The camera used to aim at the chat's collapsed footprint, so an opened chat could land with its bottom half off-screen and need a manual pan after every autofocus.", "Starting a new chat no longer makes another agent's revealed subagents disappear from the canvas. Their cards were being cleaned up as strays by the same pass that places the new one.", + "Workflow run history shows each run's workflow name instead of the word \"Workflow\" on every row. The name now travels with the run, so it survives renames and deleted workflows.", "Heavy sessions no longer vanish without a trace. When memory climbs past the safe line the app now sheds weight itself: preview thumbnails pause and refetchable caches drop, instead of growing until the operating system kills it mid-task.", ], ), diff --git a/backend/apps/workflows/models.py b/backend/apps/workflows/models.py index a60724b8..fbfe0f8e 100644 --- a/backend/apps/workflows/models.py +++ b/backend/apps/workflows/models.py @@ -137,6 +137,8 @@ class Workflow(BaseModel): class WorkflowRun(BaseModel): id: str = Field(default_factory=lambda: uuid4().hex) workflow_id: str + # Stamped at record time so run history stays readable after the workflow is renamed or deleted. + workflow_title: Optional[str] = None status: Literal["running", "success", "failure", "ran_late", "skipped"] = "running" scheduled_for: Optional[datetime] = None started_at: datetime = Field(default_factory=datetime.now) diff --git a/backend/apps/workflows/storage.py b/backend/apps/workflows/storage.py index 01cda893..2d6743bc 100644 --- a/backend/apps/workflows/storage.py +++ b/backend/apps/workflows/storage.py @@ -241,6 +241,10 @@ def record_run(run: WorkflowRun) -> WorkflowRun: if run.workflow_id in p_deleted_ids: return run _ensure_dirs() + if run.workflow_title is None: + wf = _workflow_cache.get(run.workflow_id) + if wf is not None: + run.workflow_title = wf.title arr = _runs_cache.setdefault(run.workflow_id, []) # Replace prior entry with same id if we're updating an in-flight run. for i, prior in enumerate(arr): diff --git a/backend/tests/test_run_rows_keep_their_name.py b/backend/tests/test_run_rows_keep_their_name.py new file mode 100644 index 00000000..07cd455a --- /dev/null +++ b/backend/tests/test_run_rows_keep_their_name.py @@ -0,0 +1,48 @@ +"""Run history rows all read "Workflow" once their workflow is gone (ENG-307, live-reproduced: +/api/workflows/runs/all returned 8 runs whose workflow_ids matched zero listed workflows, so the +frontend's title join could never succeed). The title is now stamped onto the run at record time, +so the label survives rename and deletion instead of depending on a join that can fail. +""" +import pytest +from typeguard import typechecked + +from backend.apps.workflows import storage +from backend.apps.workflows.models import Workflow, WorkflowRun + + +@pytest.fixture() +def p_isolated_store(tmp_path, monkeypatch): + monkeypatch.setattr(storage, "DATA_DIR", str(tmp_path)) + storage.init() + yield tmp_path + monkeypatch.undo() + storage.init() + + +@typechecked +def p_make_workflow(title: str) -> Workflow: + wf = Workflow(title=title) + storage.save_workflow(wf) + return wf + + +def test_record_run_stamps_the_workflow_title(p_isolated_store): + wf = p_make_workflow("Read Cart Screenshots") + run = storage.record_run(WorkflowRun(workflow_id=wf.id)) + assert run.workflow_title == "Read Cart Screenshots" + + +def test_stamped_title_survives_workflow_deletion(p_isolated_store): + wf = p_make_workflow("Nightly digest") + storage.record_run(WorkflowRun(workflow_id=wf.id, status="success")) + storage.delete_workflow(wf.id) + survivors = [r for r in storage.list_all_runs() if r.workflow_id == wf.id] + if not survivors: + pytest.skip("delete purges run history in this store; nothing left to label") + assert survivors[0].workflow_title == "Nightly digest" + + +def test_a_caller_provided_title_is_never_overwritten(p_isolated_store): + wf = p_make_workflow("Fresh name") + run = storage.record_run(WorkflowRun(workflow_id=wf.id, workflow_title="Name at fire time")) + assert run.workflow_title == "Name at fire time" diff --git a/frontend/src/app/pages/Workflows/app/HomeView.tsx b/frontend/src/app/pages/Workflows/app/HomeView.tsx index cd7f0cf3..3f63ef76 100644 --- a/frontend/src/app/pages/Workflows/app/HomeView.tsx +++ b/frontend/src/app/pages/Workflows/app/HomeView.tsx @@ -85,7 +85,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { const recents = useMemo(() => allRuns.slice(0, 8).map((r) => ({ id: r.id, wfId: r.workflow_id, - title: items[r.workflow_id]?.title || 'Workflow', + title: items[r.workflow_id]?.title || r.workflow_title || 'Workflow', status: r.status, summary: r.error || r.last_tool_label || (r.status === 'success' ? 'Completed' : r.status), when: r.started_at ? new Date(r.started_at) : null, diff --git a/frontend/src/shared/state/workflowsSlice.ts b/frontend/src/shared/state/workflowsSlice.ts index acba632c..4d7535ca 100644 --- a/frontend/src/shared/state/workflowsSlice.ts +++ b/frontend/src/shared/state/workflowsSlice.ts @@ -123,6 +123,7 @@ export interface Workflow { export interface WorkflowRun { id: string; workflow_id: string; + workflow_title?: string | null; status: 'running' | 'success' | 'failure' | 'ran_late' | 'skipped'; scheduled_for: string | null; started_at: string;