fix: use unique workflow ID to prevent collision across roots

workflow_dir.name was used as the discovery key and API ID, so two
different WORKFLOW_ROOTS containing a leaf directory with the same
name would silently drop one. Use root name + relative path joined
with __ as a stable unique ID instead.

Co-authored-by: wul48527-code <wul48527-code@users.noreply.github.com>
This commit is contained in:
Dennis-yxchen 2026-03-31 17:18:38 +08:00
parent 64a3076058
commit e48e22afc8
2 changed files with 95 additions and 4 deletions

View file

@ -419,6 +419,18 @@ def _build_lineage_payload(skill_id: str, store: SkillStore) -> Dict[str, Any]:
}
def _workflow_id(workflow_dir: Path) -> str:
"""Stable short ID for a workflow directory, unique across roots."""
resolved = workflow_dir.resolve()
for root in WORKFLOW_ROOTS:
try:
rel = resolved.relative_to(root.resolve())
return f"{root.name}__{'__'.join(rel.parts)}"
except ValueError:
continue
return workflow_dir.name
def _discover_workflow_dirs() -> List[Path]:
discovered: Dict[str, Path] = {}
for root in WORKFLOW_ROOTS:
@ -439,14 +451,14 @@ def _scan_workflow_tree(directory: Path, discovered: Dict[str, Path], *, _depth:
if not child.is_dir():
continue
if (child / "metadata.json").exists() or (child / "traj.jsonl").exists():
discovered.setdefault(child.name, child)
discovered.setdefault(str(child.resolve()), child)
else:
_scan_workflow_tree(child, discovered, _depth=_depth + 1, _max_depth=_max_depth)
def _get_workflow_dir(workflow_id: str) -> Optional[Path]:
for path in _discover_workflow_dirs():
if path.name == workflow_id:
if _workflow_id(path) == workflow_id:
return path
return None
@ -464,7 +476,7 @@ def _build_workflow_summary(workflow_dir: Path) -> Dict[str, Any]:
for candidate in video_candidates:
if candidate.exists():
rel = candidate.relative_to(workflow_dir).as_posix()
video_url = url_for("workflow_artifact", workflow_id=workflow_dir.name, artifact_path=rel)
video_url = url_for("workflow_artifact", workflow_id=_workflow_id(workflow_dir), artifact_path=rel)
break
outcome = metadata.get("execution_outcome") or {}
@ -514,7 +526,7 @@ def _build_workflow_summary(workflow_dir: Path) -> Dict[str, Any]:
iterations = len(trajectory)
return {
"id": workflow_dir.name,
"id": _workflow_id(workflow_dir),
"path": str(workflow_dir),
"task_id": metadata.get("task_id") or metadata.get("task_name") or workflow_dir.name,
"task_name": metadata.get("task_name") or metadata.get("task_id") or workflow_dir.name,

79
tests/test_workflow_id.py Normal file
View file

@ -0,0 +1,79 @@
"""Tests for workflow ID uniqueness and discovery."""
from pathlib import Path
from unittest.mock import patch
import pytest
from openspace import dashboard_server
def _make_workflow(path: Path):
"""Create a minimal workflow directory with metadata.json."""
path.mkdir(parents=True, exist_ok=True)
(path / "metadata.json").write_text("{}", encoding="utf-8")
class TestWorkflowId:
def test_same_leaf_name_different_roots_get_unique_ids(self, tmp_path):
root_a = tmp_path / "root_a"
root_b = tmp_path / "root_b"
wf_a = root_a / "task1"
wf_b = root_b / "task1"
_make_workflow(wf_a)
_make_workflow(wf_b)
with patch.object(dashboard_server, "WORKFLOW_ROOTS", [root_a, root_b]):
id_a = dashboard_server._workflow_id(wf_a)
id_b = dashboard_server._workflow_id(wf_b)
assert id_a != id_b
assert "task1" in id_a
assert "task1" in id_b
def test_nested_workflow_id_includes_path(self, tmp_path):
root = tmp_path / "root"
wf = root / "sub" / "deep" / "task1"
_make_workflow(wf)
with patch.object(dashboard_server, "WORKFLOW_ROOTS", [root]):
wf_id = dashboard_server._workflow_id(wf)
assert wf_id == "root__sub__deep__task1"
def test_workflow_outside_roots_falls_back_to_name(self, tmp_path):
wf = tmp_path / "orphan_workflow"
_make_workflow(wf)
with patch.object(dashboard_server, "WORKFLOW_ROOTS", []):
wf_id = dashboard_server._workflow_id(wf)
assert wf_id == "orphan_workflow"
class TestDiscoverWorkflowDirs:
def test_same_name_workflows_both_discovered(self, tmp_path):
root_a = tmp_path / "root_a"
root_b = tmp_path / "root_b"
_make_workflow(root_a / "task1")
_make_workflow(root_b / "task1")
with patch.object(dashboard_server, "WORKFLOW_ROOTS", [root_a, root_b]):
dirs = dashboard_server._discover_workflow_dirs()
assert len(dirs) == 2
def test_get_workflow_dir_resolves_correct_path(self, tmp_path):
root_a = tmp_path / "root_a"
root_b = tmp_path / "root_b"
_make_workflow(root_a / "task1")
_make_workflow(root_b / "task1")
with patch.object(dashboard_server, "WORKFLOW_ROOTS", [root_a, root_b]):
id_a = dashboard_server._workflow_id(root_a / "task1")
id_b = dashboard_server._workflow_id(root_b / "task1")
found_a = dashboard_server._get_workflow_dir(id_a)
found_b = dashboard_server._get_workflow_dir(id_b)
assert found_a.resolve() == (root_a / "task1").resolve()
assert found_b.resolve() == (root_b / "task1").resolve()