fix: enhance error handling in isSpacetimedbReachable and update private table tests for clearer rejection paths

This commit is contained in:
Aadesh Kheria 2026-04-20 15:41:21 -07:00
parent dc5ab66b39
commit 2532632958
3 changed files with 36 additions and 17 deletions

View File

@ -36,8 +36,11 @@ export async function isSpacetimedbReachable(): Promise<boolean> {
signal: controller.signal,
});
return res.ok;
} catch {
return false;
} catch (err) {
const isAbort = err instanceof DOMException && err.name === "AbortError";
const isNetwork = err instanceof TypeError;
if (isAbort || isNetwork) return false;
throw err;
} finally {
clearTimeout(timeout);
}

View File

@ -66,14 +66,20 @@ describe.skipIf(!canRun)("private log tables and view gating", () => {
// Private table: SpacetimeDB should either reject the query outright or return
// zero rows to non-operators. Either outcome is acceptable — the invariant is
// "the caller does not see any private-table rows."
// "the caller does not see any private-table rows." If rejection, the error
// must come from our own sqlQuery helper's HTTP-4xx path against this exact
// table (not a network blip, not a helper regression).
const stranger = await mintIdentity();
try {
const { rows } = await sqlQuery(stranger.token, "SELECT * FROM mcp_call_log");
expect(rows.length).toBe(0);
} catch (err) {
// Rejection path: the error confirms the table isn't publicly readable.
expect(err).toBeInstanceOf(Error);
const result = await sqlQuery(stranger.token, "SELECT * FROM mcp_call_log")
.then(r => ({ ok: true as const, rows: r.rows }))
.catch(err => ({ ok: false as const, err }));
if (result.ok) {
expect(result.rows.length).toBe(0);
} else {
expect(result.err).toBeInstanceOf(Error);
expect((result.err as Error).message).toMatch(
/SQL\s+"SELECT \* FROM mcp_call_log"\s+failed: HTTP 4\d\d/,
);
}
});
@ -109,11 +115,16 @@ describe.skipIf(!canRun)("private log tables and view gating", () => {
expect(seed.ok).toBe(true);
const stranger = await mintIdentity();
try {
const { rows } = await sqlQuery(stranger.token, "SELECT * FROM ai_query_log");
expect(rows.length).toBe(0);
} catch (err) {
expect(err).toBeInstanceOf(Error);
const result = await sqlQuery(stranger.token, "SELECT * FROM ai_query_log")
.then(r => ({ ok: true as const, rows: r.rows }))
.catch(err => ({ ok: false as const, err }));
if (result.ok) {
expect(result.rows.length).toBe(0);
} else {
expect(result.err).toBeInstanceOf(Error);
expect((result.err as Error).message).toMatch(
/SQL\s+"SELECT \* FROM ai_query_log"\s+failed: HTTP 4\d\d/,
);
}
},
);

View File

@ -60,7 +60,13 @@ export default function App() {
enrolledRef.current.set(key, promise);
return await promise;
}, [user]);
const memoizedEnsureEnrolled = useMemo(() => user ? ensureEnrolled : undefined, [user, ensureEnrolled]);
const isAiChatReviewer = Boolean(
(user?.clientReadOnlyMetadata as Record<string, unknown> | null)?.isAiChatReviewer,
);
const memoizedEnsureEnrolled = useMemo(
() => (user && isAiChatReviewer) ? ensureEnrolled : undefined,
[user, isAiChatReviewer, ensureEnrolled],
);
const { rows, connectionState } = useMcpCallLogs(memoizedEnsureEnrolled);
const { rows: usageRows, connectionState: usageConnectionState } = useAiQueryLogs(memoizedEnsureEnrolled);
@ -88,8 +94,7 @@ export default function App() {
);
}
const metadata = user.clientReadOnlyMetadata as Record<string, unknown> | null;
if (!metadata?.isAiChatReviewer) {
if (!isAiChatReviewer) {
return (
<div className="flex items-center justify-center h-screen bg-gray-50">
<div className="text-center">