Skip to content

Commit 75877fc

Browse files
committed
refactor: consolidate provider types to single source of truth
Eliminates triple-definition of ProviderConfigInfo/AWSCredentialStatus types that existed in providerService.ts, Settings/types.ts, and the oRPC schema. Now the Zod schema is the single source of truth, with TypeScript types derived via z.infer. Adds conformance tests that validate oRPC schemas preserve all fields when parsing - this would have caught the missing couponCodeSet/aws fields bug.
1 parent 929cf13 commit 75877fc

File tree

5 files changed

+160
-40
lines changed

5 files changed

+160
-40
lines changed
Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
11
import type { ReactNode } from "react";
2+
import type {
3+
AWSCredentialStatus,
4+
ProviderConfigInfo,
5+
ProvidersConfigMap,
6+
} from "@/common/orpc/types";
7+
8+
// Re-export types for local usage
9+
export type { AWSCredentialStatus, ProvidersConfigMap };
10+
11+
// Alias for backward compatibility (ProviderConfigDisplay was the old name)
12+
export type ProviderConfigDisplay = ProviderConfigInfo;
213

314
export interface SettingsSection {
415
id: string;
516
label: string;
617
icon: ReactNode;
718
component: React.ComponentType;
819
}
9-
10-
/** AWS credential status for Bedrock provider */
11-
export interface AWSCredentialStatus {
12-
region?: string;
13-
bearerTokenSet: boolean;
14-
accessKeyIdSet: boolean;
15-
secretAccessKeySet: boolean;
16-
}
17-
18-
export interface ProviderConfigDisplay {
19-
apiKeySet: boolean;
20-
baseUrl?: string;
21-
models?: string[];
22-
/** AWS-specific fields (only present for bedrock provider) */
23-
aws?: AWSCredentialStatus;
24-
/** Mux Gateway-specific fields */
25-
couponCodeSet?: boolean;
26-
}
27-
28-
export type ProvidersConfigMap = Record<string, ProviderConfigDisplay>;

src/common/orpc/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090

9191
// API router schemas
9292
export {
93+
AWSCredentialStatusSchema,
9394
general,
9495
menu,
9596
projects,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, expect, it } from "bun:test";
2+
import {
3+
AWSCredentialStatusSchema,
4+
ProviderConfigInfoSchema,
5+
ProvidersConfigMapSchema,
6+
} from "./api";
7+
import type { AWSCredentialStatus, ProviderConfigInfo, ProvidersConfigMap } from "../types";
8+
9+
/**
10+
* Schema conformance tests for provider types.
11+
*
12+
* These tests ensure that the Zod schemas preserve all fields when parsing data.
13+
* oRPC uses these schemas for output validation and strips fields not in the schema,
14+
* so any field present in the TypeScript type MUST be present in the schema.
15+
*
16+
* If these tests fail, it means the schema is missing fields that the backend
17+
* service returns, which would cause data loss when crossing the IPC boundary.
18+
*/
19+
describe("ProviderConfigInfoSchema conformance", () => {
20+
it("preserves all AWSCredentialStatus fields", () => {
21+
const full: AWSCredentialStatus = {
22+
region: "us-east-1",
23+
bearerTokenSet: true,
24+
accessKeyIdSet: true,
25+
secretAccessKeySet: false,
26+
};
27+
28+
const parsed = AWSCredentialStatusSchema.parse(full);
29+
30+
// Verify no fields were stripped
31+
expect(parsed).toEqual(full);
32+
expect(Object.keys(parsed).sort()).toEqual(Object.keys(full).sort());
33+
});
34+
35+
it("preserves all ProviderConfigInfo fields (base case)", () => {
36+
const full: ProviderConfigInfo = {
37+
apiKeySet: true,
38+
baseUrl: "https://api.example.com",
39+
models: ["model-a", "model-b"],
40+
};
41+
42+
const parsed = ProviderConfigInfoSchema.parse(full);
43+
44+
expect(parsed).toEqual(full);
45+
expect(Object.keys(parsed).sort()).toEqual(Object.keys(full).sort());
46+
});
47+
48+
it("preserves all ProviderConfigInfo fields (with AWS/Bedrock)", () => {
49+
const full: ProviderConfigInfo = {
50+
apiKeySet: false,
51+
baseUrl: undefined,
52+
models: [],
53+
aws: {
54+
region: "eu-west-1",
55+
bearerTokenSet: false,
56+
accessKeyIdSet: true,
57+
secretAccessKeySet: true,
58+
},
59+
};
60+
61+
const parsed = ProviderConfigInfoSchema.parse(full);
62+
63+
expect(parsed).toEqual(full);
64+
// Check nested aws object is preserved
65+
expect(parsed.aws).toEqual(full.aws);
66+
});
67+
68+
it("preserves all ProviderConfigInfo fields (with couponCodeSet)", () => {
69+
const full: ProviderConfigInfo = {
70+
apiKeySet: true,
71+
couponCodeSet: true,
72+
};
73+
74+
const parsed = ProviderConfigInfoSchema.parse(full);
75+
76+
expect(parsed).toEqual(full);
77+
expect(parsed.couponCodeSet).toBe(true);
78+
});
79+
80+
it("preserves all ProviderConfigInfo fields (full object with all optional fields)", () => {
81+
// This is the most comprehensive test - includes ALL possible fields
82+
const full: ProviderConfigInfo = {
83+
apiKeySet: true,
84+
baseUrl: "https://custom.endpoint.com",
85+
models: ["claude-3-opus", "claude-3-sonnet"],
86+
aws: {
87+
region: "ap-northeast-1",
88+
bearerTokenSet: true,
89+
accessKeyIdSet: true,
90+
secretAccessKeySet: true,
91+
},
92+
couponCodeSet: true,
93+
};
94+
95+
const parsed = ProviderConfigInfoSchema.parse(full);
96+
97+
// Deep equality check
98+
expect(parsed).toEqual(full);
99+
100+
// Explicit field-by-field verification for clarity
101+
expect(parsed.apiKeySet).toBe(full.apiKeySet);
102+
expect(parsed.baseUrl).toBe(full.baseUrl);
103+
expect(parsed.models).toEqual(full.models);
104+
expect(parsed.aws).toEqual(full.aws);
105+
expect(parsed.couponCodeSet).toBe(full.couponCodeSet);
106+
});
107+
108+
it("preserves ProvidersConfigMap with multiple providers", () => {
109+
const full: ProvidersConfigMap = {
110+
anthropic: {
111+
apiKeySet: true,
112+
models: ["claude-3-opus"],
113+
},
114+
bedrock: {
115+
apiKeySet: false,
116+
aws: {
117+
region: "us-west-2",
118+
bearerTokenSet: false,
119+
accessKeyIdSet: true,
120+
secretAccessKeySet: true,
121+
},
122+
},
123+
"mux-gateway": {
124+
apiKeySet: false,
125+
couponCodeSet: true,
126+
models: ["anthropic/claude-sonnet-4-5"],
127+
},
128+
};
129+
130+
const parsed = ProvidersConfigMapSchema.parse(full);
131+
132+
expect(parsed).toEqual(full);
133+
expect(Object.keys(parsed)).toEqual(Object.keys(full));
134+
});
135+
});

