[eric] apps: renaming an app writes the new name into its meta.json so agents stop reading the old one (ENG-308)

This commit is contained in:
ciregenz
2026-08-14 15:20:11 -07:00
parent c3db3b5c0d
commit cc6f504873
2 changed files with 95 additions and 0 deletions
+35
View File
@@ -186,6 +186,36 @@ async def read_workspace(workspace_id: str):
return {"files": files, "meta": meta, "path": os.path.abspath(folder), "truncated": truncated}
def write_meta_json_fields(workspace_id: str, fields: dict) -> bool:
"""Push a rename back onto disk. `meta.json` is what AGENTS read, so a name that only ever
reached the record meant the user said "the X app" while the agent saw the old one. Merges
into the existing file so nothing else in it is lost, and stays quiet if there is no workspace."""
if not workspace_id or not fields:
return False
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
return False
meta_path = os.path.join(folder, "meta.json")
meta: dict = {}
if os.path.exists(meta_path):
try:
with open(meta_path) as f:
loaded = json.load(f)
if isinstance(loaded, dict):
meta = loaded
except (OSError, json.JSONDecodeError, ValueError):
meta = {}
if all(meta.get(k) == v for k, v in fields.items()):
return False
meta.update(fields)
try:
with open(meta_path, "w") as f:
json.dump(meta, f, indent=2)
return True
except OSError:
return False
def sync_output_from_meta_json(workspace_id: str, fallback_name: str | None = None) -> bool:
"""Sync the Output row's name/description from meta.json (or fallback_name when
meta.json has no name). Only overwrites placeholder values; user renames win."""
@@ -652,6 +682,11 @@ async def update_output(output_id: str, body: OutputUpdate):
if body.thumbnail is not None:
output.preview_updated_at = now
save(output)
# A rename must reach meta.json too, or the UI and the file agents read drift apart (ENG-308).
p_sent = body.model_dump(exclude_unset=True)
p_meta = {k: getattr(output, k) for k in ("name", "description") if k in p_sent and getattr(output, k)}
if p_meta:
write_meta_json_fields(getattr(output, "workspace_id", "") or "", p_meta)
return {"ok": True, "output": output.model_dump()}
@@ -0,0 +1,60 @@
"""Renaming an app must land in its meta.json (ENG-308, Haik 2026-08-14).
`update_output` wrote the record and nothing else, so the UI showed the new name while the file
AGENTS read still held the old one: the user says "the X app" and the agent is looking at something
called something else. The pull direction (`sync_output_from_meta_json`) deliberately only fills
placeholders, so nothing was ever going to reconcile them.
"""
import json
import os
from backend.apps.outputs.outputs import WORKSPACE_DIR, write_meta_json_fields
def p_workspace(tmp_name: str) -> str:
ws = f"qa-rename-{tmp_name}"
folder = os.path.join(WORKSPACE_DIR, ws)
os.makedirs(folder, exist_ok=True)
return ws
def test_a_rename_lands_in_meta_json():
ws = p_workspace("basic")
path = os.path.join(WORKSPACE_DIR, ws, "meta.json")
with open(path, "w") as f:
json.dump({"name": "Old Name", "description": "d", "keepme": 1}, f)
assert write_meta_json_fields(ws, {"name": "New Name"}) is True
meta = json.load(open(path))
assert meta["name"] == "New Name"
assert meta["keepme"] == 1, "a rename must not drop the rest of the file"
assert meta["description"] == "d"
def test_writing_the_same_name_is_a_no_op():
ws = p_workspace("noop")
path = os.path.join(WORKSPACE_DIR, ws, "meta.json")
with open(path, "w") as f:
json.dump({"name": "Same"}, f)
assert write_meta_json_fields(ws, {"name": "Same"}) is False
def test_a_missing_workspace_is_quiet_not_fatal():
assert write_meta_json_fields("does-not-exist-anywhere", {"name": "x"}) is False
assert write_meta_json_fields("", {"name": "x"}) is False
def test_a_garbled_meta_json_is_rebuilt_rather_than_crashing():
ws = p_workspace("garbled")
path = os.path.join(WORKSPACE_DIR, ws, "meta.json")
with open(path, "w") as f:
f.write("{not json at all")
assert write_meta_json_fields(ws, {"name": "Recovered"}) is True
assert json.load(open(path))["name"] == "Recovered"
def test_the_update_route_pushes_renames_to_disk():
import inspect
from backend.apps.outputs import outputs as mod
src = inspect.getsource(mod.update_output)
assert "write_meta_json_fields" in src, "a rename that never reaches disk is the whole bug"