feat(KB-012): add --paused flag to dashboard command
- Add --paused CLI flag to start dashboard in paused mode - Update runDashboard to accept paused option - Add comprehensive tests for --paused flag behavior - Update STANDALONE.md documentation with new flag - Add changeset for patch release
This commit is contained in:
@@ -46,6 +46,7 @@ kb — AI-orchestrated task board
|
||||
|
||||
Usage:
|
||||
kb dashboard Start the board web UI
|
||||
kb dashboard --paused Start with automation paused
|
||||
kb task create [desc] [opts] Create a new task (goes to triage)
|
||||
kb task list List all tasks
|
||||
kb task show <id> Show task details, steps, log
|
||||
@@ -60,6 +61,7 @@ Usage:
|
||||
|
||||
Options:
|
||||
--port, -p <port> Dashboard port (default: 4040)
|
||||
--paused Start with engine paused (automation disabled)
|
||||
--attach <file> Attach file(s) on task create (repeatable)
|
||||
--depends <id> Declare dependency on task create (repeatable)
|
||||
--limit, -l <n> Max issues to import (default: 30, max: 100)
|
||||
@@ -92,7 +94,8 @@ async function main() {
|
||||
const pi = portIdx !== -1 ? portIdx : portIdxShort;
|
||||
const port = pi !== -1 ? parseInt(args[pi + 1], 10) : 4040;
|
||||
const open = !args.includes("--no-open");
|
||||
await runDashboard(port, { open });
|
||||
const paused = args.includes("--paused");
|
||||
await runDashboard(port, { open, paused });
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ function makeMockStore() {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
watch: vi.fn().mockResolvedValue(undefined),
|
||||
stopWatching: vi.fn(),
|
||||
updateSettings: vi.fn().mockResolvedValue(undefined),
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 1,
|
||||
maxWorktrees: 2,
|
||||
@@ -613,3 +614,115 @@ describe("runDashboard — enginePaused (soft pause)", () => {
|
||||
expect(mergedIds).toContain("KB-EP2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runDashboard — --paused flag", () => {
|
||||
let mockStore: ReturnType<typeof makeMockStore>;
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
capturedExecutorOpts = undefined;
|
||||
vi.clearAllMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@kb/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@kb/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
);
|
||||
(engine.TaskExecutor as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
(_store: unknown, _cwd: unknown, opts: unknown) => {
|
||||
capturedExecutorOpts = opts as Record<string, unknown>;
|
||||
return { resumeOrphaned: vi.fn().mockResolvedValue(undefined) };
|
||||
},
|
||||
);
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("calls store.updateSettings({ enginePaused: true }) when paused: true is passed", async () => {
|
||||
await runDashboard(0, { open: false, paused: true });
|
||||
|
||||
expect(mockStore.updateSettings).toHaveBeenCalledWith({ enginePaused: true });
|
||||
});
|
||||
|
||||
it("logs a message when starting in paused mode", async () => {
|
||||
await runDashboard(0, { open: false, paused: true });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"[engine] Starting in paused mode — automation disabled",
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT set enginePaused when paused option is absent", async () => {
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
// updateSettings should not be called with enginePaused during normal startup
|
||||
const enginePausedCalls = mockStore.updateSettings.mock.calls.filter(
|
||||
(call: any[]) => call[0]?.enginePaused !== undefined,
|
||||
);
|
||||
expect(enginePausedCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("does NOT log paused message when starting normally", async () => {
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
const pausedMessageCalls = consoleSpy.mock.calls.filter(
|
||||
(args) => args[0] === "[engine] Starting in paused mode — automation disabled",
|
||||
);
|
||||
expect(pausedMessageCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runDashboard — --paused flag", () => {
|
||||
let mockStore: ReturnType<typeof makeMockStore>;
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@kb/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@kb/engine");
|
||||
(engine.TaskExecutor as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
() => ({ resumeOrphaned: vi.fn().mockResolvedValue(undefined) }),
|
||||
);
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("calls store.updateSettings({ enginePaused: true }) when paused: true is passed", async () => {
|
||||
await runDashboard(0, { open: false, paused: true });
|
||||
|
||||
expect(mockStore.updateSettings).toHaveBeenCalledWith({ enginePaused: true });
|
||||
expect(mockStore.updateSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT call store.updateSettings when paused flag is absent", async () => {
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
expect(mockStore.updateSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs paused mode message when starting with paused: true", async () => {
|
||||
await runDashboard(0, { open: false, paused: true });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"[engine] Starting in paused mode — automation disabled",
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT log paused mode message when paused flag is absent", async () => {
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
const pausedMessageCalls = consoleSpy.mock.calls.filter(
|
||||
(args) => typeof args[0] === "string" && args[0].includes("paused mode"),
|
||||
);
|
||||
expect(pausedMessageCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,12 +13,18 @@ function openBrowser(url: string): void {
|
||||
exec(cmd, () => {});
|
||||
}
|
||||
|
||||
export async function runDashboard(port: number, opts: { open?: boolean } = {}) {
|
||||
export async function runDashboard(port: number, opts: { open?: boolean; paused?: boolean } = {}) {
|
||||
const cwd = process.cwd();
|
||||
const store = new TaskStore(cwd);
|
||||
await store.init();
|
||||
await store.watch();
|
||||
|
||||
// Set enginePaused if starting in paused mode
|
||||
if (opts.paused) {
|
||||
await store.updateSettings({ enginePaused: true });
|
||||
console.log("[engine] Starting in paused mode — automation disabled");
|
||||
}
|
||||
|
||||
// ── Shared concurrency semaphore ──────────────────────────────────
|
||||
//
|
||||
// Gates all agentic activities (triage, execution, merge) behind a
|
||||
|
||||
Reference in New Issue
Block a user