test(FN-4907): cover stale settings invalidation and sync refresh paths
Fusion-Task-Id: FN-4907 Fusion-Task-Lineage: e84d5357-0719-4cc7-ab3f-f665ac83b218
This commit is contained in:
committed by
gsxdsm
parent
4416de159c
commit
a2bd876eb4
@@ -573,6 +573,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
PeerExchangeService: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
})),
|
||||
TriageProcessor: mocks.triageCtor,
|
||||
TaskExecutor: mocks.executorCtor,
|
||||
|
||||
@@ -715,6 +715,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
PeerExchangeService: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
})),
|
||||
shouldUseHybridExecutor: mockShouldUseHybridExecutor,
|
||||
HybridExecutor: mockHybridExecutorCtor,
|
||||
@@ -2626,6 +2627,7 @@ describe("runDashboard — mesh lifecycle ownership", () => {
|
||||
expect(peerExchangeCtor.mock.calls.length).toBeGreaterThan(baselineCalls);
|
||||
const peerExchangeInstance = peerExchangeCtor.mock.results.at(-1)?.value;
|
||||
expect(peerExchangeInstance.start).toHaveBeenCalledTimes(1);
|
||||
expect(peerExchangeInstance.updateGlobalSettings).toHaveBeenCalledTimes(1);
|
||||
expect(startDiscovery).toHaveBeenCalledWith(expect.objectContaining({
|
||||
broadcast: true,
|
||||
listen: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ const mockApplyRemoteSettings = vi.fn();
|
||||
const mockGetSettingsForSync = vi.fn();
|
||||
const mockGetAuthMaterialSnapshot = vi.fn();
|
||||
const mockApplyAuthMaterialSnapshot = vi.fn();
|
||||
const mockStoreUpdateGlobalSettings = vi.fn().mockResolvedValue({});
|
||||
const mockChatStoreInit = vi.fn().mockResolvedValue(undefined);
|
||||
const mockAgentStoreInit = vi.fn().mockResolvedValue(undefined);
|
||||
const mockAgentStoreGetAgent = vi.fn().mockResolvedValue(null);
|
||||
@@ -118,6 +119,10 @@ class MockStore extends EventEmitter {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async updateGlobalSettings(patch: Record<string, unknown>) {
|
||||
return mockStoreUpdateGlobalSettings(patch);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────
|
||||
@@ -177,6 +182,7 @@ describe("Node settings sync routes", () => {
|
||||
mockGetSettingsSyncState.mockResolvedValue(null);
|
||||
mockUpdateSettingsSyncState.mockResolvedValue({});
|
||||
mockApplyRemoteSettings.mockResolvedValue({ success: true, globalCount: 1, projectCount: 1, authCount: 0 });
|
||||
mockStoreUpdateGlobalSettings.mockReset();
|
||||
mockGetSettingsForSync.mockResolvedValue({});
|
||||
mockGetAuthMaterialSnapshot.mockReturnValue({
|
||||
version: 1,
|
||||
@@ -1026,6 +1032,34 @@ describe("Node settings sync routes", () => {
|
||||
expect(mockApplyRemoteSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies inbound global settings via store.updateGlobalSettings when local values are unset", async () => {
|
||||
const localNode = createMockLocalNode();
|
||||
mockListNodes.mockResolvedValue([localNode]);
|
||||
mockApplyRemoteSettings.mockResolvedValue({
|
||||
success: true,
|
||||
globalCount: 2,
|
||||
projectCount: 0,
|
||||
authCount: 0,
|
||||
});
|
||||
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/settings/sync-receive",
|
||||
JSON.stringify({
|
||||
sourceNodeId: "node-remote-001",
|
||||
exportedAt: "2026-04-14T10:00:00.000Z",
|
||||
checksum: "abc123",
|
||||
version: 1,
|
||||
global: { dashboardCurrentNodeId: "node-remote-001", defaultProvider: "openai" },
|
||||
}),
|
||||
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockStoreUpdateGlobalSettings).toHaveBeenCalledWith({ dashboardCurrentNodeId: "node-remote-001" });
|
||||
});
|
||||
|
||||
it("returns 401 when auth header is missing", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
|
||||
@@ -6,6 +6,17 @@ import type { TaskStore, GlobalSettings, CustomProvider } from "@fusion/core";
|
||||
import { createApiRoutes } from "../../routes.js";
|
||||
import { request as performRequest } from "../../test-request.js";
|
||||
|
||||
const { mockInvalidateAllGlobalSettingsCaches } = vi.hoisted(() => ({
|
||||
mockInvalidateAllGlobalSettingsCaches: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../project-store-resolver.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../project-store-resolver.js")>("../../project-store-resolver.js");
|
||||
return {
|
||||
...actual,
|
||||
invalidateAllGlobalSettingsCaches: mockInvalidateAllGlobalSettingsCaches,
|
||||
};
|
||||
});
|
||||
|
||||
function createMockGlobalSettingsStore(settings: GlobalSettings) {
|
||||
return {
|
||||
getSettings: vi.fn(async () => settings),
|
||||
@@ -97,6 +108,7 @@ describe("custom provider routes", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
settings = {};
|
||||
mockInvalidateAllGlobalSettingsCaches.mockReset();
|
||||
});
|
||||
|
||||
it("GET /custom-providers returns empty array when none configured", async () => {
|
||||
@@ -165,6 +177,7 @@ describe("custom provider routes", () => {
|
||||
);
|
||||
expect(res.body.apiKey).toBe("sk-•••••5678");
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(mockInvalidateAllGlobalSettingsCaches).toHaveBeenCalledTimes(1);
|
||||
|
||||
const persisted = updates[0].customProviders as CustomProvider[];
|
||||
expect(persisted[0]?.apiKey).toBe("sk-my-secret-5678");
|
||||
@@ -264,6 +277,7 @@ describe("custom provider routes", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ success: true });
|
||||
expect(settings.customProviders).toEqual([]);
|
||||
expect(mockInvalidateAllGlobalSettingsCaches).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("DELETE /custom-providers/:id returns 404 for non-existent id", async () => {
|
||||
|
||||
Reference in New Issue
Block a user