瀏覽代碼

fix: reject stale replay before token advance

Problem: A stale replay invocation could advance the shared load token and invalidate an already-running replay for the current session.

Risk: Low. The guard only rejects non-current session IDs before token advancement, with executable reverse-order coverage preserving the Todo49.2 cases.
zhenyu.hu 2 周之前
父節點
當前提交
ac526139d2
共有 2 個文件被更改,包括 47 次插入0 次删除
  1. 3 0
      src/agent_lab/presentation/static/app.js
  2. 44 0
      tests/test_websocket_api.py

+ 3 - 0
src/agent_lab/presentation/static/app.js

@@ -206,6 +206,9 @@ async function loadSelectedSession() {
 }
 
 async function loadSessionReplay(sessionId, options = {}) {
+  if (sessionId !== currentSessionId) {
+    return;
+  }
   const loadToken = ++sessionReplayLoadToken;
   const isCurrentLoad = () => (
     loadToken === sessionReplayLoadToken && sessionId === currentSessionId

+ 44 - 0
tests/test_websocket_api.py

@@ -1695,6 +1695,50 @@ console.log(JSON.stringify(mutations.status));
     assert result == ["Session loaded"]
 
 
+def test_static_session_replay_rejects_stale_invocation_before_token_advance():
+    result = _run_load_session_replay_scenario(
+        """
+currentSessionId = "new";
+const newLoad = loadSessionReplay("new");
+const tokenAfterNew = sessionReplayLoadToken;
+const oldLoad = loadSessionReplay("old");
+const oldResult = await Promise.race([
+  oldLoad.then(() => "returned"),
+  new Promise((resolve) => setTimeout(() => resolve("pending"), 0)),
+]);
+const tokenAfterOld = sessionReplayLoadToken;
+const oldFetchStarted = pending.has("/api/sessions/old");
+resolveReplay("new");
+await newLoad;
+console.log(JSON.stringify({
+  oldResult,
+  tokenAfterNew,
+  tokenAfterOld,
+  oldFetchStarted,
+  mutations,
+  title: sessionTitle.value,
+  selected: sessionList.value,
+}));
+"""
+    )
+
+    assert result == {
+        "oldResult": "returned",
+        "tokenAfterNew": 1,
+        "tokenAfterOld": 1,
+        "oldFetchStarted": False,
+        "mutations": {
+            "workspace": ["new"],
+            "transcript": ["new"],
+            "audit": ["new"],
+            "usage": ["new"],
+            "status": ["Session loaded"],
+        },
+        "title": "new title",
+        "selected": "new",
+    }
+
+
 def test_static_replay_hides_whitespace_assistant_tool_protocol_records():
     js = Path("src/agent_lab/presentation/static/app.js").read_text()
     render_replay = js[