FN-9189: expand dashboard concurrency callback coverage
Expand post-landing QA coverage for dashboard concurrency settings callbacks. - Exercise unset, configured, and worktree-bound settings across all TUI callback paths. - Verify pause callbacks use project defaults while the task store is still initializing. - Extend the dashboard harness with live settings events and a startup barrier. Files changed: .../dashboard-concurrency-settings.test.ts | 104 ++++++++++++++++++--- 1 file changed, 92 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-9189 Fusion-Task-Lineage: c373954a-7316-47d2-9821-75e0d34b22d4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -3,14 +3,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const harness = vi.hoisted(() => {
|
||||
let settings: Record<string, unknown> = {};
|
||||
let latestTui: FakeDashboardTui | undefined;
|
||||
let resolveTuiReady: ((tui: FakeDashboardTui) => void) | undefined;
|
||||
let tuiReady = new Promise<FakeDashboardTui>((resolve) => { resolveTuiReady = resolve; });
|
||||
let resolveTaskStoreBarrier: (() => void) | undefined;
|
||||
let taskStoreBarrier: Promise<void> = Promise.resolve();
|
||||
|
||||
class FakeDashboardTui {
|
||||
settingsPayloads: Array<Record<string, unknown>> = [];
|
||||
callbackPayloads: Array<Record<string, unknown>> = [];
|
||||
callbacks: Record<string, unknown> = {};
|
||||
interactiveData: Record<string, unknown> = {};
|
||||
boardScopedProjectPath: string | null = null;
|
||||
|
||||
constructor() {
|
||||
latestTui = this;
|
||||
resolveTuiReady?.(this);
|
||||
}
|
||||
|
||||
start = vi.fn(async () => undefined);
|
||||
@@ -19,11 +26,14 @@ const harness = vi.hoisted(() => {
|
||||
setSystemInfo = vi.fn();
|
||||
setReady = vi.fn();
|
||||
setTaskStats = vi.fn();
|
||||
setInteractiveData = vi.fn();
|
||||
setInteractiveData = vi.fn((data: Record<string, unknown>) => { this.interactiveData = data; });
|
||||
onBoardScopeChange = vi.fn();
|
||||
hydrateVitestKillSettings = vi.fn();
|
||||
log = vi.fn();
|
||||
setCallbacks = vi.fn((callbacks: Record<string, unknown>) => { this.callbacks = callbacks; });
|
||||
setCallbacks = vi.fn((callbacks: Record<string, unknown>) => {
|
||||
this.callbackPayloads.push(callbacks);
|
||||
this.callbacks = callbacks;
|
||||
});
|
||||
setSettings = vi.fn((payload: Record<string, unknown>) => { this.settingsPayloads.push(payload); });
|
||||
}
|
||||
|
||||
@@ -37,6 +47,9 @@ const harness = vi.hoisted(() => {
|
||||
}
|
||||
|
||||
const listeners = new Map<string, Array<(...args: any[]) => void>>();
|
||||
const emitStoreEvent = (event: string, ...args: unknown[]) => {
|
||||
for (const listener of listeners.get(event) ?? []) listener(...args);
|
||||
};
|
||||
const store: Record<string, any> = {
|
||||
on: (event: string, listener: (...args: any[]) => void) => {
|
||||
listeners.set(event, [...(listeners.get(event) ?? []), listener]);
|
||||
@@ -52,6 +65,9 @@ const harness = vi.hoisted(() => {
|
||||
getFusionDir: vi.fn(() => "/repo/.fusion"),
|
||||
getRootDir: vi.fn(() => "/repo"),
|
||||
getSettings: vi.fn(async () => settings),
|
||||
updateSettings: vi.fn(async (patch: Record<string, unknown>) => {
|
||||
settings = { ...settings, ...patch };
|
||||
}),
|
||||
getGlobalSettingsStore: vi.fn(() => ({ getSettings: vi.fn(async () => ({})), updateSettings: vi.fn(async () => undefined) })),
|
||||
getPluginStore: vi.fn(() => ({ init: vi.fn(async () => undefined) })),
|
||||
healthCheck: vi.fn(async () => ({ ok: true })),
|
||||
@@ -82,10 +98,25 @@ const harness = vi.hoisted(() => {
|
||||
store,
|
||||
getSettings: () => settings,
|
||||
latestTui: () => latestTui,
|
||||
waitForTui: () => tuiReady,
|
||||
emitStoreEvent,
|
||||
holdTaskStore: () => {
|
||||
taskStoreBarrier = new Promise<void>((resolve) => { resolveTaskStoreBarrier = resolve; });
|
||||
},
|
||||
releaseTaskStore: () => resolveTaskStoreBarrier?.(),
|
||||
createTaskStore: async () => {
|
||||
await taskStoreBarrier;
|
||||
return { taskStore: store, shutdown: vi.fn(async () => undefined) };
|
||||
},
|
||||
setSettings: (next: Record<string, unknown>) => { settings = next; },
|
||||
reset: () => {
|
||||
resolveTaskStoreBarrier?.();
|
||||
settings = {};
|
||||
latestTui = undefined;
|
||||
resolveTuiReady = undefined;
|
||||
tuiReady = new Promise<FakeDashboardTui>((resolve) => { resolveTuiReady = resolve; });
|
||||
taskStoreBarrier = Promise.resolve();
|
||||
resolveTaskStoreBarrier = undefined;
|
||||
vi.clearAllMocks();
|
||||
},
|
||||
};
|
||||
@@ -100,11 +131,16 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
}
|
||||
return {
|
||||
...actual,
|
||||
createTaskStoreForBackend: vi.fn(async () => ({ taskStore: harness.store, shutdown: vi.fn(async () => undefined) })),
|
||||
createTaskStoreForBackend: vi.fn(() => harness.createTaskStore()),
|
||||
AutomationStore: NoopStore,
|
||||
AgentStore: class extends NoopStore { listAgents = vi.fn(async () => []); },
|
||||
PluginLoader: class { loadAllPlugins = vi.fn(async () => ({ loaded: 0, errors: 0 })); getPluginSkills = vi.fn(() => []); },
|
||||
MissionStore: NoopStore,
|
||||
CentralCore: class {
|
||||
init = vi.fn(async () => undefined);
|
||||
listProjects = vi.fn(async () => []);
|
||||
listNodes = vi.fn(async () => []);
|
||||
},
|
||||
setHostTaskStore: vi.fn(),
|
||||
setDiagnosticDbHealthCheck: vi.fn(),
|
||||
setDiagnosticStoreListenerCheck: vi.fn(),
|
||||
@@ -138,29 +174,73 @@ vi.mock("../dashboard-startup-chain.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { DEFAULT_PROJECT_SETTINGS, resolveEffectiveConcurrency, resolveWorktreeCapacityLimit } = await import("@fusion/core");
|
||||
const { runDashboard } = await import("../dashboard.js");
|
||||
|
||||
type SettingsCallbacks = {
|
||||
onTogglePause: (paused: boolean) => Promise<Record<string, unknown>>;
|
||||
getSettings: () => Promise<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
function getSettingsCallbacks(tui: InstanceType<typeof harness.FakeDashboardTui>): SettingsCallbacks {
|
||||
const pauseCallbacks = tui.callbackPayloads.find((callbacks) => "onTogglePause" in callbacks);
|
||||
return {
|
||||
onTogglePause: pauseCallbacks?.onTogglePause as SettingsCallbacks["onTogglePause"],
|
||||
getSettings: tui.interactiveData.getSettings as SettingsCallbacks["getSettings"],
|
||||
};
|
||||
}
|
||||
|
||||
function expectedConcurrency(settings: Record<string, unknown>): Pick<Record<string, unknown>, "maxConcurrent" | "maxWorktrees"> {
|
||||
const capacity = resolveEffectiveConcurrency(settings);
|
||||
return {
|
||||
maxConcurrent: capacity.maxConcurrent,
|
||||
maxWorktrees: resolveWorktreeCapacityLimit({ ...settings, worktreeLimitEnabled: true }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:CapacityModel 2026-08-21-17:43:
|
||||
* The reported console mismatch was in dashboard startup, not the standalone mapping helper.
|
||||
* Run the real TTY branch with a controlled live store and capture DashboardTUI.setSettings so
|
||||
* future callback changes cannot reintroduce private defaults or bypass the shared resolver.
|
||||
* FNXC:CapacityModel 2026-08-21-23:57:
|
||||
* Callback-level coverage is required because a correct mapping helper cannot detect a
|
||||
* runDashboard callback that stops reading live settings and reintroduces private defaults.
|
||||
*/
|
||||
describe("dashboard TUI concurrency settings", () => {
|
||||
beforeEach(() => harness.reset());
|
||||
|
||||
it.each([
|
||||
["unset", {}, { maxConcurrent: 2, maxWorktrees: 4 }],
|
||||
["configured", { maxConcurrent: 6, maxWorktrees: 9 }, { maxConcurrent: 6, maxWorktrees: 9 }],
|
||||
["worktree-bound", { maxConcurrent: 8, maxWorktrees: 4, worktreeLimitEnabled: true }, { maxConcurrent: 8, maxWorktrees: 4 }],
|
||||
])("hydrates %s live settings through runDashboard's TUI setter", async (_state, settings, expected) => {
|
||||
["unset", {}],
|
||||
["configured", { maxConcurrent: 6, maxWorktrees: 9 }],
|
||||
["worktree-bound", { maxConcurrent: 8, maxWorktrees: 4, worktreeLimitEnabled: true }],
|
||||
] as const)("drives %s live settings through every runDashboard TUI callback", async (_state, settings) => {
|
||||
harness.setSettings(settings);
|
||||
const expected = expectedConcurrency(settings);
|
||||
|
||||
await runDashboard(0, { noEngine: true, noAuth: true });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
const tui = harness.latestTui();
|
||||
expect(harness.store.getSettings).toHaveBeenCalled();
|
||||
expect(tui).toBeDefined();
|
||||
expect(tui?.setSettings).toHaveBeenCalledWith(expect.objectContaining(expected));
|
||||
|
||||
const callbacks = getSettingsCallbacks(tui!);
|
||||
await expect(callbacks.onTogglePause(true)).resolves.toEqual(expect.objectContaining(expected));
|
||||
await expect(callbacks.getSettings()).resolves.toEqual(expect.objectContaining(expected));
|
||||
|
||||
tui!.setSettings.mockClear();
|
||||
harness.emitStoreEvent("settings:updated", { settings, previous: {} });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(tui!.setSettings).toHaveBeenCalledWith(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
it("uses DEFAULT_PROJECT_SETTINGS when onTogglePause runs before a store exists", async () => {
|
||||
harness.holdTaskStore();
|
||||
const dashboard = runDashboard(0, { noEngine: true, noAuth: true });
|
||||
const tui = await harness.waitForTui();
|
||||
|
||||
await expect(getSettingsCallbacks(tui).onTogglePause(true)).resolves.toEqual(expect.objectContaining({
|
||||
maxConcurrent: DEFAULT_PROJECT_SETTINGS.maxConcurrent,
|
||||
maxWorktrees: DEFAULT_PROJECT_SETTINGS.maxWorktrees,
|
||||
}));
|
||||
harness.releaseTaskStore();
|
||||
await dashboard;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user