src/common/orpc/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import type {
1616

1717
export type BranchListResult = z.infer<typeof schemas.BranchListResultSchema>;
1818
export type SendMessageOptions = z.infer<typeof schemas.SendMessageOptionsSchema>;
19+
20+
// Provider types (single source of truth - derived from schemas)
21+
export type AWSCredentialStatus = z.infer<typeof schemas.AWSCredentialStatusSchema>;
22+
export type ProviderConfigInfo = z.infer<typeof schemas.ProviderConfigInfoSchema>;
23+
export type ProvidersConfigMap = z.infer<typeof schemas.ProvidersConfigMapSchema>;
1924
export type ImagePart = z.infer<typeof schemas.ImagePartSchema>;
2025
export type WorkspaceChatMessage = z.infer<typeof schemas.WorkspaceChatMessageSchema>;
2126
export type CaughtUpMessage = z.infer<typeof schemas.CaughtUpMessageSchema>;

src/node/services/providerService.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,14 @@ import { EventEmitter } from "events";
22
import type { Config } from "@/node/config";
33
import { SUPPORTED_PROVIDERS } from "@/common/constants/providers";
44
import type { Result } from "@/common/types/result";
5-
6-
/** AWS credential status for Bedrock provider */
7-
export interface AWSCredentialStatus {
8-
region?: string;
9-
bearerTokenSet: boolean;
10-
accessKeyIdSet: boolean;
11-
secretAccessKeySet: boolean;
12-
}
13-
14-
export interface ProviderConfigInfo {
15-
apiKeySet: boolean;
16-
baseUrl?: string;
17-
models?: string[];
18-
/** AWS-specific fields (only present for bedrock provider) */
19-
aws?: AWSCredentialStatus;
20-
/** Mux Gateway-specific fields */
21-
couponCodeSet?: boolean;
22-
}
23-
24-
export type ProvidersConfigMap = Record<string, ProviderConfigInfo>;
5+
import type {
6+
AWSCredentialStatus,
7+
ProviderConfigInfo,
8+
ProvidersConfigMap,
9+
} from "@/common/orpc/types";
10+
11+
// Re-export types for backward compatibility
12+
export type { AWSCredentialStatus, ProviderConfigInfo, ProvidersConfigMap };
2513

2614
export class ProviderService {
2715
private readonly emitter = new EventEmitter();

0 commit comments

Comments
 (0)