fix(kanban): preserve scratch completion artifacts
This commit is contained in:
@@ -2363,6 +2363,125 @@ def test_cleanup_workspace_removes_managed_scratch_dir(kanban_home):
|
||||
assert not ws.exists(), "Hermes-managed scratch dir should be cleaned up"
|
||||
|
||||
|
||||
def test_complete_task_persists_scratch_artifacts_before_cleanup(kanban_home):
|
||||
"""Completion artifacts from scratch workspaces survive workspace cleanup."""
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="render chart")
|
||||
task = kb.get_task(conn, t)
|
||||
ws = kb.resolve_workspace(task)
|
||||
kb.set_workspace_path(conn, t, ws)
|
||||
artifact = ws / "chart.png"
|
||||
artifact.write_bytes(b"png-bytes")
|
||||
|
||||
assert kb.complete_task(
|
||||
conn,
|
||||
t,
|
||||
result="ok",
|
||||
metadata={"artifacts": [str(artifact)]},
|
||||
)
|
||||
|
||||
completed = [e for e in kb.list_events(conn, t) if e.kind == "completed"][-1]
|
||||
persisted = Path(completed.payload["artifacts"][0])
|
||||
run = kb.latest_run(conn, t)
|
||||
|
||||
assert not ws.exists(), "scratch workspace should still be cleaned up"
|
||||
assert persisted.exists(), "artifact copy should survive scratch cleanup"
|
||||
assert persisted.parent == kb.task_attachments_dir(t)
|
||||
assert persisted.name == "chart.png"
|
||||
assert persisted.read_bytes() == b"png-bytes"
|
||||
assert str(persisted) != str(artifact)
|
||||
assert run is not None
|
||||
assert run.metadata["artifacts"] == [str(persisted)]
|
||||
|
||||
|
||||
def test_complete_task_leaves_non_scratch_artifact_paths_unchanged(
|
||||
kanban_home,
|
||||
tmp_path,
|
||||
):
|
||||
"""Only artifacts inside the managed scratch workspace are copied."""
|
||||
external = tmp_path / "report.md"
|
||||
external.write_text("keep me here", encoding="utf-8")
|
||||
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="external report")
|
||||
task = kb.get_task(conn, t)
|
||||
ws = kb.resolve_workspace(task)
|
||||
kb.set_workspace_path(conn, t, ws)
|
||||
|
||||
assert kb.complete_task(
|
||||
conn,
|
||||
t,
|
||||
result="ok",
|
||||
metadata={"artifacts": [str(external)]},
|
||||
)
|
||||
|
||||
completed = [e for e in kb.list_events(conn, t) if e.kind == "completed"][-1]
|
||||
run = kb.latest_run(conn, t)
|
||||
|
||||
assert not ws.exists(), "scratch workspace should still be cleaned up"
|
||||
assert external.exists()
|
||||
assert completed.payload["artifacts"] == [str(external)]
|
||||
assert run is not None
|
||||
assert run.metadata["artifacts"] == [str(external)]
|
||||
|
||||
|
||||
def test_complete_task_persists_duplicate_scratch_artifact_names(kanban_home):
|
||||
"""Scratch artifact persistence does not overwrite duplicate basenames."""
|
||||
with kb.connect() as conn:
|
||||
t = kb.create_task(conn, title="render reports")
|
||||
task = kb.get_task(conn, t)
|
||||
ws = kb.resolve_workspace(task)
|
||||
kb.set_workspace_path(conn, t, ws)
|
||||
first = ws / "a" / "report.txt"
|
||||
second = ws / "b" / "report.txt"
|
||||
first.parent.mkdir(parents=True)
|
||||
second.parent.mkdir(parents=True)
|
||||
first.write_text("first", encoding="utf-8")
|
||||
second.write_text("second", encoding="utf-8")
|
||||
|
||||
assert kb.complete_task(
|
||||
conn,
|
||||
t,
|
||||
result="ok",
|
||||
metadata={"artifacts": [str(first), str(second)]},
|
||||
)
|
||||
|
||||
completed = [e for e in kb.list_events(conn, t) if e.kind == "completed"][-1]
|
||||
persisted = [Path(p) for p in completed.payload["artifacts"]]
|
||||
|
||||
assert not ws.exists(), "scratch workspace should still be cleaned up"
|
||||
assert [p.name for p in persisted] == ["report.txt", "report_1.txt"]
|
||||
assert [p.read_text(encoding="utf-8") for p in persisted] == ["first", "second"]
|
||||
assert all(p.parent == kb.task_attachments_dir(t) for p in persisted)
|
||||
|
||||
|
||||
def test_complete_task_persists_board_scratch_artifacts_to_board_attachments(kanban_home):
|
||||
"""Board scratch artifacts are copied under that board's attachment root."""
|
||||
kb.create_board("work-proj")
|
||||
|
||||
with kb.connect(board="work-proj") as conn:
|
||||
t = kb.create_task(conn, title="board chart", board="work-proj")
|
||||
task = kb.get_task(conn, t)
|
||||
ws = kb.resolve_workspace(task, board="work-proj")
|
||||
kb.set_workspace_path(conn, t, ws)
|
||||
artifact = ws / "chart.png"
|
||||
artifact.write_bytes(b"board-png")
|
||||
|
||||
assert kb.complete_task(
|
||||
conn,
|
||||
t,
|
||||
result="ok",
|
||||
metadata={"artifacts": [str(artifact)]},
|
||||
)
|
||||
|
||||
completed = [e for e in kb.list_events(conn, t) if e.kind == "completed"][-1]
|
||||
persisted = Path(completed.payload["artifacts"][0])
|
||||
|
||||
assert not ws.exists(), "board scratch workspace should still be cleaned up"
|
||||
assert persisted.exists()
|
||||
assert persisted.parent == kb.task_attachments_dir(t, board="work-proj")
|
||||
|
||||
|
||||
def test_cleanup_workspace_refuses_path_outside_scratch_root(kanban_home, tmp_path):
|
||||
"""A scratch task with a user path outside the workspaces root must NOT be deleted (#28818).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user