Skip to content

refactor(slm-frontend): derive the code-sync, orchestration and role contracts from the generated schema (#13138) - #13152

Merged
mrveiss merged 3 commits into
Dev_new_guifrom
issue-13138-envelope-enum
Jul 31, 2026
Merged

mrveiss merged 3 commits into
Dev_new_guifrom
issue-13138-envelope-enum

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Closes #13155 — the envelope / enum-widening slice of #13138 — the group the previous pass deferred. #13138 itself stays OPEN: 53 shapes remain (the types/api-responses.ts monitoring long tail and the rest of types/slm.ts), plus the three still blocked on #13145. Partial delivery never closes the parent.

Thinking Path

Measured count first, because the issue's scan and my predecessor's differ. Requiring a real declaration body and excluding names already derived (type X = components['schemas'][…], which a body-requiring regex still matches) gives, on c0a45b77a:

value
generated schemas 309
hand-declared names 414
exact-name collisions 131
— already fully derived 55
— still hand-written 76

76, not the 77 quoted on the issue. The single-unit difference is ThreatSummary: it is derived at types/slm.ts:727, but SecurityView.vue:43 re-declares it locally, so it lands in the hand-written bucket under a per-name rule and in the derived bucket under a per-declaration rule. Worth pinning down, because the next pass will otherwise re-litigate the same number a third time.

The widening direction was the thing to get right. Every status in this group is str server-side with the value set living only in a trailing # comment, so the generated schema says string while the frontend says a 4- or 5-member union. Plain derivation would have widened all of them and thrown away a real guarantee; keeping them as-is would have preserved two provably false claims. So each one was resolved against the backend's actual assignment sites rather than against either type:

  • Backend genuinely unconstrained but exhaustively enumerable (PostSyncAction.category — 4 construction sites; FleetSyncNodeStatus.status; ComponentSyncJobStatus.status) → derive the shape, keep the union, cite the sites.
  • Backend constrained in a way the schema cannot express (ServiceCategoryUpdate.category has pattern="^(autobot|system)$", which openapi-typescript can only render as string) → derive, keep the union: sending anything else is a guaranteed 422.
  • Frontend union simply wrong (UpdateAllJob.status, UpdateAllStage.status) → the finding. Below.

Two schema-shaped traps found the hard way. The generated types are intersected with an additionalProperties index signature, so keyof T is string | number and Omit/Pick silently collapse every named member into the index signature — Omit<ServiceActionRequest, 'force'>.node_id types as unknown, no error at the definition site. Verified with a standalone tsc probe before relying on it. Partial is homomorphic and does preserve them, and plain intersection narrows a widened string correctly, so those are the two tools used here.

The second trap: openapi-typescript emits fields carrying a server-side default as required, which is right for a response and backwards for a request body. ServiceActionRequest.force (default=False) and BulkActionRequest.exclude (default_factory=list) are optional to send; deriving them bare would have forced every call site to pass them. Both use Partial<>.

What Changed

23 shapes derived from components['schemas'][…] across the four composables (76 → 53 by the same scan). Each alias carries the file:line of the backend model it is the response_model/body of.

File Types
composables/useCodeSync.ts PendingNode, PendingNodesResponse, FleetSyncResponse, FleetSyncNodeStatus, FleetSyncJobStatus, ScheduleRunResponse, DriftedFile, FileDriftReport, DriftResolveResponse, DriftResolveJobResponse, ComponentSyncJobStatus, UpdateAllStage, UpdateAllJob
composables/useOrchestration.ts ServiceActionRequest, ServiceMigrateRequest, FleetStatusResponse, BulkActionRequest, BulkActionResponse
composables/useRoles.ts PortInfo, PostSyncAction, NodeActionsResponse, DecommissionRoleInfo
composables/useOrchestrationManagement.ts FleetServiceNodeStatus, ServiceCategoryUpdate

The defect: a partial update-all reports nothing at all

UpdateAllJob.status was declared 'pending' | 'running' | 'completed' | 'failed' | 'already_current'. Since #11511 the backend has a sixth terminal status:

# autobot-slm-backend/api/code_sync.py:4940
job.status = "partial" if skipped else "completed"

set whenever the fleet stage skips a node that is not operational. On the frontend, 'partial' matches neither banner — the failure banner tests status === 'failed' (CodeSyncView.vue:913) and the success banner tests 'completed' | 'already_current' (:921). So the pipeline stopped, the button re-armed, and the user was shown no outcome whatsoever. The backend's own summary — "Updated 2/3 nodes (1 skipped — not operational)" (code_sync.py:4933-4935) — is written to stage.message, which the template never renders anywhere, so that explanation had no route to the screen either.

Compounding it, skipped_fleet_nodes (code_sync.py:4270) was absent from the frontend type entirely, so even a hand-written fix had nothing to count. Deriving UpdateAllJob restored the field and immediately broke the two test fixtures that had been mocking the truncated shape — the same failure mode as #13137's scope picker, where the test encoded the same wrong assumption as the code and so could never have caught it.

Fixed: 'partial' added to the union with every job.status = … site cited, and an amber terminal banner that interpolates the skipped count. Four tests pin it (partial renders the count, interpolates >1, and neither completed nor failed claims the branch). Confirmed they fail against the pre-fix view — the rendered text is "Fleet nodesdone …" with no outcome line.

Same class, second instance

UpdateAllStage.status omitted 'current' (_StageStatus.CURRENT, code_sync.py:4245 — "already at target commit"), even though CodeSyncView.vue:127 and :145 already map it. The type contradicted the view's own rendering. Added — and current now gets its own label ("already current") instead of borrowing pipelineStageSuccess's "done", which is what it had been silently rendering as.

Fields the frontend could not reach

  • FleetSyncJobStatus.failure_reason (schemas.py:1795) — populated on every job failure (code_sync.py:401-402), absent from the hand-written type. Not a live defect: getJobStatus/getRecentJobs have no view consumer, so nothing rendered it either way. Restored by derivation.
  • DriftResolveResponse.deps_changed / post_steps (schemas.py:1689-1690) — the caller could not tell whether a resync changed dependencies or which post-steps ran.
  • PortInfo.address (schemas.py:106) — the bind interface added by GH#11224 for the security-posture audit, dropped by the frontend type.
  • PendingNodeResponse.current_version is optional and nullable, but formatVersion (CodeSyncView.vue:248) narrowed its parameter to string | null — narrower than the getCommitHashDisplay it delegates to, which already accepts undefined. Widened to match.

Contract looser than reality — narrowing deliberately kept

FleetStatusResponse.services and BulkActionResponse.results are bare dict server-side (orchestration.py:79, :97), so the contract can only say { [key: string]: unknown }. Every consumer reads .status/.host/.port off those entries. Derived for the scalar fields (so a renamed or added count is caught) and intersected to pin the element shape, rather than derived verbatim and cast back.

Deferred, with reasons

Two observations recorded but not acted on, to keep this diff to contract derivation: stage.message is never rendered by the template (the backend writes a useful summary into it on every stage), and getJobStatus/getRecentJobs are exported from useCodeSync with no consumer outside tests.

Verification

Run in autobot-slm-frontend/.

Before — pristine origin/Dev_new_gui:

npm run type-check  -> clean
npm run test:unit   -> Test Files 26 passed (26) | Tests 211 passed (211)
npm run lint        -> 16 problems (0 errors, 16 warnings)

After:

npm run type-check  -> clean
npm run test:unit   -> Test Files 26 passed (26) | Tests 215 passed (215)
npm run lint        -> 16 problems (0 errors, 16 warnings)

+4 tests, all for the partial terminal status. Proven to be real regression tests, not tautologies: restoring the pre-fix CodeSyncView.vue over the new suite fails 2 of them with expected 'Code SyncManage agent code versions a…' to contain '2 node(s) skipped', and the received text shows the partial job rendering Fleet nodesdone and then nothing.

Lint findings were captured to file and diffed line-for-line against the baseline — identical 18-line finding set, so no pre-existing warning was masked and none was added.

git diff --exit-code src/types/generated/api.ts clean, so verify-generated-types-slm holds. The generated file was read but never edited, and no regeneration was needed because no backend schema changed. The Omit/Partial/intersection behaviour relied on above was verified with a standalone tsc --ignoreConfig probe rather than assumed.

Branch rebased onto e489b51dc after origin/Dev_new_gui moved mid-work (#13150); type-check, the full suite and lint were all re-run green on the rebased tip.

Per the #13090 execution constraint no AutoBot application code was run — no backend, no SLM backend, no schema dump, no ad-hoc Postgres/Redis. Every contract claim above was read from the committed generated artefact and cross-checked against backend source at the cited file:line.

The new user-visible string is i18n'd. Note for reviewers: the SLM admin app bootstraps a single en locale (src/i18n/index.ts:13, src/locales/ contains only en.json) — it is not the 11-locale main frontend, so en.json is the complete set here.

Model Used

claude-opus-5

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant