From fc0945c7a24687c03d63ce9d1dfd356a091c94a0 Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Sat, 16 May 2026 16:46:06 +0200 Subject: [PATCH 1/2] fix(generate-types): re-sync hardcoded TS output with contracts.ts The TypeScript-as-Go-string literal in cmd/generate-types/main.go drifted from frontend/src/types/contracts.ts when PR #424 (Server Config tab parity) and PR #463 (per-tool enable/disable) edited contracts.ts directly without updating the generator. Running `go run ./cmd/generate-types` (invoked by Makefile's `frontend-build` target) silently reverts those fields, producing a dirty working tree on every `make build`: - Server.isolation_defaults - IsolationConfig.network_mode, IsolationConfig.extra_args - IsolationDefaults (entire interface) - Tool.disabled, Tool.approval_status The reverted contracts.ts also feeds back into Vite's bundle hashes, which is the likely reason web/frontend/dist/* also churns on rebuilds. This commit catches the generator up to the actual contracts.ts content. After this, `go run ./cmd/generate-types` is idempotent against HEAD. Verified: generator output is byte-identical to contracts.ts. --- cmd/generate-types/main.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/generate-types/main.go b/cmd/generate-types/main.go index b8769769..bef2068c 100644 --- a/cmd/generate-types/main.go +++ b/cmd/generate-types/main.go @@ -117,6 +117,7 @@ export interface HealthStatus { created: string; // ISO date string updated: string; // ISO date string isolation?: IsolationConfig; + isolation_defaults?: IsolationDefaults; // Resolved baseline values (read-only, used as placeholders) oauth_status?: 'authenticated' | 'expired' | 'error' | 'none'; // OAuth authentication status token_expires_at?: string; // ISO date string when OAuth token expires user_logged_out?: boolean; // True if user explicitly logged out (prevents auto-reconnection) @@ -135,12 +136,27 @@ export interface OAuthConfig { export interface IsolationConfig { enabled: boolean; image?: string; + network_mode?: string; + extra_args?: string[]; memory_limit?: string; cpu_limit?: string; working_dir?: string; timeout?: string; } +// IsolationDefaults reports the resolved baseline Docker isolation +// values the backend will apply when no per-server override is set. +// Populated on server-list / server-get responses; the Web UI uses these +// as placeholders so "empty = inherit" is discoverable instead of +// mysterious. Never sent back on PATCH requests. +export interface IsolationDefaults { + runtime_type?: string; + image?: string; + network_mode?: string; + extra_args?: string[]; + working_dir?: string; +} + `) // Tool types @@ -151,6 +167,12 @@ export interface IsolationConfig { schema?: Record; usage: number; last_used?: string; // ISO date string + // Mirrors contracts.Tool.Disabled on the Go side — present when an + // approval record exists for this tool. Absent means "enabled" (default). + disabled?: boolean; + // Tool-level quarantine status surfaced by the same approval record. + // Optional because non-quarantined tools simply omit the field. + approval_status?: string; } export interface SearchResult { From f2d90e5c88f75f13d2afe584240b4818440540c2 Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Sat, 16 May 2026 17:13:24 +0200 Subject: [PATCH 2/2] test(generate-types): catch future contracts.ts drift in CI Adds TestContractsInSync, which runs the generator's content function and asserts byte-equality with the committed frontend/src/types/contracts.ts. The next time a contributor hand-edits contracts.ts (or hand-edits the hardcoded TS string in main.go) without updating both sides, CI fails with a clear message pointing at the fix: Either run \`go run ./cmd/generate-types\` from the module root (if the generator is the source of truth) or update the string literals in main.go (if contracts.ts is the source of truth). The drift this test guards against is what allowed PRs #424 and #463 to silently leave the generator out of sync. Refactors main.go to factor out generateFileContent() so the test can compare without re-implementing the header concat. --- cmd/generate-types/main.go | 32 +++++++++++++---------- cmd/generate-types/main_test.go | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 cmd/generate-types/main_test.go diff --git a/cmd/generate-types/main.go b/cmd/generate-types/main.go index bef2068c..93a509df 100644 --- a/cmd/generate-types/main.go +++ b/cmd/generate-types/main.go @@ -8,30 +8,34 @@ import ( "strings" ) -func main() { - // Define the TypeScript types based on our Go contracts - typeDefinitions := generateTypeDefinitions() +// contractsRelPath is the location of the generated TypeScript file +// relative to the module root. +const contractsRelPath = "frontend/src/types/contracts.ts" + +// generateFileContent returns the full contents that should be written to +// contracts.ts (header + generated type definitions). Exposed so tests can +// verify the committed file matches the generator output and catch drift +// without writing to disk. +func generateFileContent() string { + return fmt.Sprintf(`// Generated TypeScript types from Go contracts +// DO NOT EDIT - This file is auto-generated by cmd/generate-types + +%s`, generateTypeDefinitions()) +} - // Create output directory if it doesn't exist - outputDir := "frontend/src/types" +func main() { + outputDir := filepath.Dir(contractsRelPath) if err := os.MkdirAll(outputDir, 0755); err != nil { fmt.Printf("Error creating output directory: %v\n", err) os.Exit(1) } - // Write the TypeScript file - outputFile := filepath.Join(outputDir, "contracts.ts") - content := fmt.Sprintf(`// Generated TypeScript types from Go contracts -// DO NOT EDIT - This file is auto-generated by cmd/generate-types - -%s`, typeDefinitions) - - if err := os.WriteFile(outputFile, []byte(content), 0600); err != nil { + if err := os.WriteFile(contractsRelPath, []byte(generateFileContent()), 0600); err != nil { fmt.Printf("Error writing TypeScript file: %v\n", err) os.Exit(1) } - fmt.Printf("Successfully generated TypeScript types: %s\n", outputFile) + fmt.Printf("Successfully generated TypeScript types: %s\n", contractsRelPath) } func generateTypeDefinitions() string { diff --git a/cmd/generate-types/main_test.go b/cmd/generate-types/main_test.go new file mode 100644 index 00000000..80c401e6 --- /dev/null +++ b/cmd/generate-types/main_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// TestContractsInSync fails when frontend/src/types/contracts.ts has drifted +// from what cmd/generate-types would produce today. Catches the failure mode +// where a contributor hand-edits contracts.ts (or hand-edits the generator's +// hardcoded string literals) without updating the other side: the next +// `make build` / `go run ./cmd/generate-types` silently reverts their work +// and leaves a dirty working tree. +// +// To fix a failure of this test: +// 1. Decide which side is correct (usually: the generator). +// 2. Run `go run ./cmd/generate-types` from the module root, OR update the +// string literals in main.go to match contracts.ts. +// 3. Commit both files in the same change. +func TestContractsInSync(t *testing.T) { + // cmd/generate-types tests run with cwd = the package directory. + // Walk up two levels to reach the module root. + contractsPath := filepath.Join("..", "..", contractsRelPath) + + committed, err := os.ReadFile(contractsPath) + if err != nil { + t.Fatalf("reading %s: %v", contractsPath, err) + } + + generated := []byte(generateFileContent()) + + if string(committed) == string(generated) { + return + } + + t.Fatalf( + "%s is out of sync with cmd/generate-types/main.go.\n"+ + "\nThe TypeScript string literals in main.go must produce a byte-identical\n"+ + "copy of contracts.ts. To fix: either run `go run ./cmd/generate-types`\n"+ + "from the module root (if the generator is the source of truth) or update\n"+ + "the string literals in main.go (if contracts.ts is the source of truth).\n"+ + "\ncommitted size: %d bytes\ngenerated size: %d bytes", + contractsRelPath, len(committed), len(generated), + ) +}