From 30d421993b17411ea4c7d035fedcf0ab23f4c850 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 29 Jun 2026 15:57:21 -0700 Subject: [PATCH] [eric] apps: export drops the stale inline files snapshot for workspace apps so edits don't revert on import (disk is source of truth) --- backend/apps/swarm/entities/apps.py | 3 +- .../tests/test_app_export_no_stale_files.py | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_app_export_no_stale_files.py diff --git a/backend/apps/swarm/entities/apps.py b/backend/apps/swarm/entities/apps.py index 024c37ad..648bbb50 100644 --- a/backend/apps/swarm/entities/apps.py +++ b/backend/apps/swarm/entities/apps.py @@ -40,7 +40,8 @@ class AppExportable: "description": self.output.description, "icon": self.output.icon, "input_schema": self.output.input_schema, - "files": self.output.files, # flat-app inline source; webapp apps leave this empty + # With a workspace, disk is the source and files() ships it; carrying output.files too snaps every edited file back to its creation-time v1 on import (new files survive, edits look reverted). Only true flat apps (no workspace) still need the inline copy. + "files": self.output.files if not self.output.workspace_id else {}, } def files(self) -> dict[str, bytes]: diff --git a/backend/tests/test_app_export_no_stale_files.py b/backend/tests/test_app_export_no_stale_files.py new file mode 100644 index 00000000..221b5841 --- /dev/null +++ b/backend/tests/test_app_export_no_stale_files.py @@ -0,0 +1,73 @@ +"""Export must not carry the stale inline output.files snapshot when a workspace +exists. output.files is frozen at creation (v1); the agent edits land on disk +(v2). serialize() used to ship the v1 snapshot alongside the v2 disk copy, so an +imported app reverted every edited file (new files survived since they were never +in the snapshot). With a workspace, disk is the single source of truth; only true +flat apps (no workspace) still carry the inline copy.""" +import os +import uuid + +import pytest + +from backend.apps.outputs import workspace_io +from backend.apps.outputs.models import Output +from backend.apps.swarm.entities import apps as apps_mod +from backend.apps.swarm.entities.apps import AppExportable + + +class P_Ctx: + pass + + +@pytest.fixture +def p_ws_root(tmp_path, monkeypatch): + root = tmp_path / "ws" + root.mkdir() + out = tmp_path / "out" + out.mkdir() + monkeypatch.setattr(apps_mod, "OUTPUTS_WORKSPACE_DIR", str(root)) + monkeypatch.setattr(workspace_io, "DATA_DIR", str(out)) + return root + + +def p_make_app(ws_root, *, workspace: bool) -> str: + wsid = uuid.uuid4().hex if workspace else None + if wsid: + folder = ws_root / wsid + folder.mkdir() + (folder / "app.py").write_text("print('v1')\n") + o = Output( + name="Demo", description="", icon="view_quilt", + input_schema={"type": "object", "properties": {}, "required": []}, + files={"app.py": "print('v1')\n"}, + workspace_id=wsid, session_id=None, + ) + workspace_io.save(o) + return o.id + + +def test_workspace_app_export_omits_stale_inline_files(p_ws_root): + oid = p_make_app(p_ws_root, workspace=True) + o = workspace_io.load_output(oid) + folder = p_ws_root / o.workspace_id + # agent edit -> v2 on disk: modify app.py + add new.py (output.files stays v1) + (folder / "app.py").write_text("print('v2 EDITED')\n") + (folder / "new.py").write_text("print('v2 NEW')\n") + + exp = AppExportable.load(oid) + payload = exp.serialize(P_Ctx()) + disk = exp.files() + + # the stale v1 snapshot must NOT ride along + assert payload["files"] == {}, "workspace app must not export the frozen output.files snapshot" + # disk (v2) is what travels, both the edit and the new file + assert disk["workspace/app.py"] == b"print('v2 EDITED')\n" + assert disk["workspace/new.py"] == b"print('v2 NEW')\n" + + +def test_flat_app_without_workspace_still_carries_inline_files(p_ws_root): + oid = p_make_app(p_ws_root, workspace=False) + exp = AppExportable.load(oid) + payload = exp.serialize(P_Ctx()) + # no workspace -> inline files ARE the source, must survive export + assert payload["files"] == {"app.py": "print('v1')\n"}