merge(custom-cli): progress/tools restaurés dès la 2e sollicitation quand le provider réutilise le même progressId — #157 (QA verte)

This commit is contained in:
2026-08-24 09:43:33 +02:00
7 changed files with 177 additions and 17 deletions

View File

@ -1,11 +1,47 @@
---
issueRef: "#157"
version: 19
version: 21
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedAt: 1786380032724
updatedAt: 1787556963342
---
# Journal d'itération (le plus récent en haut)
## Itération 5 — 2026-08-24 — Réouverture ciblée sur régression 2e sollicitation / progressId réutilisé entre tours
### Nouveau signal utilisateur
Dans la custom CLI, à partir de la seconde sollicitation sur une même conversation, les messages `progress` et l'activité tools ne s'affichent plus comme une nouvelle activité de tour.
### Repro QA validée
QA a trouvé une repro automatisée fiable dans `frontend/src/features/agents/CustomAgentChatView.test.tsx`.
- scénario minimal: 2 requêtes successives dans la même conversation
- le provider réémet un `progressId` identique entre les 2 tours (ex. `codex:item:call_1`)
- résultat actuel: la carte du 2e tour remplace/écrase celle du 1er au lieu de créer une nouvelle activité
### Preuve rouge actuelle
Commande QA:
`cd /home/anthony/Documents/Projects/IdeA/frontend && npm test -- --run src/features/agents/CustomAgentChatView.test.tsx -t "keeps second-turn progress visible when the provider reuses the same progressId across turns"`
Verdict actuel: **ROUGE**.
### Hypothèse active
Le point suspect principal est le dédoublonnage global par `progressId` dans `CustomAgentChatView.tsx`, alors que l'identité visuelle attendue doit rester bornée au tour/callback courant et non à tout l'historique de conversation.
### Cycle relancé
- UX: valider l'attendu visuel/fonctionnel sur la conservation des cartes de tours successifs.
- Architect: cadrer la règle d'identité/dédoublonnage côté surface.
- Git: décider de la branche locale de correction.
- DevFrontend: implémenter le fix avec le test QA rouge comme garde-fou.
- QA: rejouer la repro puis la suite ciblée; si rouge, reboucler.
---
## Itération 4 — 2026-08-10 — Correctif livré, QA verte ciblée, merge local sur `develop`, attente de validation AppImage live
### Diagnostic final du cycle

View File

@ -2,7 +2,7 @@
id: "d49aaae5-0d03-43fe-a51c-2ab5e921045c"
number: 157
title: "CLI custom: surface all available agent progress, MCP activity, and inter-agent delegation flow"
status: "qa"
status: "inProgress"
priority: "high"
sprint: null
links: [{"target":"#156","kind":"dependsOn"}]
@ -11,7 +11,7 @@ attachments: []
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
createdAt: 1785965383914
updatedAt: 1786380032724
version: 19
updatedAt: 1787556963342
version: 21
---
Régression/remontée utilisateur (profil OpenCode/GLM) sur la CLI custom : aucun message de progress ne s'affiche pendant le traitement de l'agent, et aucune bulle/carte d'appel de tool MCP n'apparaît quand l'agent invoque un outil MCP. Périmètre initial (surfacer progress + activité MCP + délégations inter-agent) à valider à nouveau sur le profil OpenCode.

View File

@ -1,8 +1,8 @@
---
issueRef: "#253"
version: 2
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
updatedAt: 1787551753848
version: 4
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedAt: 1787552434207
---
# Git — décision de branche (2026-08-24)
@ -12,3 +12,15 @@ updatedAt: 1787551753848
- Interdits : aucun push distant ; pas de merge/rebase vers `develop` sans l'agent Git. Fusion locale décidée par Git après QA verte.
- `scripts/restart-idea-over-ssh.sh` reste non suivi — ne pas le committer.
- Contexte : symptômes initiaux documentés dans le carnet #252 (section « Signalement QA » du 2026-08-24).
# Git — commits, merge et tag de build (2026-08-24, QA verte vérifiée)
Commits sur `fix/253-opencode-tests-parallel-flakiness` :
- `cd70a3c` test(opencode): fiabilisation de la suite en parallèle — écriture atomique des fixtures, gates par fichier au lieu de sleep, runtime dédié par test, timeout 5s — #253 (QA verte)
- `c05b671` chore(tickets): synchro du store local — carnet et statut #253
Merge local : **effectué** — `d9af4b5` merge(opencode) `--no-ff` dans `develop`.
Tag de build AppImage : **`v0.33.0`** (annoté) sur le commit de merge `d9af4b5` — build sur `develop` → incrément de `y` (dernier tag `v0.32.0`). **Branche finale de build : `develop` @ `d9af4b5`.** Aucun push (branche, tag, tout reste local).
Re-vérification indépendante par Git avant merge : 3 runs parallèles + 1 run sériel (`--test-threads=1`) de `cargo test -p infrastructure session::opencode` → 21 passed partout ; durée de la suite passée de ~1 s à ~0,06 s (suppression des `sleep` au profit de gates par fichier). Stratégie retenue (par DevBackend, validée par Git) : écriture atomique des scripts fixtures (tmp+fsync+rename), synchronisation déterministe par fichier de release au lieu de `sleep`, exécution des fixtures via `sh <script>`, runtime tokio dédié par test au lieu du runtime partagé `#[tokio::test]`, timeout d'événements live porté à 5 s (`LIVE_EVENT_TIMEOUT`). Couverture inchangée — mêmes 21 scénarios.

