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:
Dustin Byrne
2026-03-26 20:36:34 -04:00
parent 1a61f09379
commit 5e76a03287
4 changed files with 131 additions and 3 deletions

View 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);
});
});

View File

@@ -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) {