Explorar o código

fix: make computeVendorKey async for Server Actions compliance

The computeVendorKey function was exported from a file with "use server"
directive but was not async, causing Next.js build to fail with:
"Server Actions must be async functions."

Changes:
- Made computeVendorKey async and return Promise<string | null>
- Added await to all call sites in provider-endpoints.ts
- Added await to call site in providers.ts (reclusterProviderVendors)
- Updated all test cases to use async/await

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21436692308

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
claude[bot] hai 2 semanas
pai
achega
6543af8de1

+ 1 - 1
src/actions/providers.ts

@@ -3637,7 +3637,7 @@ export async function reclusterProviderVendors(args: {
 
     // Calculate new vendor key for each provider
     for (const provider of allProviders) {
-      const newVendorKey = computeVendorKey({
+      const newVendorKey = await computeVendorKey({
         providerUrl: provider.url,
         websiteUrl: provider.websiteUrl,
       });

+ 4 - 4
src/repository/provider-endpoints.ts

@@ -109,10 +109,10 @@ function normalizeHostWithPort(rawUrl: string): string | null {
  *   - Missing port: use protocol default (http=80, https=443)
  *   - No scheme: assume https
  */
-export function computeVendorKey(input: {
+export async function computeVendorKey(input: {
   providerUrl: string;
   websiteUrl?: string | null;
-}): string | null {
+}): Promise<string | null> {
   const { providerUrl, websiteUrl } = input;
 
   // Case 1: websiteUrl is non-empty - use hostname only (existing behavior)
@@ -259,7 +259,7 @@ export async function getOrCreateProviderVendorIdFromUrls(input: {
   displayName?: string | null;
 }): Promise<number> {
   // Use new computeVendorKey for consistent vendor key calculation
-  const websiteDomain = computeVendorKey({
+  const websiteDomain = await computeVendorKey({
     providerUrl: input.providerUrl,
     websiteUrl: input.websiteUrl,
   });
@@ -379,7 +379,7 @@ export async function backfillProviderVendorsFromProviders(): Promise<{
       stats.processed++;
 
       // Use new computeVendorKey for consistent vendor key calculation
-      const vendorKey = computeVendorKey({
+      const vendorKey = await computeVendorKey({
         providerUrl: row.url,
         websiteUrl: row.websiteUrl,
       });

+ 43 - 43
tests/unit/repository/provider-endpoints-vendor-key.test.ts

@@ -3,36 +3,36 @@ import { computeVendorKey } from "@/repository/provider-endpoints";
 
 describe("computeVendorKey", () => {
   describe("with websiteUrl (priority over providerUrl)", () => {
-    test("returns hostname only, ignoring port", () => {
+    test("returns hostname only, ignoring port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://api.example.com:8080/v1/messages",
           websiteUrl: "https://example.com:3000",
         })
       ).toBe("example.com");
     });
 
-    test("strips www prefix", () => {
+    test("strips www prefix", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://api.example.com",
           websiteUrl: "https://www.example.com",
         })
       ).toBe("example.com");
     });
 
-    test("lowercases hostname", () => {
+    test("lowercases hostname", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://api.Example.COM",
           websiteUrl: "https://WWW.EXAMPLE.COM",
         })
       ).toBe("example.com");
     });
 
-    test("handles websiteUrl without protocol", () => {
+    test("handles websiteUrl without protocol", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://api.example.com",
           websiteUrl: "example.com",
         })
@@ -41,21 +41,21 @@ describe("computeVendorKey", () => {
   });
 
   describe("without websiteUrl (fallback to providerUrl with host:port)", () => {
-    test("returns host:port for IP address", () => {
+    test("returns host:port for IP address", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://192.168.1.1:8080/v1/messages",
           websiteUrl: null,
         })
       ).toBe("192.168.1.1:8080");
     });
 
-    test("different ports create different keys", () => {
-      const key1 = computeVendorKey({
+    test("different ports create different keys", async () => {
+      const key1 = await computeVendorKey({
         providerUrl: "http://192.168.1.1:8080/v1/messages",
         websiteUrl: null,
       });
-      const key2 = computeVendorKey({
+      const key2 = await computeVendorKey({
         providerUrl: "http://192.168.1.1:9090/v1/messages",
         websiteUrl: null,
       });
@@ -64,63 +64,63 @@ describe("computeVendorKey", () => {
       expect(key1).not.toBe(key2);
     });
 
-    test("uses default port 443 for https without explicit port", () => {
+    test("uses default port 443 for https without explicit port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://api.example.com/v1/messages",
           websiteUrl: null,
         })
       ).toBe("api.example.com:443");
     });
 
-    test("uses default port 80 for http without explicit port", () => {
+    test("uses default port 80 for http without explicit port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://api.example.com/v1/messages",
           websiteUrl: null,
         })
       ).toBe("api.example.com:80");
     });
 
-    test("assumes https (port 443) for URL without scheme", () => {
+    test("assumes https (port 443) for URL without scheme", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "api.example.com/v1/messages",
           websiteUrl: null,
         })
       ).toBe("api.example.com:443");
     });
 
-    test("strips www prefix in host:port mode", () => {
+    test("strips www prefix in host:port mode", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://www.example.com:8080/v1/messages",
           websiteUrl: null,
         })
       ).toBe("example.com:8080");
     });
 
-    test("lowercases hostname in host:port mode", () => {
+    test("lowercases hostname in host:port mode", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://API.EXAMPLE.COM:8080/v1/messages",
           websiteUrl: null,
         })
       ).toBe("api.example.com:8080");
     });
 
-    test("handles localhost with port", () => {
+    test("handles localhost with port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://localhost:3000/v1/messages",
           websiteUrl: null,
         })
       ).toBe("localhost:3000");
     });
 
-    test("handles localhost without explicit port", () => {
+    test("handles localhost without explicit port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://localhost/v1/messages",
           websiteUrl: null,
         })
@@ -129,27 +129,27 @@ describe("computeVendorKey", () => {
   });
 
   describe("IPv6 addresses", () => {
-    test("formats IPv6 with brackets and port", () => {
+    test("formats IPv6 with brackets and port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://[::1]:8080/v1/messages",
           websiteUrl: null,
         })
       ).toBe("[::1]:8080");
     });
 
-    test("handles IPv6 without explicit port", () => {
+    test("handles IPv6 without explicit port", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "https://[::1]/v1/messages",
           websiteUrl: null,
         })
       ).toBe("[::1]:443");
     });
 
-    test("handles full IPv6 address", () => {
+    test("handles full IPv6 address", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://[2001:db8::1]:9000/v1/messages",
           websiteUrl: null,
         })
@@ -158,45 +158,45 @@ describe("computeVendorKey", () => {
   });
 
   describe("edge cases", () => {
-    test("returns null for empty providerUrl", () => {
+    test("returns null for empty providerUrl", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "",
           websiteUrl: null,
         })
       ).toBeNull();
     });
 
-    test("returns null for whitespace-only providerUrl", () => {
+    test("returns null for whitespace-only providerUrl", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "   ",
           websiteUrl: null,
         })
       ).toBeNull();
     });
 
-    test("uses providerUrl when websiteUrl is empty string", () => {
+    test("uses providerUrl when websiteUrl is empty string", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://192.168.1.1:8080/v1/messages",
           websiteUrl: "",
         })
       ).toBe("192.168.1.1:8080");
     });
 
-    test("uses providerUrl when websiteUrl is whitespace", () => {
+    test("uses providerUrl when websiteUrl is whitespace", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "http://192.168.1.1:8080/v1/messages",
           websiteUrl: "   ",
         })
       ).toBe("192.168.1.1:8080");
     });
 
-    test("returns null for truly invalid URL", () => {
+    test("returns null for truly invalid URL", async () => {
       expect(
-        computeVendorKey({
+        await computeVendorKey({
           providerUrl: "://invalid",
           websiteUrl: null,
         })