View File

@ -2,16 +2,16 @@
id: "1dc63ee0-0963-44c2-a036-6bb0f9ad4e95"
number: 253
title: "Fiabiliser la suite de tests OpenCode flaky en exécution parallèle"
status: "inProgress"
status: "closed"
priority: "high"
sprint: null
links: []
agentRefs: []
attachments: []
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
createdAt: 1787551681631
updatedAt: 1787551753848
version: 2
updatedAt: 1787552434207
version: 4
---
Des tests anciens de `session::opencode` échouent de façon intermittente en exécution parallèle, alors qu'ils passent en série (`--test-threads=1`). Symptômes remontés pendant le ticket #252: tests différents à chaque run, concernés notamment `seeded_session_ignores_and_does_not_capture_other_session_events`, `seeded_or_captured_session_id_is_passed_as_opencode_session_arg`, etc. Objectif: supprimer la flakiness sans dégrader la couverture, obtenir une exécution verte en parallèle sur la suite ciblée, et documenter la cause racine ainsi que la stratégie retenue.

View File

@ -2058,7 +2058,7 @@
"issueRef": "#157",
"path": "157",
"title": "CLI custom: surface all available agent progress, MCP activity, and inter-agent delegation flow",
"status": "qa",
"status": "inProgress",
"priority": "high",
"sprint": null,
"assignedAgentIds": [
@ -2068,7 +2068,7 @@
"kind": "agent",
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
},
"updatedAt": 1786380032724
"updatedAt": 1787556963342
},
{
"issueRef": "#158",
@ -3411,7 +3411,7 @@
"issueRef": "#253",
"path": "253",
"title": "Fiabiliser la suite de tests OpenCode flaky en exécution parallèle",
"status": "inProgress",
"status": "closed",
"priority": "high",
"sprint": null,
"assignedAgentIds": [],
@ -3419,7 +3419,7 @@
"kind": "agent",
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
},
"updatedAt": 1787551753848
"updatedAt": 1787552434207
}
]
}

View File

@ -1764,6 +1764,118 @@ describe("CustomAgentChatView", () => {
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(2);
});
it("keeps second-turn progress visible when the provider reuses the same progressId across turns", async () => {
const harness = createStreamingAgentHarness();
render(
<DIProvider
gateways={{
agent: harness.agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="chat-session-1"
conversationId="conversation-1"
onSessionId={vi.fn()}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() =>
expect(harness.agent.reattachAgentChat).toHaveBeenCalledWith(
"chat-session-1",
expect.any(Function),
),
);
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "first request" },
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(harness.emitterCount()).toBe(1));
act(() => {
harness.emit(0, {
kind: "progress",
progress: {
progressId: "codex:item:call_1",
source: "providerNative",
kind: "tool",
stage: "started",
label: "shell",
toolName: "shell",
text: "Running first request",
},
});
harness.emit(0, {
kind: "progress",
progress: {
progressId: "codex:item:call_1",
source: "providerNative",
kind: "tool",
stage: "completed",
label: "shell",
toolName: "shell",
text: "Done first request",
},
});
harness.emit(0, { kind: "final", content: "done first request" });
});
await screen.findByText("done first request");
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "second request" },
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(harness.emitterCount()).toBe(2));
act(() => {
harness.emit(1, {
kind: "progress",
progress: {
progressId: "codex:item:call_1",
source: "providerNative",
kind: "tool",
stage: "started",
label: "shell",
toolName: "shell",
text: "Running second request",
},
});
});
expect(screen.getByText("Running second request")).toBeTruthy();
act(() => {
harness.emit(1, {
kind: "progress",
progress: {
progressId: "codex:item:call_1",
source: "providerNative",
kind: "tool",
stage: "completed",
label: "shell",
toolName: "shell",
text: "Done second request",
},
});
harness.emit(1, { kind: "final", content: "done second request" });
});
await screen.findByText("done second request");
expect(screen.getByText("Done first request")).toBeTruthy();
expect(screen.getByText("Done second request")).toBeTruthy();
expect(screen.getAllByTestId("chat-tool-progress")).toHaveLength(2);
});
it("ignores a late first-turn progress chunk but still shows the real second-turn progress from the next callback", async () => {
const harness = createStreamingAgentHarness();

View File

@ -325,7 +325,7 @@ function progressKey(progress: ReplyProgress): string {
function progressUpsertKey(progress: ReplyProgress, scope: number): string {
const progressId = progress.progressId?.trim();
if (progressId) return progressId;
if (progressId) return `progress:${scope}:${progressId}`;
return `legacy:${scope}:${progressDisplayKey(progress)}`;
}