From 231ca2f19961653f81229bdd8afec4a7e8aa992c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 26 Oct 2026 01:03:32 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9:=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=20=D0=BE=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B5=D0=B6=D0=BD=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B5=20(#124,=20#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/shell/activity/ActivityProvider.tsx | 3 +- web/src/shell/activity/activity.test.tsx | 34 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/web/src/shell/activity/ActivityProvider.tsx b/web/src/shell/activity/ActivityProvider.tsx index bf64571..e668973 100644 --- a/web/src/shell/activity/ActivityProvider.tsx +++ b/web/src/shell/activity/ActivityProvider.tsx @@ -124,7 +124,8 @@ function useTracker(m: (typeof TRACKED)[number], { qc, project, pollMs, agents } }, [qc, status, setActiveNow]); useEffect(() => { - if (!active) return; + // #116: ответ обрабатывается, только если отслеживаемый пакет — тот же объект, что в activeRef (смена проекта обнуляет его синхронно) + if (!active || activeRef.current !== active) return; const b = batch.data, e = batch.error; if (b && b.id === active.id && !batch.isError) { if (terminal(b)) { diff --git a/web/src/shell/activity/activity.test.tsx b/web/src/shell/activity/activity.test.tsx index 8e0e12a..d0884b6 100644 --- a/web/src/shell/activity/activity.test.tsx +++ b/web/src/shell/activity/activity.test.tsx @@ -337,6 +337,40 @@ describe("ActivityProvider", () => { expect(m.now().jira.event).toMatchObject({ kind: "batch", id: 5 }); }); + it("#116: ответ опроса прежнего проекта, пришедший в коммите смены проекта, не возвращает событие", async () => { + const late = deferred(); + let polls = 0; + hub({ + ...listing((mod, project) => (mod === "wiki" && project === "AWPCA" ? [item(9, "wiki")] : [])), + "GET /batches/:id": async () => { + if (++polls > 1) await late.promise; // первый ответ показан сразу, следующий держится до смены проекта + return batch("running", { type: "wiki_sync" }, { id: 9, module: "wiki" }); + }, + }); + const m = mount({ pollMs: 5 }); + await waitFor(() => expect(m.now().wiki.event).toMatchObject({ kind: "batch", id: 9 })); + await waitFor(() => expect(polls).toBeGreaterThanOrEqual(2)); // повторный опрос ушёл и висит + // смена проекта и ответ опроса — одним коммитом: оба обновления копятся в act и рендерятся вместе на выходе из него + await act(async () => { m.setProject("STOCKS_DRRK"); late.resolve(); await sleep(20); }); + await waitFor(() => expect(m.now().wiki.event).toBeNull()); + await sleep(50); + expect(m.now().wiki.event).toBeNull(); + expect(m.now().wiki.busy).toBe(false); + }); + + it("#116: A → B → A — после возврата на AWPCA незавершённый пакет 9 снова подхватывается", async () => { + hub({ + ...listing((mod, project) => (mod === "wiki" && project === "AWPCA" ? [item(9, "wiki")] : [])), + ...byId({ 9: "wiki_sync" }, () => "running"), + }); + const m = mount(); + await waitFor(() => expect(m.now().wiki.event).toMatchObject({ kind: "batch", id: 9 })); + m.setProject("STOCKS_DRRK"); + await waitFor(() => expect(m.now().wiki.event).toBeNull()); + m.setProject("AWPCA"); + await waitFor(() => expect(m.now().wiki.event).toMatchObject({ kind: "batch", id: 9 })); + }); + it("M-2: пакет, отданный track для нового проекта, переживает смену проекта на него; чужой — сбрасывается", async () => { hub({ ...byId({ 9: "wiki_sync" }, () => "running") }); const m = mount();