[eric] backend: leading-_ -> p_/public for class-private self-attributes (coordinated .attr + string-ref rename); promote cross-file attrs (per_session/idle_lru/payload)

This commit is contained in:
ciregenz
2026-06-23 21:47:49 -07:00
parent 6721c39719
commit d608eb68e3
15 changed files with 98 additions and 98 deletions
+11 -11
View File
@@ -34,10 +34,10 @@ class SeqLogStore:
"""Process-wide store. Per-session locks live inside `p_SessionSeqLog`."""
def __init__(self, persist_dir: Optional[str] = None) -> None:
self._per_session: dict[str, p_SessionSeqLog] = {}
self.per_session: dict[str, p_SessionSeqLog] = {}
# Coarse lock guards only the setdefault path; never crosses an await.
self._dict_lock = asyncio.Lock()
self._persist_dir = persist_dir
self.p_dict_lock = asyncio.Lock()
self.p_persist_dir = persist_dir
if persist_dir:
try:
os.makedirs(persist_dir, exist_ok=True)
@@ -45,18 +45,18 @@ class SeqLogStore:
logger.warning("seq_log: failed to create persist dir %s", persist_dir)
async def _get_or_create(self, session_id: str) -> p_SessionSeqLog:
log = self._per_session.get(session_id)
log = self.per_session.get(session_id)
if log is not None:
return log
async with self._dict_lock:
log = self._per_session.get(session_id)
async with self.p_dict_lock:
log = self.per_session.get(session_id)
if log is None:
log = p_SessionSeqLog()
self._per_session[session_id] = log
self.per_session[session_id] = log
return log
def _peek(self, session_id: str) -> Optional[p_SessionSeqLog]:
return self._per_session.get(session_id)
return self.per_session.get(session_id)
@asynccontextmanager
async def stamp(
@@ -99,13 +99,13 @@ class SeqLogStore:
return log.seq if log else 0
def _terminal_path(self, session_id: str) -> Optional[str]:
if not self._persist_dir:
if not self.p_persist_dir:
return None
# Session ids are uuid4 hex; sanitize anyway against path traversal.
safe = "".join(c for c in session_id if c.isalnum() or c in ("-", "_"))
if not safe:
return None
return os.path.join(self._persist_dir, f"{safe}.json")
return os.path.join(self.p_persist_dir, f"{safe}.json")
def persist_terminal(self, session_id: str, payload_str: str) -> None:
"""Atomic write of a terminal event for post-restart clients; best-effort, never blocks broadcast."""
@@ -134,7 +134,7 @@ class SeqLogStore:
def clear(self, session_id: str) -> None:
"""Drop in-memory log and persisted terminal; for full deletion only, closed-but-retained sessions keep it."""
self._per_session.pop(session_id, None)
self.per_session.pop(session_id, None)
path = self._terminal_path(session_id)
if path and os.path.exists(path):
try:
+48 -48
View File
@@ -85,15 +85,15 @@ class AppRuntime:
# _start_new_mode). frontend_url returns null until this flips,
# so the preview pane doesn't try to navigate to an unbound port
# and show a "Site can't be reached" error mid-npm-install.
self._frontend_ready: bool = False
self.p_frontend_ready: bool = False
# True while the process tree is SIGSTOP'd in the idle pool. A frozen
# vite still holds its port but can't answer it, so frontend_url must
# stay null while suspended (else the webview loads a dead port = the
# ERR_FAILED on fast app-switching).
self._suspended: bool = False
self.p_suspended: bool = False
self.process: Optional[asyncio.subprocess.Process] = None
self.log_buffer: deque[LogLine] = deque(maxlen=LOG_BUFFER_LINES)
self._subscribers: set[LogSubscriber] = set()
self.p_subscribers: set[LogSubscriber] = set()
# Recent build/runtime errors scraped from stderr; drained by
# the agent's post-tool hook after Write/Edit so the agent sees
# vite/babel/uvicorn errors in its next turn and can self-fix
@@ -101,10 +101,10 @@ class AppRuntime:
self.recent_errors: deque[str] = deque(maxlen=RECENT_ERRORS_MAX)
self.render_state: Optional[str] = None
self.render_error_text: str = ""
self._stdout_task: Optional[asyncio.Task] = None
self._stderr_task: Optional[asyncio.Task] = None
self._wait_task: Optional[asyncio.Task] = None
self._frontend_ready_task: Optional[asyncio.Task] = None
self.p_stdout_task: Optional[asyncio.Task] = None
self.p_stderr_task: Optional[asyncio.Task] = None
self.p_wait_task: Optional[asyncio.Task] = None
self.p_frontend_ready_task: Optional[asyncio.Task] = None
self.p_lock = asyncio.Lock()
def drain_errors(self) -> list[str]:
@@ -151,7 +151,7 @@ class AppRuntime:
# the ERR_FAILED you see on reopen. No live process, no URL.
# And gated on `not _suspended`: a SIGSTOP'd idle runtime is "running"
# (returncode is None) but frozen, so its port won't answer.
if self.frontend_port and self._frontend_ready and self.running and not self._suspended:
if self.frontend_port and self.p_frontend_ready and self.running and not self.p_suspended:
return f"http://127.0.0.1:{self.frontend_port}/"
return None
@@ -283,13 +283,13 @@ class AppRuntime:
return False
backend_note = f" + backend on {self.port}" if self.port else ""
self._broadcast(LogLine("runtime", f"[runtime] {launch_desc} started; frontend on {self.frontend_port}{backend_note} (pid {self.process.pid})"))
self._stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
self._stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
self._wait_task = asyncio.create_task(self._await_exit())
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
self.p_wait_task = asyncio.create_task(self._await_exit())
# Kick off the port-bind poller so frontend_url flips on once
# Vite is actually accepting connections.
self._frontend_ready = False
self._frontend_ready_task = asyncio.create_task(self._await_frontend_bind())
self.p_frontend_ready = False
self.p_frontend_ready_task = asyncio.create_task(self._await_frontend_bind())
return True
def _resolve_launch(self, env: dict) -> tuple[list[str], str, str]:
@@ -368,7 +368,7 @@ class AppRuntime:
await writer.wait_closed()
except Exception:
pass
self._frontend_ready = True
self.p_frontend_ready = True
self._broadcast(LogLine(
"runtime",
f"[runtime] frontend ready at http://127.0.0.1:{port}/",
@@ -424,9 +424,9 @@ class AppRuntime:
self.process = None
return False
self._broadcast(LogLine("runtime", f"[runtime] backend started on port {self.port} (pid {self.process.pid})"))
self._stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
self._stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
self._wait_task = asyncio.create_task(self._await_exit())
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
self.p_wait_task = asyncio.create_task(self._await_exit())
return True
def _spawn_env_base(self) -> dict[str, str]:
@@ -448,8 +448,8 @@ class AppRuntime:
if not self.process or self.process.returncode is not None:
# Still cancel the bind poller in case stop() races a
# never-launched runtime; defensive no-op otherwise.
if self._frontend_ready_task and not self._frontend_ready_task.done():
self._frontend_ready_task.cancel()
if self.p_frontend_ready_task and not self.p_frontend_ready_task.done():
self.p_frontend_ready_task.cancel()
return
try:
# Walk the descendant tree first so vite/uvicorn grandchildren
@@ -468,9 +468,9 @@ class AppRuntime:
pass
# Cancel the bind poller so it stops scanning a port that's
# gone away, and reset the readiness flag.
if self._frontend_ready_task and not self._frontend_ready_task.done():
self._frontend_ready_task.cancel()
self._frontend_ready = False
if self.p_frontend_ready_task and not self.p_frontend_ready_task.done():
self.p_frontend_ready_task.cancel()
self.p_frontend_ready = False
async def restart(self) -> bool:
await self.stop()
@@ -480,7 +480,7 @@ class AppRuntime:
"""Register a log subscriber. Immediately replays the ring buffer
so a Terminal pane that opens mid-session shows context. Returns
an unsubscribe function."""
self._subscribers.add(cb)
self.p_subscribers.add(cb)
for line in list(self.log_buffer):
try:
cb(line)
@@ -488,14 +488,14 @@ class AppRuntime:
pass
def p_unsub() -> None:
self._subscribers.discard(cb)
self.p_subscribers.discard(cb)
return p_unsub
def _broadcast(self, line: LogLine) -> None:
self.log_buffer.append(line)
# Snapshot subscribers; they can self-remove during dispatch.
for cb in list(self._subscribers):
for cb in list(self.p_subscribers):
try:
cb(line)
except Exception:
@@ -536,7 +536,7 @@ class AppRuntime:
# Unclean death (vite crash, OOM, orphaned parent) must drop readiness;
# otherwise frontend_url keeps advertising a dead port and the preview
# navigates into ERR_FAILED. stop() already does this for clean stops.
self._frontend_ready = False
self.p_frontend_ready = False
self._broadcast(LogLine("runtime", f"[runtime] backend exited with code {rc}"))
@@ -553,11 +553,11 @@ class AppRuntimeManager:
def __init__(self) -> None:
# workspace_id → AppRuntime, currently has >=1 subscriber.
self.runtimes: dict[str, AppRuntime] = {}
self._attached: dict[str, int] = {}
self.p_attached: dict[str, int] = {}
# workspace_id → AppRuntime with no subscribers but still
# alive. OrderedDict gives O(1) move_to_end + popitem(last=False)
# for LRU semantics.
self._idle_lru: "OrderedDict[str, AppRuntime]" = OrderedDict()
self.idle_lru: "OrderedDict[str, AppRuntime]" = OrderedDict()
self.p_lock = asyncio.Lock()
async def attach(self, workspace_id: str, workspace_path: str) -> AppRuntime:
@@ -571,7 +571,7 @@ class AppRuntimeManager:
if rt is None:
# Maybe the runtime is sitting idle in the LRU; revive
# it without paying the spawn cost again.
idle_rt = self._idle_lru.pop(workspace_id, None)
idle_rt = self.idle_lru.pop(workspace_id, None)
if idle_rt is not None and idle_rt.running:
rt = idle_rt
rt.workspace_path = workspace_path
@@ -580,7 +580,7 @@ class AppRuntimeManager:
# SIGCONT the process tree if A2 had it paused while
# idle. Pair with the SIGSTOP in detach() below.
resume_process_tree(rt.process)
rt._suspended = False
rt.p_suspended = False
else:
if idle_rt is not None:
# Stale idle entry; process died while idling.
@@ -595,7 +595,7 @@ class AppRuntimeManager:
# folder), trust the latest caller; they have the
# current truth.
rt.workspace_path = workspace_path
self._attached[workspace_id] = self._attached.get(workspace_id, 0) + 1
self.p_attached[workspace_id] = self.p_attached.get(workspace_id, 0) + 1
if not revived and not rt.running:
await rt.start()
# Stop any dead idle runtime outside the lock to avoid blocking.
@@ -610,11 +610,11 @@ class AppRuntimeManager:
to_idle: Optional[AppRuntime] = None
to_reap: list[AppRuntime] = []
async with self.p_lock:
count = self._attached.get(workspace_id, 0) - 1
count = self.p_attached.get(workspace_id, 0) - 1
if count > 0:
self._attached[workspace_id] = count
self.p_attached[workspace_id] = count
return
self._attached.pop(workspace_id, None)
self.p_attached.pop(workspace_id, None)
rt = self.runtimes.pop(workspace_id, None)
if rt is None:
return
@@ -625,12 +625,12 @@ class AppRuntimeManager:
if not rt.running:
to_reap.append(rt)
else:
self._idle_lru[workspace_id] = rt
self._idle_lru.move_to_end(workspace_id)
self.idle_lru[workspace_id] = rt
self.idle_lru.move_to_end(workspace_id)
suspend_process_tree(rt.process)
rt._suspended = True
while len(self._idle_lru) > MAX_IDLE_RUNTIMES:
_, old_rt = self._idle_lru.popitem(last=False)
rt.p_suspended = True
while len(self.idle_lru) > MAX_IDLE_RUNTIMES:
_, old_rt = self.idle_lru.popitem(last=False)
# Reaping a stopped process: SIGCONT first so the
# SIGTERM in stop() can be delivered cleanly (a
# SIGSTOP'd process can't run its own shutdown).
@@ -647,7 +647,7 @@ class AppRuntimeManager:
except Exception:
logger.exception("failed to reap idle runtime %s", workspace_id)
if to_idle is not None:
logger.debug("workspace %s idled (LRU size now %d)", workspace_id, len(self._idle_lru))
logger.debug("workspace %s idled (LRU size now %d)", workspace_id, len(self.idle_lru))
def get(self, workspace_id: str) -> Optional[AppRuntime]:
# Active subscribers see the live runtime; idle-pool members
@@ -656,7 +656,7 @@ class AppRuntimeManager:
rt = self.runtimes.get(workspace_id)
if rt is not None:
return rt
return self._idle_lru.get(workspace_id)
return self.idle_lru.get(workspace_id)
def drain_errors_for_path(self, file_path: str) -> list[str]:
"""If `file_path` falls under one of the live workspace
@@ -675,7 +675,7 @@ class AppRuntimeManager:
# navigated away from the workspace mid-build, but the agent
# could still be editing files; the LRU keeps the runtime alive
# for ~3 idle slots.
for rt in (*self.runtimes.values(), *self._idle_lru.values()):
for rt in (*self.runtimes.values(), *self.idle_lru.values()):
try:
ws_root = os.path.abspath(rt.workspace_path)
except Exception:
@@ -685,18 +685,18 @@ class AppRuntimeManager:
return []
def get_render_state_for_workspace(self, workspace_id: str) -> tuple[Optional[str], str]:
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
if rt is None:
return None, ""
return rt.render_state, rt.render_error_text
def reset_render_state_for_workspace(self, workspace_id: str) -> None:
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
if rt is not None:
rt.reset_render_state()
async def restart(self, workspace_id: str, workspace_path: Optional[str] = None) -> Optional[AppRuntime]:
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
if rt is None:
return None
if workspace_path:
@@ -718,12 +718,12 @@ class AppRuntimeManager:
victims: list[AppRuntime] = []
for rt in list(self.runtimes.values()):
victims.append(rt)
for rt in list(self._idle_lru.values()):
for rt in list(self.idle_lru.values()):
resume_process_tree(rt.process)
victims.append(rt)
self.runtimes.clear()
self._idle_lru.clear()
self._attached.clear()
self.idle_lru.clear()
self.p_attached.clear()
if not victims:
return 0
await asyncio.gather(
+2 -2
View File
@@ -41,10 +41,10 @@ def p_created_with() -> str:
class p_Ctx:
def __init__(self, local_to_bundle: dict[tuple, str]):
self._m = local_to_bundle
self.p_m = local_to_bundle
def bundle_id_for(self, etype: EntityType, local_id: str) -> str | None:
return self._m.get((etype, local_id))
return self.p_m.get((etype, local_id))
# ---------- export ----------
+4 -4
View File
@@ -19,7 +19,7 @@ class DashboardExportable:
def __init__(self, did: str, name: str, data: dict):
self.local_id = did
self.name = name
self._data = data
self.p_data = data
@classmethod
def load(cls, local_id: str) -> "DashboardExportable | None":
@@ -29,7 +29,7 @@ class DashboardExportable:
return cls(local_id, data.get("name") or "Dashboard", data)
def serialize(self, ctx: ExportContext) -> dict:
layout = dict(self._data.get("layout") or {})
layout = dict(self.p_data.get("layout") or {})
cards = {}
for sid, card in (layout.get("cards") or {}).items():
bid = ctx.bundle_id_for(EntityType.session, sid)
@@ -53,7 +53,7 @@ class DashboardExportable:
c["spawned_by"] = ctx.bundle_id_for(EntityType.session, spawn) if spawn else None
browser_cards[bkey] = c
expanded = [b for b in (ctx.bundle_id_for(EntityType.session, s) for s in (layout.get("expanded_session_ids") or [])) if b]
return {"name": self._data.get("name") or "Dashboard", "layout": {
return {"name": self.p_data.get("name") or "Dashboard", "layout": {
**layout, "cards": cards, "view_cards": view_cards,
"browser_cards": browser_cards, "notes": layout.get("notes") or {},
"expanded_session_ids": expanded,
@@ -63,7 +63,7 @@ class DashboardExportable:
return {}
def dependencies(self) -> list[DepRef]:
layout = self._data.get("layout") or {}
layout = self.p_data.get("layout") or {}
deps = [DepRef(EntityType.session, sid, "has_agent") for sid in (layout.get("cards") or {})]
deps += [DepRef(EntityType.app, oid, "has_app") for oid in (layout.get("view_cards") or {})]
return deps
+2 -2
View File
@@ -19,7 +19,7 @@ class ModeExportable:
def __init__(self, mode_id: str, name: str, data: dict):
self.local_id = mode_id
self.name = name
self._data = data
self.p_data = data
@classmethod
def load(cls, local_id: str) -> "ModeExportable | None":
@@ -33,7 +33,7 @@ class ModeExportable:
return cls(local_id, d.get("name") or local_id, d)
def serialize(self, ctx: ExportContext) -> dict:
return {k: v for k, v in self._data.items() if k not in P_DROP}
return {k: v for k, v in self.p_data.items() if k not in P_DROP}
def files(self) -> dict[str, bytes]:
return {}
+6 -6
View File
@@ -32,7 +32,7 @@ class SessionExportable:
def __init__(self, sid: str, name: str, data: dict):
self.local_id = sid
self.name = name
self._data = data
self.p_data = data
@classmethod
def load(cls, local_id: str) -> "SessionExportable | None":
@@ -52,31 +52,31 @@ class SessionExportable:
return cls(local_id, d.get("name") or "Agent", d)
def serialize(self, ctx: ExportContext) -> dict:
return {k: self._data.get(k) for k in P_KEEP if k in self._data}
return {k: self.p_data.get(k) for k in P_KEEP if k in self.p_data}
def files(self) -> dict[str, bytes]:
return {}
def dependencies(self) -> list[DepRef]:
mode = self._data.get("mode")
mode = self.p_data.get("mode")
if mode and mode not in P_BUILTIN_MODES:
return [DepRef(EntityType.mode, mode, "uses_mode")]
return []
def requirements(self) -> list[Requirement]:
reqs: list[Requirement] = []
for mcp in self._data.get("active_mcps") or []:
for mcp in self.p_data.get("active_mcps") or []:
reqs.append(Requirement(
kind=RequirementKind.mcp_action, key=mcp, label=mcp,
detail="An agent here uses this action.",
))
mode = self._data.get("mode") or "agent"
mode = self.p_data.get("mode") or "agent"
if mode in P_BUILTIN_MODES and mode != "agent":
reqs.append(Requirement(
kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode",
detail="A built-in mode an agent runs in.",
))
provider = self._data.get("provider") or "anthropic"
provider = self.p_data.get("provider") or "anthropic"
reqs.append(Requirement(
kind=RequirementKind.api_key, key=provider, label=f"A {provider} model",
detail="Set up this provider so the agents can run.",
+4 -4
View File
@@ -20,8 +20,8 @@ class SkillExportable:
def __init__(self, local_id: str, name: str, payload: dict, files: dict[str, bytes] | None = None):
self.local_id = local_id
self.name = name
self._payload = payload
self._files = files or {}
self.payload = payload
self.p_files = files or {}
@classmethod
def load(cls, local_id: str) -> "SkillExportable | None":
@@ -46,10 +46,10 @@ class SkillExportable:
return cls(local_id, name, payload, files)
def serialize(self, ctx: ExportContext) -> dict:
return dict(self._payload)
return dict(self.payload)
def files(self) -> dict[str, bytes]:
return dict(self._files)
return dict(self.p_files)
def dependencies(self) -> list[DepRef]:
return []
+5 -5
View File
@@ -48,7 +48,7 @@ class WorkflowExportable:
def __init__(self, local_id: str, name: str, data: dict):
self.local_id = local_id
self.name = name
self._data = data
self.p_data = data
@classmethod
def load(cls, local_id: str) -> "WorkflowExportable | None":
@@ -62,7 +62,7 @@ class WorkflowExportable:
return cls(local_id, data.get("title") or "Untitled workflow", data)
def serialize(self, ctx: ExportContext) -> dict:
return sanitize_workflow(self._data)
return sanitize_workflow(self.p_data)
def files(self) -> dict[str, bytes]:
return {}
@@ -72,18 +72,18 @@ class WorkflowExportable:
def requirements(self) -> list[Requirement]:
reqs: list[Requirement] = []
for name in (self._data.get("actions") or {}).get("configured_sets") or []:
for name in (self.p_data.get("actions") or {}).get("configured_sets") or []:
reqs.append(Requirement(
kind=RequirementKind.mcp_action, key=name, label=name,
detail="This workflow uses this action.",
))
mode = self._data.get("mode") or "agent"
mode = self.p_data.get("mode") or "agent"
if mode in P_BUILTIN_MODES and mode != "agent":
reqs.append(Requirement(
kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode",
detail="A built-in mode this workflow runs in.",
))
provider = self._data.get("provider") or "anthropic"
provider = self.p_data.get("provider") or "anthropic"
reqs.append(Requirement(
kind=RequirementKind.api_key, key=provider, label=f"A {provider} model",
detail="Set up this provider to run the workflow.",
+3 -3
View File
@@ -27,13 +27,13 @@ class RemapTable:
"""bundle_id -> fresh local id, filled as import walks entities leaves-first."""
def __init__(self) -> None:
self._m: dict[str, str] = {}
self.p_m: dict[str, str] = {}
def assign(self, bundle_id: str, local_id: str) -> None:
self._m[bundle_id] = local_id
self.p_m[bundle_id] = local_id
def local(self, bundle_id: str) -> str | None:
return self._m.get(bundle_id)
return self.p_m.get(bundle_id)
@runtime_checkable
+2 -2
View File
@@ -71,10 +71,10 @@ def test_audit_fires_every_n_finished_tasks(monkeypatch, tmp_path):
class p_SyncThread:
def __init__(self, target=None, **kw):
self._t = target
self.p_t = target
def start(self):
self._t()
self.p_t()
monkeypatch.setattr(m.threading, "Thread", p_SyncThread)
log = [{"tool": "BrowserClickIndex", "elapsed_ms": 5, "result_summary": "ok"}]
+5 -5
View File
@@ -20,16 +20,16 @@ class p_FakeClient:
"""Minimal Anthropic-shaped client: client.messages.create(...)."""
def __init__(self, resp=None, raise_exc=None):
self._resp = resp
self._raise = raise_exc
self.p_resp = resp
self.p_raise = raise_exc
self.calls = []
self.messages = self
async def create(self, **kwargs):
self.calls.append(kwargs)
if self._raise:
raise self._raise
return self._resp
if self.p_raise:
raise self.p_raise
return self.p_resp
def test_returns_extracted_guidance_and_assembles_prompt():
+1 -1
View File
@@ -286,7 +286,7 @@ def test_terminal_event_visible_after_full_eviction(p_patch_persist_dir):
# Simulate a process restart: clear the in-memory ring buffer
# but keep the persisted terminal file.
seq_log._per_session.pop(sid, None)
seq_log.per_session.pop(sid, None)
with client.websocket_connect(f"/ws/agents/{sid}") as ws:
ws.send_text(json.dumps({"event": "client:hello", "data": {"last_seq": 0, "connection_uuid": "c1"}}))
@@ -146,7 +146,7 @@ async def test_stop_all_kills_active():
await asyncio.sleep(0.05)
else:
raise AssertionError(f"port {port} not released after stop_all")
assert not m.runtimes and not m._idle_lru, "manager should be empty after stop_all"
assert not m.runtimes and not m.idle_lru, "manager should be empty after stop_all"
print("PASS test_stop_all_kills_active")
@@ -161,7 +161,7 @@ async def test_stop_all_kills_idle():
# Detach -> moves into LRU + SIGSTOP'd. If stop_all forgets to
# SIGCONT before SIGTERM, the kill queues and the process hangs.
await m.detach("ws-idle")
assert "ws-idle" in m._idle_lru, "should be in idle LRU"
assert "ws-idle" in m.idle_lru, "should be in idle LRU"
# Confirm the process is suspended (T state on Linux, T on darwin).
# Skip the OS check; just rely on the eventual kill working.
killed = await m.stop_all()
+1 -1
View File
@@ -168,7 +168,7 @@ def test_swarm_export_folder_skill_carries_supporting_files(skills_dir):
files = exp.files()
assert "scripts/go.py" in files
assert files["scripts/go.py"] == b"print(1)"
assert exp._payload["content"] == "render"
assert exp.payload["content"] == "render"
def test_swarm_import_writes_folder_when_files_present(skills_dir):
+2 -2
View File
@@ -28,7 +28,7 @@ class p_FakeResp:
class p_FakeClient:
"""Stands in for httpx.AsyncClient; returns a canned response."""
def __init__(self, resp: p_FakeResp):
self._resp = resp
self.p_resp = resp
async def __aenter__(self):
return self
@@ -37,7 +37,7 @@ class p_FakeClient:
return False
async def post(self, *a, **k):
return self._resp
return self.p_resp
def p_patch_client(monkeypatch, resp: p_FakeResp):