fix(web-modules): подхват после 409 заменяет чужой отслеживаемый пакет (T3.I1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
112720ab1d
commit
4dbda116c0
@ -132,3 +132,4 @@ type HubModule = {
|
||||
| 28 | 24.10.2026 | Task 3: wiki `refresh.start` приводит ответ `{batch_id}` к `{id}`; `track()` перехватывает отслеживание, если текущий пакет не «свой» (раньше — только если ничего не отслеживалось) | контракт ядра единый `{id}`; пакет нового проекта важнее чужого | отдельная форма ответа / прежняя проверка |
|
||||
| 29 | 24.10.2026 | Task 3: сводка `useActivitySummary`: error > offline > busy > online | ошибка и офлайн агента важнее, чем «идёт пакет» | поменять порядок |
|
||||
| 30 | 24.10.2026 | Task 3: временный `shell/activity/StatusBlock.tsx` (строки модулей и строка событий узкого экрана) вместо `SidebarStatus`/`TopbarNotice` модулей; Task 4 заменяет его; страница вики берёт действие из трекера, уведомление об ошибке постановки (`syncError`) убрано — ошибка видна событием модуля | оболочка должна работать и набор быть зелёным до Task 4 | вернуть Notice на странице вики |
|
||||
| 31 | 24.10.2026 | Task 3, fix T3.I1: подхват после 409 (`pickup(true)`) заменяет отслеживаемый чужой пакет первым (самым старым) другим незавершённым пакетом модуля; если другого нет — чужой остаётся | детали 409 (`batch_id`) клиент не разбирает, а список без команд не различает типы | брать `batch_id` из `ApiError.detail` |
|
||||
|
||||
@ -49,16 +49,19 @@ function useTracker(m: (typeof TRACKED)[number], { qc, project, pollMs, agents }
|
||||
}, []);
|
||||
|
||||
// M-3: подхватить незавершённый пакет модуля (самый старый), если ничего не отслеживается
|
||||
const pickup = useCallback(() => {
|
||||
// replaceForeign — после 409: идущий пакет важнее чужого отслеживаемого (T3.I1), чужой пропускается при выборе
|
||||
const pickup = useCallback((replaceForeign = false) => {
|
||||
if (!refresh) return;
|
||||
if (scoped && projectRef.current === null) return;
|
||||
if (activeRef.current || startingRef.current || picking.current) return;
|
||||
const blocked = () => !!activeRef.current && !(replaceForeign && !activeRef.current.own);
|
||||
if (blocked() || startingRef.current || picking.current) return;
|
||||
picking.current = true;
|
||||
const gen = generation.current;
|
||||
(async () => {
|
||||
const r = await api.get<{ batches: Batch[] }>("/batches", { module: name, limit: PICKUP_LIMIT, project: scoped ? projectRef.current : undefined });
|
||||
if (gen !== generation.current || activeRef.current || startingRef.current) return;
|
||||
const open = r.batches.filter((b) => b.status === "queued" || b.status === "running").sort((a, b) => a.id - b.id)[0];
|
||||
if (gen !== generation.current || blocked() || startingRef.current) return;
|
||||
const skip = replaceForeign ? activeRef.current?.id : undefined;
|
||||
const open = r.batches.filter((b) => (b.status === "queued" || b.status === "running") && b.id !== skip).sort((a, b) => a.id - b.id)[0];
|
||||
if (!open) return;
|
||||
failures.current = 0;
|
||||
setActiveNow({ id: open.id, agentOffline: offlineNow(), own: false });
|
||||
@ -80,7 +83,7 @@ function useTracker(m: (typeof TRACKED)[number], { qc, project, pollMs, agents }
|
||||
}
|
||||
}
|
||||
pickup();
|
||||
const t = setInterval(pickup, PICKUP_MS);
|
||||
const t = setInterval(() => pickup(), PICKUP_MS);
|
||||
return () => clearInterval(t);
|
||||
}, [project, scoped, pickup, setActiveNow]);
|
||||
|
||||
@ -182,7 +185,7 @@ function useTracker(m: (typeof TRACKED)[number], { qc, project, pollMs, agents }
|
||||
} finally {
|
||||
startingRef.current = false;
|
||||
setStarting(false);
|
||||
if (conflict) pickup();
|
||||
if (conflict) pickup(true);
|
||||
}
|
||||
}, [refresh, scoped, status, qc, report, pickup, setActiveNow]);
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ describe("ActivityProvider", () => {
|
||||
expect(getToken()).toBe("new-tok");
|
||||
});
|
||||
|
||||
it("T4.1: упавшее перечитывание снимка после своего пакета не гасит подхват следующей независимой загрузки", async () => {
|
||||
it("T4.1: упавшее перечитывание снимка после своего пакета не мешает подхвату следующего пакета через 60 с", async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
let boardFails = false, listed: unknown[] = [];
|
||||
const h = hub({
|
||||
@ -352,6 +352,21 @@ describe("ActivityProvider", () => {
|
||||
expect(h.count("POST /wiki/sync")).toBe(1);
|
||||
});
|
||||
|
||||
it("wiki 409 with foreign batch tracked switches to running own batch: чужой пакет не мешает подхвату идущего", async () => {
|
||||
let conflict = false;
|
||||
hub({
|
||||
"POST /wiki/sync": () => { conflict = true; return json(409, { error: "sync_in_progress", detail: { batch_id: 9 } }); },
|
||||
...listing((mod) => (mod === "wiki" ? (conflict ? [item(5, "wiki"), item(9, "wiki")] : [item(5, "wiki")]) : [])),
|
||||
...byId({ 5: "ensure_root", 9: "wiki_sync" }, () => "running"),
|
||||
});
|
||||
const m = mount();
|
||||
await waitFor(() => expect(m.now().wiki.event).toMatchObject({ kind: "batch", id: 5 }));
|
||||
expect(m.now().wiki.busy).toBe(false);
|
||||
await act(() => m.now().wiki.start());
|
||||
await waitFor(() => expect(m.now().wiki.event).toMatchObject({ kind: "batch", id: 9 }));
|
||||
await waitFor(() => expect(m.now().wiki.busy).toBe(true));
|
||||
});
|
||||
|
||||
it("wiki: «Обновить вики» шлёт проект и автора viewer; пакет — из batch_id ответа", async () => {
|
||||
let body: unknown;
|
||||
hub({ "POST /wiki/sync": (_u, b) => { body = b; return json(201, { batch_id: 11 }); }, ...byId({ 11: "wiki_sync" }, () => "running") });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user