Branch feat/orch-fase3-c · commit a7d430fe9ee3ae1fd1d86ff0083318fc1e96005b
(fix(dashboard): carry conductor requirements and focus the repeated task, corpo com Refs #12639).
hubTaskSchemaganhourequirements: z.object({ cli: z.string().nullish(), model: z.string().nullish() }).nullish().catch(null). O.catch(null)é deliberado: o MESMO schema parseia a lista emgetFleetSnapshot(z.array(hubTaskSchema).parse(rawTasks)) — sem ele, umrequirementsde formato inesperado numa única task faria oparseinteiro lançar e o snapshot cair para{offline: true}.ConductorTaskDetailganhoucli: string | nullemodel: string | null(com JSDoc);getConductorTaskDetailpreenche comt.requirements?.cli ?? null/?? null.- Origem do shape: é exatamente o objeto que
createConductorTask(mesmo arquivo, linhas ~238-241) envia para o hub (body.requirements = { cli?, model? }) — não foi inventado.
repeatReqForConductoragora enviacli: d.cli ?? undefined/model: d.model ?? undefined.undefined, nuncanull— o Zod real da rota (src/app/api/conductor/tasks/route.ts) temcli: z.string().optional()/model: z.string().optional(), enulldaria 400.JSON.stringifydescarta as chavesundefined, então o corpo sai idêntico ao de hoje quando não há requirement. Os dois campos são independentes (um pode existir sem o outro) — há teste para esse caso.jsonRpcErrorCodedeixou de lerres.json()e passou a receber o corpo já parseado; novoreadJsonBody(res)lê o corpo UMA vez (um body só pode ser consumido uma vez, e agora o id da task nova sai do mesmo corpo). Body não-JSON/vazio ⇒null, sem virar falha (mantém o contrato anterior: "corpo ilegível não é evidência de falha, o status já valeu").performActionpassou a devolver{ ok, body }em vez deboolean.- Novo
export function newNodeIdFrom(node, body): string | null— extrai o id da task criada e devolve já no formato do canvas (com prefixo), porquesetParams({ node })casa contrasnapshot.nodes.find(n => n.id === nodeId)e os ids emmergeSnapshot.tssão prefixados:- conductor:
body.task_id→conductor:task:<id> - cloud-agent:
body.data.id→cloud-agent:<id> - a2a:
body.result.task.id→a2a:<id>(envelope real de/a2amessage/sendv0.3) Leitura defensiva: id ausente/vazio/não-string ⇒null(nunca um prefixo pelado), corpo não-objeto ⇒null, nunca lança.
- conductor:
- No hook:
approve/cancelcontinuamPromise<boolean>(viarunBooleanAction) — de propósito: eles agem sobre a task já aberta e a resposta do cloud-agent pode ecoar o id da MESMA task, o que faria o painel "navegar" para onde já está. SórepeatdevolveRepeatOutcome { ok, newNodeId }(interface exportada).
RepeatButtonconsomeRepeatOutcomee chamaonActionDone(newNodeId ?? undefined).onActionDonevirou(newNodeId?: string) => voidnoRepeatButton,DrawerActionse no componente exportado.DrawerActions.run()(approve/cancel) segue chamandoonActionDone()sem argumento.
- Novo
onActionDonememoizado:refetch()e, se veio id,setParams({ node: newNodeId }). A aba History renderiza o próprio drawer (HistoryTab.tsx) e não foi tocada — seuonActionDone={() => …}ignora o argumento extra, então o comportamento lá continua idêntico (re-amostranowMs, não navega), como o plano pede.
tests/unit/conductor-routes-auth.test.ts:src/app/api/conductor/tasks/route.tsentrou no arrayROUTES; o regex do proxy ganhou a alternativacreateConductorTask\(.tests/unit/conductor-create-route.test.ts: removido o teste de fonte duplicado (route: requireManagementAuth antes de criar a task no hub), substituído por um comentário apontando para o array.fs/pathcontinuam usados pelo resto do arquivo.tests/unit/ui/orchestrationDrawerRepeat.test.tsx(786 → 971 linhas, cap 1200 — não precisou dividir): 3 describes novos, 6 testes:repeatReqForconductor comcli/model; com ambosnull(corpo idêntico ao atual e sem as stringscli/model); com sóclisetado.newNodeIdFromnas três fontes (prefixado), corpos malformados ⇒null, fonte sem contrato ⇒null.- drawer: repeat de conductor faz POST com
cli/modele chamaonActionDoneexatamente uma vez com"conductor:task:t_new", com o toastrepeatDone.
C3.2 (vermelho) npx vitest run tests/unit/ui/orchestrationDrawerRepeat.test.tsx:
Test Files 1 failed (1)
Tests 6 failed | 29 passed (35)
(falhas: newNodeIdFrom is not a function ×3, corpo sem cli/model ×3 — a última:
expected { …(4) } to match object { cli: 'claude', model: 'sonnet' })
node --import tsx/esm --test tests/unit/conductor-routes-auth.test.ts tests/unit/conductor-create-route.test.ts já passou nesse ponto
(ℹ pass 11 / fail 0) — divergência menor com o plano: a mudança do array ROUTES é ampliação
de cobertura + de-duplicação, não a prova de um defeito, então ela não tinha como "falhar antes".
A rota já chamava requireManagementAuth corretamente.
C3.4 (verde):
$ npx vitest run tests/unit/ui/orchestrationDrawerRepeat.test.tsx
Test Files 1 passed (1)
Tests 35 passed (35)
$ node --import tsx/esm --test tests/unit/conductor-routes-auth.test.ts tests/unit/conductor-create-route.test.ts tests/unit/client-bundle-no-server-only-10692.test.ts
✔ src/app/api/conductor/tasks/route.ts: requireManagementAuth antes do proxy ao hub (4.175999ms)
ℹ tests 12
ℹ pass 12
ℹ fail 0
$ npx vitest run tests/unit/ui/
Test Files 210 passed (210)
Tests 1200 passed (1200)
Duration 179.85s
$ node --import tsx/esm --test tests/unit/conductor-hub-proxy.test.ts tests/unit/conductor-fleet-mirror.test.ts tests/unit/conductor-fleet-route.test.ts tests/unit/conductor-delegate.test.ts
ℹ tests 17
ℹ pass 17
ℹ fail 0
$ npm run typecheck:core
EXIT=0 (sem saída de erro)
$ npm run check:dashboard-typecheck
[dashboard-typecheck] OK — 206 pre-existing error(s), all within frozen baseline.
EXIT=0
$ npx eslint <os 7 arquivos alterados>
EXIT=0 (zero erros, zero warnings)
$ npm run check:file-size
[file-size] OK — 136 arquivos congelados, cap 1200 para novos (4588 arquivos verificados)
[test-file-size] OK — 40 test files congelados, testCap 1200 para novos (5495 test files verificados)
Hooks do husky rodaram no commit (lint-staged + docs-sync + any-budget + tracked-artifacts, todos PASS);
nada de --no-verify.
newNodeIdFromprefixado (não previsto explicitamente no plano). O plano diz "extrair o id da task nova (task_idno Conductor,data.idno Cloud Agent) e chamaronActionDonecom ele". O id CRU não serve parasetParams({ node }): o canvas indexa por id prefixado (conductor:task:<id>,cloud-agent:<id>,a2a:<id>—mergeSnapshot.ts/routeFor). Segui o código:newNodeIdFromjá devolve o id de canvas. O plano não citava o a2a; incluí porque orepeatdo drawer também atende a2a e o envelope (result.task.id) é verificável emsrc/app/a2a/route.ts:250.performAction"devolve o corpo parseado" — só orepeatpropaga o id.approve/cancelficaramPromise<boolean>de propósito (ver item 2 acima): o POST de approve/cancel do cloud-agent pode devolver o id da MESMA task, e navegar para ela seria ruído. Cumprimento do espírito do plano ("o drawer extrai o id da task nova") sem o efeito colateral..catch(null)norequirements. O plano não pedia; adicionei porquehubTaskSchematambém parseia a LISTA de tasks, e um campo novo estrito ali seria um vetor de "snapshot vira offline por causa de um campo estranho de uma task".- A mudança em
conductor-routes-auth.test.tsnão é um teste que falha antes (explicado acima). ConductorTaskDetailestendeFleetTask;cli/modelforam adicionados só no detalhe (não emFleetTask/toFleetTask), porque o repeat lê o DETALHE carregado, não o nó.
changelog.d/fixes/orchestration-followups.md, o comentário na issue #12639 e a bateria completa
(lint total, test:vitest, check:cycles, i18n) continuam pendentes na C4.