This commit is contained in:
abhinav7x94 2026-08-26 04:03:35 +05:30 committed by GitHub
commit c4720021c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 6 deletions

View file

@ -29,5 +29,9 @@ jobs:
- name: Run TypeScript type checking
run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
- name: Run web unit tests
working-directory: apps/web
run: bun test
- name: Run Biome CI (format & lint on changed files)
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched

View file

@ -55,6 +55,7 @@ import { useViewMode } from "@/lib/view-mode-context"
import type { ViewParamValue } from "@/lib/search-params"
import { parseAsString, parseAsStringEnum, useQueryState } from "nuqs"
import { addDocumentParam, docParam } from "@/lib/search-params"
import { revokePluginKey } from "@/components/integrations/plugin-key-revocation"
import {
useCallback,
useEffect,
@ -2971,9 +2972,11 @@ export function IntegrationsView({
const handleRevokePluginKey = async (keyId: string) => {
try {
await authClient.apiKey.delete({ keyId })
toast.success("Plugin disconnected")
refetchKeys()
await revokePluginKey({
deleteKey: () => authClient.apiKey.delete({ keyId }),
onSuccess: () => toast.success("Plugin disconnected"),
refetch: () => void refetchKeys(),
})
} catch {
toast.error("Failed to disconnect plugin")
}

View file

@ -0,0 +1,107 @@
import { describe, expect, it, mock } from "bun:test"
import { readFile } from "node:fs/promises"
import { revokePluginKey } from "./plugin-key-revocation"
describe("revokePluginKey", () => {
it("does not report success or refetch when Better Auth resolves an error", async () => {
const onSuccess = mock(() => {})
const refetch = mock(() => {})
await expect(
revokePluginKey({
deleteKey: async () => ({
data: null,
error: { message: "API key not found" },
}),
onSuccess,
refetch,
}),
).rejects.toThrow("API key not found")
expect(onSuccess).not.toHaveBeenCalled()
expect(refetch).not.toHaveBeenCalled()
})
it("does not report success or refetch when deletion rejects", async () => {
const onSuccess = mock(() => {})
const refetch = mock(() => {})
const deleteError = new Error("Network unavailable")
await expect(
revokePluginKey({
deleteKey: async () => {
throw deleteError
},
onSuccess,
refetch,
}),
).rejects.toBe(deleteError)
expect(onSuccess).not.toHaveBeenCalled()
expect(refetch).not.toHaveBeenCalled()
})
it("uses a fallback message when Better Auth omits one", async () => {
const onSuccess = mock(() => {})
const refetch = mock(() => {})
await expect(
revokePluginKey({
deleteKey: async () => ({ data: null, error: {} }),
onSuccess,
refetch,
}),
).rejects.toThrow("Failed to disconnect plugin")
expect(onSuccess).not.toHaveBeenCalled()
expect(refetch).not.toHaveBeenCalled()
})
it("reports success and refetches after Better Auth resolves success", async () => {
const onSuccess = mock(() => {})
const refetch = mock(() => {})
await revokePluginKey({
deleteKey: async () => ({
data: { success: true },
error: null,
}),
onSuccess,
refetch,
})
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(refetch).toHaveBeenCalledTimes(1)
})
})
describe("plugin revoke handler wiring", () => {
const handlers = [
{
name: "integrations view",
file: new URL("../integrations-view.tsx", import.meta.url),
handler: "handleRevokePluginKey",
},
{
name: "plugin detail",
file: new URL("./plugins-detail.tsx", import.meta.url),
handler: "handleRevoke",
},
]
for (const { name, file, handler } of handlers) {
it(`${name} delegates revocation through the guarded helper`, async () => {
const source = await readFile(file, "utf8")
const handlerStart = source.indexOf(`const ${handler} = async`)
const handlerSource = source.slice(handlerStart, handlerStart + 500)
expect(handlerStart).toBeGreaterThanOrEqual(0)
expect(handlerSource).toContain("await revokePluginKey({")
expect(handlerSource).toMatch(
/deleteKey:\s*\(\)\s*=>\s*authClient\.apiKey\.delete\(\{\s*keyId\s*\}\)/,
)
expect(
handlerSource.match(/authClient\.apiKey\.delete/g) ?? [],
).toHaveLength(1)
expect(handlerSource).toContain("onSuccess:")
expect(handlerSource).toContain("refetch:")
})
}
})

View file

@ -0,0 +1,24 @@
type PluginKeyDeleteResult = {
data: unknown
error: { message?: string } | null
}
type RevokePluginKeyOptions = {
deleteKey: () => Promise<PluginKeyDeleteResult>
onSuccess: () => void
refetch: () => void
}
export async function revokePluginKey({
deleteKey,
onSuccess,
refetch,
}: RevokePluginKeyOptions): Promise<void> {
const result = await deleteKey()
if (result.error) {
throw new Error(result.error.message ?? "Failed to disconnect plugin")
}
onSuccess()
refetch()
}

View file

@ -30,6 +30,7 @@ import {
type PluginInfo,
} from "@/lib/plugin-catalog"
import { INSET, InstallSteps, PillButton } from "./install-steps"
import { revokePluginKey } from "./plugin-key-revocation"
import { usePromoCode } from "@/hooks/use-promo-code"
interface ConnectedPlugin {
@ -560,9 +561,11 @@ export function PluginsDetail() {
const handleRevoke = async (keyId: string) => {
try {
await authClient.apiKey.delete({ keyId })
toast.success("Plugin disconnected")
refetchKeys()
await revokePluginKey({
deleteKey: () => authClient.apiKey.delete({ keyId }),
onSuccess: () => toast.success("Plugin disconnected"),
refetch: () => void refetchKeys(),
})
} catch {
toast.error("Failed to disconnect plugin")
}