From ba83d2e73912be0ff0e5eafeadff9c0a5837754a Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Sat, 16 May 2026 16:46:06 +0200 Subject: [PATCH 1/3] 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 8f54dd43515f6c3fb8cc25b98f471426c0490c60 Mon Sep 17 00:00:00 2001 From: Roman Chernyak Date: Sat, 16 May 2026 17:13:24 +0200 Subject: [PATCH 2/3] 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), + ) +} From 08973c4b3bbac99211ee69497228e8ea6ed32f84 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 17 May 2026 09:14:36 +0300 Subject: [PATCH 3/3] fix(generate-types): make TestContractsInSync CRLF-safe on Windows The new TestContractsInSync did a raw byte comparison of the committed contracts.ts against generator output. On Windows CI (core.autocrlf=true) git checks out contracts.ts with CRLF endings while the generator emits LF, so the test failed on windows-amd64 even though the contract was in sync (observed on PR #472). Two-layer fix: - .gitattributes pins frontend/src/types/contracts.ts to `text eol=lf` so it is checked out identically on every platform (the real fix). - The test now normalizes CRLF->LF before comparing, keeping it green regardless of a contributor's local git config (defense in depth). Verified: test passes with both LF and CRLF checkouts of contracts.ts; `go run ./cmd/generate-types` remains idempotent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitattributes | 4 ++++ cmd/generate-types/main_test.go | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..338b1be9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Pin generated/source files that are byte-compared in tests to LF so they are +# checked out identically on Windows (core.autocrlf=true would otherwise yield +# CRLF and break cmd/generate-types TestContractsInSync). +frontend/src/types/contracts.ts text eol=lf diff --git a/cmd/generate-types/main_test.go b/cmd/generate-types/main_test.go index 80c401e6..6ed11a8c 100644 --- a/cmd/generate-types/main_test.go +++ b/cmd/generate-types/main_test.go @@ -1,11 +1,20 @@ package main import ( + "bytes" "os" "path/filepath" "testing" ) +// normalizeEOL strips carriage returns so the comparison is robust on Windows, +// where git may check out contracts.ts with CRLF endings (core.autocrlf=true) +// even though the generator always emits LF. .gitattributes pins this file to +// LF, but normalizing here keeps the test green regardless of git config. +func normalizeEOL(b []byte) []byte { + return bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n")) +} + // 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 @@ -14,10 +23,10 @@ import ( // 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. +// 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. @@ -30,7 +39,7 @@ func TestContractsInSync(t *testing.T) { generated := []byte(generateFileContent()) - if string(committed) == string(generated) { + if bytes.Equal(normalizeEOL(committed), normalizeEOL(generated)) { return }