[eric] apps: export drops the stale inline files snapshot for workspace apps so edits don't revert on import (disk is source of truth)

This commit is contained in:
ciregenz
2026-06-29 15:57:21 -07:00
parent cfaddf1126
commit 30d421993b
2 changed files with 75 additions and 1 deletions
+2 -1
View File
@@ -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]:
@@ -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"}