mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 02:07:45 +02:00
[eric] apps: seed fills missing files only so reopen keeps agent edits
This commit is contained in:
@@ -346,33 +346,41 @@ async def seed_workspace(body: WorkspaceSeedRequest):
|
||||
"already_seeded": already_seeded,
|
||||
}
|
||||
|
||||
# Legacy flat path; unchanged.
|
||||
# Legacy flat path. Seed only fills in MISSING files; it never overwrites
|
||||
# what's already on disk. A reopen re-sends the inline output.files snapshot,
|
||||
# which lags behind whatever the agent just wrote to the workspace; writing it
|
||||
# back reverted every edited file (new files survived, edited ones snapped to
|
||||
# the snapshot). Disk wins once an app exists.
|
||||
if body.files:
|
||||
for rel_path, content in body.files.items():
|
||||
full_path = os.path.normpath(os.path.join(folder, rel_path))
|
||||
if not full_path.startswith(os.path.normpath(folder)):
|
||||
continue
|
||||
if os.path.exists(full_path):
|
||||
continue
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
else:
|
||||
for rel_path, content in VIEW_TEMPLATE_FILES.items():
|
||||
full_path = os.path.join(folder, rel_path)
|
||||
if os.path.exists(full_path):
|
||||
continue
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
# Seed the workspace's SKILL.md with the LIVE skill content so an
|
||||
# agent that Reads SKILL.md sees the same text the Skills page shows.
|
||||
# Snapshot at workspace creation; subsequent edits don't rewrite
|
||||
# already-seeded workspaces (the system-prompt injection in
|
||||
# agent_manager reads live, so the agent always has the latest
|
||||
# rules regardless of this on-disk copy).
|
||||
with open(os.path.join(folder, "SKILL.md"), "w", encoding="utf-8") as f:
|
||||
f.write(load_app_builder_skill())
|
||||
# SKILL.md is a creation-time snapshot; the live rules reach the agent via
|
||||
# the system-prompt injection regardless, so never rewrite an existing one.
|
||||
skill_path = os.path.join(folder, "SKILL.md")
|
||||
if not os.path.exists(skill_path):
|
||||
with open(skill_path, "w", encoding="utf-8") as f:
|
||||
f.write(load_app_builder_skill())
|
||||
|
||||
if body.meta:
|
||||
with open(os.path.join(folder, "meta.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(body.meta, f, indent=2)
|
||||
meta_path = os.path.join(folder, "meta.json")
|
||||
if not os.path.exists(meta_path):
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
json.dump(body.meta, f, indent=2)
|
||||
|
||||
return {"path": os.path.abspath(folder), "template_mode": "flat"}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Seed must CREATE, never overwrite. Reopening an app re-POSTs the inline
|
||||
output.files snapshot, which lags behind whatever the agent last wrote to the
|
||||
workspace on disk; seeding it back used to revert every edited file while the
|
||||
agent's new files survived (edits looked half-reverted on the next export).
|
||||
|
||||
Path constants are module-level, so (like test_versions) we monkeypatch them
|
||||
into a temp tree. seed_workspace is async; we drive it with asyncio.run from a
|
||||
sync test so the suite's bare-async-skip doesn't quietly no-op these."""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.apps.outputs import outputs as outputs_mod
|
||||
from backend.apps.outputs.models import WorkspaceSeedRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ws_root(tmp_path, monkeypatch):
|
||||
root = tmp_path / "ws"
|
||||
root.mkdir()
|
||||
monkeypatch.setattr(outputs_mod, "WORKSPACE_DIR", str(root))
|
||||
return root
|
||||
|
||||
|
||||
def _seed(**kw):
|
||||
return asyncio.run(outputs_mod.seed_workspace(WorkspaceSeedRequest(**kw)))
|
||||
|
||||
|
||||
def _read(folder, rel):
|
||||
with open(os.path.join(folder, rel), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_reopen_seed_preserves_agent_edits(ws_root):
|
||||
wsid = "ws-reopen"
|
||||
folder = os.path.join(str(ws_root), wsid)
|
||||
os.makedirs(os.path.join(folder, "frontend", "src"))
|
||||
# v1 on disk, captured into the inline snapshot the editor later autosaves.
|
||||
with open(os.path.join(folder, "frontend", "src", "App.tsx"), "w") as f:
|
||||
f.write("<h1>v1</h1>")
|
||||
snapshot = {"frontend/src/App.tsx": "<h1>v1</h1>"}
|
||||
|
||||
# Agent advances the workspace to v2 on disk: edits a file, adds a new one.
|
||||
with open(os.path.join(folder, "frontend", "src", "App.tsx"), "w") as f:
|
||||
f.write("<h1>v2 agent</h1>")
|
||||
with open(os.path.join(folder, "frontend", "src", "New.tsx"), "w") as f:
|
||||
f.write("// new v2 file")
|
||||
|
||||
# Reopen replays the stale snapshot through seed.
|
||||
_seed(workspace_id=wsid, files=snapshot, meta={"name": "App"})
|
||||
|
||||
assert _read(folder, "frontend/src/App.tsx") == "<h1>v2 agent</h1>" # not reverted
|
||||
assert os.path.exists(os.path.join(folder, "frontend", "src", "New.tsx")) # survived
|
||||
|
||||
|
||||
def test_fresh_seed_materializes_saved_files(ws_root):
|
||||
wsid = "ws-fresh"
|
||||
folder = os.path.join(str(ws_root), wsid)
|
||||
_seed(workspace_id=wsid,
|
||||
files={"index.html": "<html>saved</html>", "style.css": "body{}"},
|
||||
meta={"name": "Flat"})
|
||||
assert _read(folder, "index.html") == "<html>saved</html>"
|
||||
assert _read(folder, "style.css") == "body{}"
|
||||
|
||||
|
||||
def test_seed_fills_only_missing_files(ws_root):
|
||||
wsid = "ws-partial"
|
||||
folder = os.path.join(str(ws_root), wsid)
|
||||
os.makedirs(folder)
|
||||
with open(os.path.join(folder, "keep.txt"), "w") as f:
|
||||
f.write("on-disk wins")
|
||||
# snapshot wants to change keep.txt AND add gone.txt; only the missing one lands.
|
||||
_seed(workspace_id=wsid,
|
||||
files={"keep.txt": "snapshot loses", "gone.txt": "recreated"})
|
||||
assert _read(folder, "keep.txt") == "on-disk wins"
|
||||
assert _read(folder, "gone.txt") == "recreated"
|
||||
Reference in New Issue
Block a user