[eric] workflows: run rows carry their workflow's name; the title join failed for every deleted workflow (ENG-307)

This commit is contained in:
ciregenz
2026-08-16 01:53:28 -07:00
parent 96bdd222c6
commit a15466ff54
6 changed files with 57 additions and 1 deletions
+1
View File
@@ -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.",
],
),
+2
View File
@@ -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)
+4
View File
@@ -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):
@@ -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"
@@ -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,
@@ -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;