FN-9103: align auth route tests with model refresh caching

Update dashboard auth route coverage and engine mocks for cached model registry refresh behavior.

- provide faithful bounded model registry refresh helpers in the dashboard engine mock
- reset refresh cache state between API key route tests
- verify API key saves invalidate the cache without redundant refreshes

Files changed:
 .../dashboard/src/__tests__/routes-auth.test.ts    | 20 ++++--
 packages/dashboard/src/test/mockCoreEngine.ts      | 74 ++++++++++++++++++++++
 2 files changed, 90 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-9103

Fusion-Task-Lineage: d09453b1-08a5-4cde-849c-31e22bd61c0f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-15 14:40:55 -07:00
parent 6401fdea89
commit 38d128ca29
2 changed files with 90 additions and 4 deletions

View File

@@ -43,6 +43,7 @@ import * as updateCheckModule from "../update-check.js";
import { __setAgentReflectionServiceForTests } from "../routes/register-agent-reflection-rating-routes.js";
import { parseGitHubCopilotDeviceCode } from "../routes/register-auth-routes.js";
import { createAuthMiddleware } from "../auth-middleware.js";
import { __resetModelRegistryRefreshCacheForTests } from "../model-registry-refresh-cache.js";
// Mock @fusion/core for gh CLI auth checks
const mockCentralListProjects = vi.fn().mockResolvedValue([]);
@@ -3491,6 +3492,7 @@ describe("POST /auth/api-key", () => {
beforeEach(() => {
store = createMockStore();
authStorage = createMockAuthStorage();
__resetModelRegistryRefreshCacheForTests();
});
function buildApp(options?: {
@@ -3640,24 +3642,34 @@ describe("POST /auth/api-key", () => {
expect(res.body.error).toContain("not supported");
});
it("runs post-save refresh hook and model registry refresh for opencode-go", async () => {
it("runs post-save refresh hook and invalidates the model registry cache for opencode-go", async () => {
const onApiKeySaved = vi.fn().mockResolvedValue({ registeredCount: 3, reason: "no-models-from-cli" });
const modelRegistry = { refresh: vi.fn(), getAvailable: vi.fn().mockReturnValue([]) } as unknown as ModelRegistryLike;
const modelRegistry = {
refresh: vi.fn(async () => undefined),
getAvailable: vi.fn().mockReturnValue([]),
} as unknown as ModelRegistryLike;
(authStorage.getApiKeyProviders as ReturnType<typeof vi.fn>).mockReturnValue([
{ id: "openrouter", name: "OpenRouter" },
{ id: "opencode-go", name: "Opencode (Go)" },
]);
const app = buildApp({ onApiKeySaved, modelRegistry });
const res = await REQUEST(buildApp({ onApiKeySaved, modelRegistry }), "POST", "/api/auth/api-key", JSON.stringify({
await GET(app, "/api/models");
expect(modelRegistry.refresh).toHaveBeenCalledOnce();
const res = await REQUEST(app, "POST", "/api/auth/api-key", JSON.stringify({
provider: "opencode-go",
apiKey: "sk-test",
}), { "Content-Type": "application/json" });
expect(res.status).toBe(200);
expect(onApiKeySaved).toHaveBeenCalledWith("opencode-go");
expect(modelRegistry.refresh).toHaveBeenCalled();
expect(modelRegistry.refresh).toHaveBeenCalledOnce();
expect(res.body.modelsRefreshed).toBe(3);
expect(res.body.refreshReason).toBe("no-models-from-cli");
await GET(app, "/api/models");
expect(modelRegistry.refresh).toHaveBeenCalledTimes(2);
});
it("returns success when post-save refresh hook throws", async () => {

View File

@@ -11,6 +11,74 @@ type AnyMock = Mock;
const fallbackFns = new Map<string, AnyMock>();
const DEFAULT_MODEL_REGISTRY_REFRESH_TIMEOUT_MS = 15_000;
type MockRefreshableModelRegistry = {
refresh: () => unknown;
modelRuntime?: {
refresh: (options?: { allowNetwork?: boolean; signal?: AbortSignal; force?: boolean }) => Promise<unknown>;
};
};
type MockModelRegistryRefreshOptions = {
timeoutMs?: number;
allowNetwork?: boolean;
log?: (message: string) => void;
};
type MockModelRegistryRefreshOutcome = "completed" | "timed_out" | "failed";
function boundMockModelRegistryRefresh(
underlying: Promise<unknown>,
options: Pick<MockModelRegistryRefreshOptions, "timeoutMs" | "log"> = {},
controller?: AbortController,
): Promise<MockModelRegistryRefreshOutcome> {
const timeoutMs = options.timeoutMs ?? DEFAULT_MODEL_REGISTRY_REFRESH_TIMEOUT_MS;
let timedOut = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
timedOut = true;
controller?.abort();
reject(new Error(`Model registry refresh timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([underlying, timeout])
.then(() => "completed" as const)
.catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
if (timedOut || controller?.signal.aborted || /timed out/i.test(message)) {
options.log?.(`Model registry refresh timed out after ${timeoutMs}ms; continuing with cached models`);
return "timed_out" as const;
}
options.log?.(`Model registry refresh failed: ${message}`);
return "failed" as const;
})
.finally(() => {
if (timer) clearTimeout(timer);
});
}
/*
FNXC:ModelCatalog 2026-08-15-21:23:
Dashboard route tests wholesale-mock @fusion/engine, but FN-8902's request cache needs real
refresh starters and bounders. Keep this light mock faithful so GET /models invokes the registry
instead of treating fallback vi.fn() output as a failed refresh.
*/
function startMockModelRegistryRefresh(
modelRegistry: MockRefreshableModelRegistry,
options: MockModelRegistryRefreshOptions = {},
): { underlying: Promise<unknown>; bounded: Promise<MockModelRegistryRefreshOutcome> } {
const controller = new AbortController();
const runtime = modelRegistry.modelRuntime;
const underlying = typeof runtime?.refresh === "function"
? Promise.resolve().then(() => runtime.refresh({ allowNetwork: options.allowNetwork ?? true, signal: controller.signal }))
: Promise.resolve().then(() => modelRegistry.refresh());
void underlying.catch(() => {});
return { underlying, bounded: boundMockModelRegistryRefresh(underlying, options, controller) };
}
function getFallback(name: string): AnyMock {
if (!fallbackFns.has(name)) fallbackFns.set(name, vi.fn());
return fallbackFns.get(name)!;
@@ -47,6 +115,12 @@ export function createEngineMock(overrides: AnyModule = {}): AnyModule {
return withFallbackFunctions(actual, {
createFnAgent: vi.fn(),
promptWithFallback: vi.fn(),
DEFAULT_MODEL_REGISTRY_REFRESH_TIMEOUT_MS,
startFusionModelRegistryRefresh: startMockModelRegistryRefresh,
boundExistingModelRegistryRefresh: boundMockModelRegistryRefresh,
refreshFusionModelRegistry: (modelRegistry: MockRefreshableModelRegistry, options: MockModelRegistryRefreshOptions = {}) => (
startMockModelRegistryRefresh(modelRegistry, options).bounded
),
/*
FNXC:TestSkills 2026-06-17-19:33:
Dashboard route tests mock @fusion/engine wholesale, so skill-aware planning lanes need a shaped session-skill helper result instead of the fallback vi.fn() returning undefined.