Merge pull request #14729 from chuiwenwei/fix/register-the-unguarded-wire-mirrors

fix(repolint): register the wire mirrors that had no guard
This commit is contained in:
YHH authored and GitHub committed 2026-10-28 21:34:37 -07:00
commit c58b1242c8
3 files changed
+336 -3

No files matched your search

+7 -3
View File
@@ -45,9 +45,13 @@ const tuiInboxFile = "internal/frontend/tui/client_inbox.go"
// mirroredWireTypes are the Go types the desktop keeps a second, hand-written
// copy of: the graph, whose consumers all fold the same deltas, and the sandbox
// editor, which draws a security posture a field it cannot read would get
// wrong. Declared and not inferred, for the reason the sensitive paths are: no
// spelling tells a mirrored contract from a struct with json tags.
var mirroredWireTypes = []wireMirror{
// wrong. Declared and not inferred: no spelling tells a mirrored contract from
// a struct with json tags. Two files only so neither outgrows the line ceiling.
var mirroredWireTypes = slices.Concat(mirroredWireTypesHead, mirroredWireTypesTail)
// mirroredWireTypesHead is the table's first half; wireparity_table.go holds
// the rest.
var mirroredWireTypesHead = []wireMirror{
{"internal/frontend/serve/workspace_files.go", "workspaceFile", tsWorkspaceFile, "WorkspaceFile"},
{"internal/base/workspacefile/file.go", "Ref", tsWorkspaceFile, "FileRef"},
{"internal/base/workspacefile/file.go", "Info", tsWorkspaceFile, "PreviewInfo"},
+184
View File
@@ -0,0 +1,184 @@
package main
// The page's port files this half of the table reads. The other half's constants
// live in wireparity.go.
const (
tsAdjudicationFile = "desktop/frontend-next/src/port/adjudication.ts"
tsAttachmentFile = "desktop/frontend-next/src/port/attachment.ts"
tsBackupFile = "desktop/frontend-next/src/port/backup.ts"
tsCompositionFile = "desktop/frontend-next/src/port/composition.ts"
tsContextCompositionFile = "desktop/frontend-next/src/port/context_composition.ts"
tsHookFile = "desktop/frontend-next/src/port/hook.ts"
tsMarketFile = "desktop/frontend-next/src/port/market.ts"
tsMemoryFile = "desktop/frontend-next/src/port/memory.ts"
tsNetworkFile = "desktop/frontend-next/src/port/network.ts"
tsSkillFile = "desktop/frontend-next/src/port/skill.ts"
tsStorageFile = "desktop/frontend-next/src/port/storage.ts"
tsUsageFile = "desktop/frontend-next/src/port/usage.ts"
)
// mirroredWireTypesTail is the second half of the registry whose first half is
// wireparity.go. It is a second file only because one table would take that
// file past the repository's line ceiling; the checker reads the two as one
// list, and every row is a page copy that agrees field for field with the Go
// struct its handler actually writes.
var mirroredWireTypesTail = []wireMirror{
// account.ts: DeviceGrant is absent. /account/login writes a map literal
// assembled from the grant, so the struct is not what the page reads, and a
// guard over it would not see that response drift.
// adjudication
{"internal/frontend/serve/adjudications.go", "adjudicationItem", tsAdjudicationFile, "AdjudicationEntry"},
{"internal/frontend/serve/adjudications.go", "adjudicationView", tsAdjudicationFile, "Adjudications"},
// attachment
{"internal/frontend/serve/drop.go", "droppedRef", tsAttachmentFile, "DroppedRef"},
// backup
{"internal/ext/configbackup/snapshot.go", "CategoryInfo", tsBackupFile, "BackupCategoryInfo"},
{"internal/platform/account/backup.go", "Backup", tsBackupFile, "BackupEntry"},
{"internal/ext/configbackup/snapshot.go", "Omission", tsBackupFile, "BackupOmission"},
{"internal/frontend/serve/backup.go", "createBackupRequest", tsBackupFile, "BackupCreateRequest"},
{"internal/ext/configbackup/paths.go", "PathRef", tsBackupFile, "BackupPathRef"},
{"internal/ext/configbackup/plan.go", "PlanItem", tsBackupFile, "BackupPlanItem"},
{"internal/ext/configbackup/plan.go", "Plan", tsBackupFile, "BackupPlan"},
{"internal/ext/configbackup/apply.go", "PluginRef", tsBackupFile, "BackupPluginRef"},
// boundary
{"internal/session/control/config_problem.go", "ConfigProblem", tsBoundaryFile, "ConfigProblem"},
{"internal/session/control/subagent_steer_settings.go", "SubagentSteerSettings", tsBoundaryFile, "SubagentSteerSettings"},
{"internal/session/control/peer_mail_send.go", "PeerView", tsBoundaryFile, "PeerView"},
{"internal/session/control/peer_mail.go", "PeerHeld", tsBoundaryFile, "PeerHeld"},
{"internal/session/control/peer_threads.go", "PeerThread", tsBoundaryFile, "PeerThread"},
{"internal/session/control/peer_mail_send.go", "PeerReceipt", tsBoundaryFile, "PeerReceipt"},
{"internal/runtime/persona/presets.go", "Preset", tsBoundaryFile, "PersonaPreset"},
{"internal/session/control/persona_settings.go", "PersonaSettings", tsBoundaryFile, "PersonaSettings"},
{"internal/contract/config/unconverted_prices.go", "UnconvertedPrice", tsBoundaryFile, "UnconvertedPrice"},
{"internal/session/control/turn_narration_settings.go", "TurnNarrationSettings", tsBoundaryFile, "TurnNarrationSettings"},
{"internal/session/control/web_access_settings.go", "WebAccessSettings", tsBoundaryFile, "WebAccessSettings"},
{"internal/tools/websearch/searxng_probe.go", "SearxngProbe", tsBoundaryFile, "SearxngProbe"},
{"internal/session/control/feedback_level_notice.go", "FeedbackLevelNoticeSettings", tsBoundaryFile, "FeedbackLevelNotices"},
// composition
{"internal/contract/agentcomposition/composition.go", "Definition", tsCompositionFile, "AgentComposition"},
{"internal/state/agentpreset/catalog.go", "Catalog", tsCompositionFile, "AgentPresetCatalog"},
// context_composition
{"internal/contract/event/notice_payload.go", "ContextComposition", tsContextCompositionFile, "ContextComposition"},
// feedback
{"internal/platform/feedback/link.go", "LinkedInstall", tsFeedbackFile, "FeedbackLinkedInstall"},
// hook
{"internal/frontend/serve/hooks.go", "hookEntry", tsHookFile, "HookEntry"},
{"internal/frontend/serve/hooks.go", "hookSource", tsHookFile, "HookSource"},
{"internal/frontend/serve/hooks.go", "hookEventInfo", tsHookFile, "HookEventInfo"},
{"internal/ext/hook/edit.go", "DryRunResult", tsHookFile, "HookDryRun"},
// host.ts is the Electron preload bridge, not kernel wire: its Refusal and
// ViewRect are answered by the shell, so neither is a mirror of a Go struct.
// hub
{"internal/frontend/serve/hub_contract.go", "RuntimeView", tsHubFile, "RuntimeView"},
{"internal/platform/openwith/openwith.go", "Status", tsHubFile, "OpenWithTarget"},
{"internal/frontend/serve/history_search.go", "historySearchHit", tsHubFile, "HistoryHit"},
// look
{"internal/frontend/serve/appearance.go", "wallpaperLayerView", tsLookFile, "WallpaperLayer"},
{"internal/frontend/serve/appearance.go", "wallpaperAudioView", tsLookFile, "WallpaperAudioTrack"},
// market
// MarketList is absent: /market/packages assembles a map, so market.Page is not the writer.
{"internal/frontend/serve/market.go", "marketInstalled", tsMarketFile, "MarketInstalled"},
{"internal/ext/market/client.go", "Version", tsMarketFile, "MarketVersion"},
{"internal/ext/market/cache.go", "CacheNote", tsMarketFile, "MarketCache"},
{"internal/frontend/serve/market.go", "marketDetailView", tsMarketFile, "MarketDetail"},
{"internal/ext/market/install.go", "Request", tsMarketFile, "MarketRequest"},
{"internal/ext/market/publish.go", "Submission", tsMarketFile, "MarketSubmission"},
{"internal/ext/market/publish.go", "Published", tsMarketFile, "MarketPublished"},
// mcp
// McpDraft is absent: /mcp/parse assembles a map of servers and risks.
{"internal/frontend/serve/catalog.go", "mcpTool", tsMcpFile, "McpTool"},
{"internal/session/control/capability_scope.go", "CapabilityScope", tsMcpFile, "CapabilityScope"},
{"internal/frontend/serve/catalog.go", "draftServer", tsMcpFile, "McpDraftServer"},
{"internal/frontend/serve/catalog.go", "draftRisk", tsMcpFile, "McpRisk"},
{"internal/ext/plugin/install.go", "MCPInstallResult", tsMcpFile, "McpInstallResult"},
// memory
{"internal/frontend/serve/memory.go", "memoryEntry", tsMemoryFile, "MemoryEntry"},
{"internal/frontend/serve/memory.go", "unreadableMemory", tsMemoryFile, "UnreadableMemory"},
{"internal/frontend/serve/memory.go", "editable", tsMemoryFile, "MemoryEdit"},
// model
{"internal/session/control/slash.go", "SlashItem", tsModelFile, "CompletionItem"},
{"internal/session/control/slash.go", "SlashArg", tsModelFile, "CompletionArg"},
{"internal/session/control/complete.go", "Completion", tsModelFile, "Completion"},
{"internal/frontend/serve/modelcaps.go", "modelPrice", tsModelFile, "ModelPrice"},
// network
{"internal/session/control/network.go", "NetworkSettings", tsNetworkFile, "NetworkSettings"},
{"internal/base/netclient/diagnose.go", "Probe", tsNetworkFile, "NetworkProbe"},
// plugin
{"internal/frontend/serve/plugins.go", "pluginItem", tsPluginFile, "PluginItem"},
{"internal/frontend/serve/plugins.go", "pluginHook", tsPluginFile, "PluginHook"},
{"internal/frontend/serve/plugins.go", "pluginServer", tsPluginFile, "PluginServer"},
{"internal/frontend/serve/plugins.go", "pluginRuntime", tsPluginFile, "PluginRuntime"},
{"internal/ext/pluginpkg/pluginpkg.go", "CompatibilityIssue", tsPluginFile, "PluginSkipped"},
{"internal/frontend/serve/plugins.go", "pluginView", tsPluginFile, "PluginPackage"},
// port
{"internal/session/control/input.go", "InvocationRequest", tsPortFile, "InvocationRequest"},
{"internal/frontend/serve/tray.go", "TrayPrefs", tsPortFile, "TrayPrefs"},
// provider
{"internal/frontend/serve/provider_check.go", "providerModelCheckRequest", tsProviderFile, "ProviderModelCheckRequest"},
{"internal/frontend/serve/provider_edit.go", "providerEditBody", tsProviderFile, "ProviderEdit"},
{"internal/frontend/serve/provider_model_prices.go", "modelPriceView", tsProviderFile, "ModelRate"},
{"internal/frontend/serve/provider_model_limits.go", "modelLimitsView", tsProviderFile, "ModelLimit"},
{"internal/frontend/serve/provider_model_effort.go", "modelEffortView", tsProviderFile, "ModelEffort"},
{"internal/frontend/serve/provider_setup.go", "providerSetupState", tsProviderFile, "ProviderSetup"},
// remote
{"internal/frontend/serve/hub_remote.go", "RemoteProbeRoute", tsRemoteFile, "RemoteRoute"},
{"internal/frontend/serve/hub_remote.go", "RemoteProbe", tsRemoteFile, "RemoteProbe"},
// session
{"internal/state/checkpoint/types.go", "FileStage", tsSessionFile, "RewindFileStage"},
{"internal/session/control/approval_orchestration.go", "PostureReport", tsSessionFile, "ApprovalDefault"},
{"internal/frontend/serve/balance.go", "walletLine", tsSessionFile, "WalletLine"},
{"internal/frontend/serve/balance.go", "walletBody", tsSessionFile, "WalletReading"},
// share
{"internal/frontend/serve/cloud_presence.go", "CloudControllerView", tsShareFile, "CloudDevice"},
{"internal/frontend/serve/device_share.go", "CloudShareOffer", tsShareFile, "CloudShareOffer"},
// shell
{"internal/session/control/shell.go", "ShellOption", tsShellFile, "ShellOption"},
{"internal/session/control/shell.go", "ShellSettings", tsShellFile, "ShellSettings"},
// skill
{"internal/frontend/serve/catalog.go", "skillEntry", tsSkillFile, "SkillEntry"},
{"internal/ext/skillupdate/skillupdate.go", "Change", tsSkillFile, "SkillUpdateChange"},
{"internal/ext/skillupdate/skillupdate.go", "Plan", tsSkillFile, "SkillUpdatePlan"},
{"internal/frontend/serve/skill_source.go", "skillSource", tsSkillFile, "SkillSource"},
// storage
{"internal/frontend/serve/storage.go", "storageRoot", tsStorageFile, "StorageRoot"},
{"internal/frontend/serve/storage.go", "storageMove", tsStorageFile, "StorageMove"},
{"internal/frontend/serve/storage.go", "storageRefusal", tsStorageFile, "StorageRefusal"},
{"internal/frontend/serve/storage.go", "storagePlan", tsStorageFile, "StoragePlan"},
// usage
{"internal/contract/pricing/money.go", "Money", tsUsageFile, "Money"},
{"internal/state/stats/query.go", "DailyTokens", tsUsageFile, "UsageDay"},
{"internal/state/stats/query.go", "HourlyTokens", tsUsageFile, "UsageHour"},
{"internal/state/stats/query.go", "ModelUsage", tsUsageFile, "UsageModel"},
{"internal/state/stats/query.go", "ProviderUsage", tsUsageFile, "UsageProvider"},
{"internal/state/stats/query.go", "RangeStats", tsUsageFile, "UsageReport"},
{"internal/state/stats/upload.go", "UploadDay", tsUsageFile, "UploadDay"},
{"internal/state/stats/upload.go", "UploadProvider", tsUsageFile, "UploadProvider"},
{"internal/state/stats/upload.go", "UploadStats", tsUsageFile, "UploadReport"},
// wire
{"internal/contract/eventwire/wire.go", "Bound", tsWireFile, "Bound"},
{"internal/contract/provider/argument_normalization.go", "ArgumentNormalization", tsWireFile, "ArgumentNormalization"},
{"internal/contract/pricing/money.go", "Money", tsWireFile, "Money"},
{"internal/contract/eventwire/wire.go", "Via", tsWireFile, "Via"},
{"internal/contract/eventwire/wire.go", "AskOption", tsWireFile, "AskOption"},
{"internal/contract/eventwire/wire.go", "Ask", tsWireFile, "Ask"},
{"internal/contract/eventwire/extension.go", "ExtensionStatus", tsWireFile, "ExtensionStatus"},
{"internal/contract/eventwire/extension.go", "ExtensionKeyValue", tsWireFile, "ExtensionKeyValue"},
{"internal/contract/eventwire/extension.go", "ExtensionActionRef", tsWireFile, "ExtensionActionRef"},
{"internal/contract/eventwire/extension.go", "ExtensionCard", tsWireFile, "ExtensionCard"},
{"internal/contract/eventwire/extension.go", "ExtensionFormField", tsWireFile, "ExtensionFormField"},
{"internal/contract/eventwire/extension.go", "ExtensionForm", tsWireFile, "ExtensionForm"},
{"internal/contract/eventwire/extension.go", "ExtensionPanel", tsWireFile, "ExtensionPanel"},
{"internal/contract/eventwire/extension.go", "ExtensionNotification", tsWireFile, "ExtensionNotification"},
{"internal/contract/eventwire/extension.go", "ExtensionView", tsWireFile, "ExtensionView"},
{"internal/contract/eventwire/wire.go", "StreamAttempt", tsWireFile, "StreamAttempt"},
{"internal/contract/eventwire/wire.go", "MemoryCitation", tsWireFile, "MemoryCitation"},
{"internal/contract/eventwire/receipt.go", "CompletionReceipt", tsWireFile, "Receipt"},
{"internal/contract/eventwire/receipt.go", "ReceiptChange", tsWireFile, "ReceiptChange"},
{"internal/contract/eventwire/receipt.go", "ReceiptVerification", tsWireFile, "ReceiptVerification"},
{"internal/contract/eventwire/receipt.go", "ReceiptGap", tsWireFile, "ReceiptGap"},
// workspace
{"internal/platform/gitstatus/gitstatus.go", "Change", tsWorkspaceFile, "WorkspaceChange"},
{"internal/frontend/serve/workspace_listing.go", "workspaceListing", tsWorkspaceFile, "WorkspaceFiles"},
// WorkspaceEntry is absent: no Go writer sends that pair; its fields only coincide with unrelated folder types.
}
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
// treeRoot is the repository root from the package directory a test runs in.
func treeRoot(t *testing.T) string {
t.Helper()
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
return root
}
// The added half of the registry is the claim in its pull request: each row is
// a page copy that agrees today with the struct its handler writes. A row that
// names nothing, or two rows that name the same reader, would guard a pair the
// tree does not have while the gate reported green.
func TestAddedWireMirrorsResolveAndAreUnique(t *testing.T) {
root := treeRoot(t)
tsBodies := map[string]string{}
headReaders := map[string]bool{}
for _, m := range mirroredWireTypesHead {
headReaders[m.tsFile+"."+m.tsType] = true
}
seenPair := map[string]bool{}
seenReader := map[string]bool{}
for _, m := range mirroredWireTypesTail {
pair := m.goFile + "." + m.goType + " -> " + m.tsFile + "." + m.tsType
reader := m.tsFile + "." + m.tsType
if seenPair[pair] {
t.Errorf("%s is declared twice", pair)
}
seenPair[pair] = true
// A reader registered once is the point; the same writer may still back
// two readers (the page keeps a copy per port file that carries it).
if headReaders[reader] || seenReader[reader] {
t.Errorf("%s returns a reader the registry already guards", pair)
}
seenReader[reader] = true
// The rows added here register data copies only: a test file, a mock or
// a fixture would make the guard read a shape the wire never carries.
if strings.HasSuffix(m.goFile, "_test.go") || strings.HasSuffix(m.tsFile, ".test.ts") ||
strings.Contains(filepath.Base(m.tsFile), "mock") || strings.Contains(filepath.Base(m.tsFile), "fixture") {
t.Errorf("%s names a test or fixture file", pair)
continue
}
src, err := parseSource(root, m.goFile)
if err != nil || src == nil || src.file == nil {
t.Errorf("%s names a Go file repolint cannot parse", pair)
continue
}
goFields, ok := wireFieldNames(src.file, m.goType)
if !ok {
t.Errorf("%s names no Go struct %s in %s", pair, m.goType, m.goFile)
continue
}
body, ok := tsBodies[m.tsFile]
if !ok {
raw, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(m.tsFile)))
if err != nil {
t.Errorf("%s names a TS file that cannot be read: %v", pair, err)
continue
}
body = string(raw)
tsBodies[m.tsFile] = body
}
tsFields, _, found := tsInterfaceFields(body, m.tsType)
if !found {
t.Errorf("%s names no TS interface %s in %s", pair, m.tsType, m.tsFile)
continue
}
// Exact today, not close: the row's justification is that the two sides
// carry the same names, and a set that differs is a row for a follow-up.
if !slices.Equal(sortedCopy(goFields), sortedCopy(tsFields)) {
t.Errorf("%s is not exact: Go sends %v, TS reads %v", pair, sortedCopy(goFields), sortedCopy(tsFields))
}
}
}
func sortedCopy(names []string) []string {
out := append([]string(nil), names...)
slices.Sort(out)
return out
}
// One newly guarded pair, with a field removed from the page's copy: the guard
// has to name the field, or a reviewer cannot tell a drift from a stale row.
func TestAddedWireMirrorDroppedFieldIsNamed(t *testing.T) {
root := treeRoot(t)
const pair = `{"internal/frontend/serve/backup.go", "createBackupRequest", tsBackupFile, "BackupCreateRequest"}`
var m wireMirror
for _, candidate := range mirroredWireTypesTail {
if candidate.goFile == "internal/frontend/serve/backup.go" && candidate.goType == "createBackupRequest" &&
candidate.tsFile == tsBackupFile && candidate.tsType == "BackupCreateRequest" {
m = candidate
break
}
}
if m.tsType == "" {
t.Fatalf("the registry no longer declares %s", pair)
}
src, err := parseSource(root, m.goFile)
if err != nil {
t.Fatal(err)
}
goFields, ok := wireFieldNames(src.file, m.goType)
if !ok {
t.Fatalf("%s does not resolve", m.goType)
}
raw, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(m.tsFile)))
if err != nil {
t.Fatal(err)
}
body := string(raw)
fields, line, found := tsInterfaceFields(body, m.tsType)
if !found || !slices.Contains(fields, "passphrase") {
t.Fatalf("%s does not declare passphrase in the tree", m.tsType)
}
dropped := strings.Replace(body, " passphrase: string;\n", "", 1)
if dropped == body {
t.Fatal("the test could not find the field to drop")
}
got := wireParityFindings([]wireMirror{m}, map[string][]string{m.goFile + "." + m.goType: goFields},
map[string]string{m.tsFile: dropped})
if len(got) != 1 {
t.Fatalf("findings = %+v, want exactly the dropped field", got)
}
if !strings.Contains(got[0].Msg, `sends "passphrase"`) || got[0].File != m.tsFile {
t.Fatalf("finding = %s:%d %s: %s, want it to name passphrase against %s",
got[0].File, got[0].Line, got[0].Rule, got[0].Msg, m.tsFile)
}
t.Logf("dropping passphrase reported: %s:%d %s", got[0].File, line, got[0].Msg)
}