feat(HAI-103): wire AuthStorage and ModelRegistry into dashboard server
- Integrate AuthStorage and ModelRegistry from pi-coding-agent into createServer - Pass authStorage and modelRegistry options to enable auth tab and model selector - Add comprehensive unit tests for AuthStorage/ModelRegistry wiring in dashboard - Add pi-coding-agent dependency to cli package
This commit is contained in:
115
packages/cli/src/commands/__tests__/dashboard.test.ts
Normal file
115
packages/cli/src/commands/__tests__/dashboard.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
// ── Capture arguments ───────────────────────────────────────────────
|
||||
|
||||
// Minimal mock store backed by EventEmitter so `store.on` works
|
||||
function makeMockStore() {
|
||||
const emitter = new EventEmitter();
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
watch: vi.fn().mockResolvedValue(undefined),
|
||||
stopWatching: vi.fn(),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 1,
|
||||
maxWorktrees: 2,
|
||||
autoMerge: false,
|
||||
pollIntervalMs: 60_000,
|
||||
}),
|
||||
listTasks: vi.fn().mockResolvedValue([]),
|
||||
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
emitter.on(event, handler);
|
||||
}),
|
||||
emit: emitter.emit.bind(emitter),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Mock @hai/core ──────────────────────────────────────────────────
|
||||
|
||||
vi.mock("@hai/core", () => ({
|
||||
TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/dashboard ─────────────────────────────────────────────
|
||||
|
||||
const mockListen = vi.fn();
|
||||
vi.mock("@hai/dashboard", () => ({
|
||||
createServer: vi.fn(() => ({ listen: mockListen })),
|
||||
}));
|
||||
|
||||
// ── Mock @hai/engine ────────────────────────────────────────────────
|
||||
|
||||
vi.mock("@hai/engine", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("@hai/engine")>();
|
||||
return {
|
||||
...original,
|
||||
WorktreePool: original.WorktreePool,
|
||||
AgentSemaphore: original.AgentSemaphore,
|
||||
TriageProcessor: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
})),
|
||||
TaskExecutor: vi.fn().mockImplementation(() => ({
|
||||
resumeOrphaned: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
Scheduler: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
})),
|
||||
aiMergeTask: vi.fn().mockResolvedValue({ merged: true }),
|
||||
};
|
||||
});
|
||||
|
||||
// ── Mock @mariozechner/pi-coding-agent ──────────────────────────────
|
||||
|
||||
const mockAuthStorage = { getAuth: vi.fn(), setAuth: vi.fn() };
|
||||
const mockModelRegistry = { getModels: vi.fn().mockResolvedValue([]) };
|
||||
|
||||
vi.mock("@mariozechner/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mockAuthStorage),
|
||||
},
|
||||
ModelRegistry: vi.fn().mockImplementation(() => mockModelRegistry),
|
||||
}));
|
||||
|
||||
// ── Import module under test (after mocks) ──────────────────────────
|
||||
|
||||
const { runDashboard } = await import("../dashboard.js");
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const { TaskStore } = await import("@hai/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
});
|
||||
|
||||
it("passes authStorage and modelRegistry to createServer", async () => {
|
||||
const { createServer } = await import("@hai/dashboard");
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
expect(createServer).toHaveBeenCalledTimes(1);
|
||||
const serverOpts = (createServer as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
||||
expect(serverOpts).toHaveProperty("authStorage", mockAuthStorage);
|
||||
expect(serverOpts).toHaveProperty("modelRegistry", mockModelRegistry);
|
||||
});
|
||||
|
||||
it("creates AuthStorage via AuthStorage.create()", async () => {
|
||||
const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
expect(AuthStorage.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("creates ModelRegistry with the authStorage instance", async () => {
|
||||
const { ModelRegistry } = await import("@mariozechner/pi-coding-agent");
|
||||
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
expect(ModelRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(ModelRegistry).toHaveBeenCalledWith(mockAuthStorage);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { exec } from "node:child_process";
|
||||
import { TaskStore } from "@hai/core";
|
||||
import { createServer } from "@hai/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask } from "@hai/engine";
|
||||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
const cmd =
|
||||
@@ -131,8 +132,16 @@ export async function runDashboard(port: number, opts: { engine?: boolean; open?
|
||||
} catch { /* ignore settings read errors */ }
|
||||
});
|
||||
|
||||
// Start the web server with AI merge wired in
|
||||
const app = createServer(store, { onMerge });
|
||||
// ── Auth & model wiring ────────────────────────────────────────────
|
||||
// AuthStorage manages OAuth/API-key credentials (stored in ~/.pi/agent/auth.json).
|
||||
// ModelRegistry discovers available models from configured providers.
|
||||
// Passing these to createServer enables the dashboard's Authentication
|
||||
// tab (login/logout) and Model selector.
|
||||
const authStorage = AuthStorage.create();
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
// Start the web server with AI merge, auth, and model registry wired in
|
||||
const app = createServer(store, { onMerge, authStorage, modelRegistry });
|
||||
|
||||
// Clean shutdown for file watcher when engine is not active
|
||||
if (!opts.engine) {
|
||||
|
||||
Reference in New Issue
Block a user