Repository navigation
refactor(slm-frontend): derive the code-sync, orchestration and role contracts from the generated schema (#13138) - #13152
Merged
Merged
Conversation
…lent partial update-all (#13138)
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
This was referenced Jul 31, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsmonitoring long tail and the rest oftypes/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, onc0a45b77a:76, not the 77 quoted on the issue. The single-unit difference is
ThreatSummary: it is derived attypes/slm.ts:727, butSecurityView.vue:43re-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
statusin this group isstrserver-side with the value set living only in a trailing#comment, so the generated schema saysstringwhile 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:PostSyncAction.category— 4 construction sites;FleetSyncNodeStatus.status;ComponentSyncJobStatus.status) → derive the shape, keep the union, cite the sites.ServiceCategoryUpdate.categoryhaspattern="^(autobot|system)$", which openapi-typescript can only render asstring) → derive, keep the union: sending anything else is a guaranteed 422.UpdateAllJob.status,UpdateAllStage.status) → the finding. Below.Two schema-shaped traps found the hard way. The generated types are intersected with an
additionalPropertiesindex signature, sokeyof Tisstring | numberandOmit/Picksilently collapse every named member into the index signature —Omit<ServiceActionRequest, 'force'>.node_idtypes asunknown, no error at the definition site. Verified with a standalonetscprobe before relying on it.Partialis homomorphic and does preserve them, and plain intersection narrows a widenedstringcorrectly, 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) andBulkActionRequest.exclude(default_factory=list) are optional to send; deriving them bare would have forced every call site to pass them. Both usePartial<>.What Changed
23 shapes derived from
components['schemas'][…]across the four composables (76 → 53 by the same scan). Each alias carries thefile:lineof the backend model it is theresponse_model/body of.composables/useCodeSync.tsPendingNode,PendingNodesResponse,FleetSyncResponse,FleetSyncNodeStatus,FleetSyncJobStatus,ScheduleRunResponse,DriftedFile,FileDriftReport,DriftResolveResponse,DriftResolveJobResponse,ComponentSyncJobStatus,UpdateAllStage,UpdateAllJobcomposables/useOrchestration.tsServiceActionRequest,ServiceMigrateRequest,FleetStatusResponse,BulkActionRequest,BulkActionResponsecomposables/useRoles.tsPortInfo,PostSyncAction,NodeActionsResponse,DecommissionRoleInfocomposables/useOrchestrationManagement.tsFleetServiceNodeStatus,ServiceCategoryUpdateThe defect: a partial update-all reports nothing at all
UpdateAllJob.statuswas declared'pending' | 'running' | 'completed' | 'failed' | 'already_current'. Since #11511 the backend has a sixth terminal status:set whenever the fleet stage skips a node that is not operational. On the frontend,
'partial'matches neither banner — the failure banner testsstatus === '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 tostage.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. DerivingUpdateAllJobrestored 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 everyjob.status = …site cited, and an amber terminal banner that interpolates the skipped count. Four tests pin it (partialrenders the count, interpolates >1, and neithercompletednorfailedclaims 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.statusomitted'current'(_StageStatus.CURRENT,code_sync.py:4245— "already at target commit"), even thoughCodeSyncView.vue:127and:145already map it. The type contradicted the view's own rendering. Added — andcurrentnow gets its own label ("already current") instead of borrowingpipelineStageSuccess'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/getRecentJobshave 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_versionis optional and nullable, butformatVersion(CodeSyncView.vue:248) narrowed its parameter tostring | null— narrower than thegetCommitHashDisplayit delegates to, which already acceptsundefined. Widened to match.Contract looser than reality — narrowing deliberately kept
FleetStatusResponse.servicesandBulkActionResponse.resultsare baredictserver-side (orchestration.py:79,:97), so the contract can only say{ [key: string]: unknown }. Every consumer reads.status/.host/.portoff 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
NodeCreate/NodeUpdate/ConnectionTestRequest— still blocked on bug(slm): Edit Node silently discards all SSH/PKI/enrollment fields — NodeUpdate accepts 5 of 14; ssh_key dropped on register and connection-test too #13145, unchanged.DashboardOverview,SystemMetrics,LogEntry×2 — view-models that remap the endpoint sharing their name; they need renaming, not derivation. Re-checked this group for the same shape and found none: every type here is a straight wire model.types/api-responses.tsmonitoring/observability read models and the rest oftypes/slm.ts(NPU, maintenance windows, services, external agents,RoleInfo×3). Not attempted.Two observations recorded but not acted on, to keep this diff to contract derivation:
stage.messageis never rendered by the template (the backend writes a useful summary into it on every stage), andgetJobStatus/getRecentJobsare exported fromuseCodeSyncwith no consumer outside tests.Verification
Run in
autobot-slm-frontend/.Before — pristine
origin/Dev_new_gui:After:
+4 tests, all for the
partialterminal status. Proven to be real regression tests, not tautologies: restoring the pre-fixCodeSyncView.vueover the new suite fails 2 of them withexpected 'Code SyncManage agent code versions a…' to contain '2 node(s) skipped', and the received text shows the partial job renderingFleet nodesdoneand 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.tsclean, soverify-generated-types-slmholds. The generated file was read but never edited, and no regeneration was needed because no backend schema changed. TheOmit/Partial/intersection behaviour relied on above was verified with a standalonetsc --ignoreConfigprobe rather than assumed.Branch rebased onto
e489b51dcafterorigin/Dev_new_guimoved 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
enlocale (src/i18n/index.ts:13,src/locales/contains onlyen.json) — it is not the 11-locale main frontend, soen.jsonis the complete set here.Model Used
claude-opus-5