refactor(HAI-116): rename kb to hai across all packages, CLI, and docs
- Rename npm packages from @kb/* to @hai/* and update all workspace references - Rename CLI binary from kb to hai and config directory from .kb to .hai - Update dashboard UI branding, titles, and references from kb to hai - Update all test files, CI workflows, and documentation to reflect new naming - Run comprehensive grep verification to ensure no stale kb references remain
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import type { TaskDetail, TaskCreateInput, Task } from "@hai/core";
|
||||
import type { TaskDetail, TaskCreateInput, Task } from "@kb/core";
|
||||
import { fetchConfig, fetchSettings, updateSettings } from "./api";
|
||||
import { Header } from "./components/Header";
|
||||
import { Board } from "./components/Board";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { fetchTaskDetail, updateTask, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "./api";
|
||||
import type { Task, TaskDetail } from "@hai/core";
|
||||
import type { Task, TaskDetail } from "@kb/core";
|
||||
|
||||
const FAKE_DETAIL: TaskDetail = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
@@ -12,7 +12,7 @@ const FAKE_DETAIL: TaskDetail = {
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# HAI-001",
|
||||
prompt: "# KB-001",
|
||||
};
|
||||
|
||||
function mockFetchResponse(ok: boolean, body: unknown, status = ok ? 200 : 500) {
|
||||
@@ -38,9 +38,9 @@ describe("fetchTaskDetail", () => {
|
||||
it("returns data on first success", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_DETAIL));
|
||||
|
||||
const result = await fetchTaskDetail("HAI-001");
|
||||
const result = await fetchTaskDetail("KB-001");
|
||||
|
||||
expect(result.id).toBe("HAI-001");
|
||||
expect(result.id).toBe("KB-001");
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -49,9 +49,9 @@ describe("fetchTaskDetail", () => {
|
||||
.mockReturnValueOnce(mockFetchResponse(false, { error: "Transient error" }))
|
||||
.mockReturnValueOnce(mockFetchResponse(true, FAKE_DETAIL));
|
||||
|
||||
const result = await fetchTaskDetail("HAI-001");
|
||||
const result = await fetchTaskDetail("KB-001");
|
||||
|
||||
expect(result.id).toBe("HAI-001");
|
||||
expect(result.id).toBe("KB-001");
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("fetchTaskDetail", () => {
|
||||
globalThis.fetch = vi.fn()
|
||||
.mockReturnValue(mockFetchResponse(false, { error: "Server error" }));
|
||||
|
||||
await expect(fetchTaskDetail("HAI-001")).rejects.toThrow("Server error");
|
||||
await expect(fetchTaskDetail("KB-001")).rejects.toThrow("Server error");
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(2); // initial + 1 retry
|
||||
});
|
||||
});
|
||||
@@ -72,10 +72,10 @@ describe("updateTask", () => {
|
||||
});
|
||||
|
||||
const FAKE_TASK: Task = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: ["HAI-002"],
|
||||
dependencies: ["KB-002"],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
@@ -86,20 +86,20 @@ describe("updateTask", () => {
|
||||
it("sends PATCH with dependencies and returns updated task", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_TASK));
|
||||
|
||||
const result = await updateTask("HAI-001", { dependencies: ["HAI-002"] });
|
||||
const result = await updateTask("KB-001", { dependencies: ["KB-002"] });
|
||||
|
||||
expect(result.dependencies).toEqual(["HAI-002"]);
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/HAI-001", {
|
||||
expect(result.dependencies).toEqual(["KB-002"]);
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ dependencies: ["HAI-002"] }),
|
||||
body: JSON.stringify({ dependencies: ["KB-002"] }),
|
||||
});
|
||||
});
|
||||
|
||||
it("throws on error response", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(false, { error: "Not found" }));
|
||||
|
||||
await expect(updateTask("HAI-001", { dependencies: [] })).rejects.toThrow("Not found");
|
||||
await expect(updateTask("KB-001", { dependencies: [] })).rejects.toThrow("Not found");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Task, TaskDetail, TaskAttachment, TaskCreateInput, AgentLogEntry, Column, MergeResult, Settings } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskAttachment, TaskCreateInput, AgentLogEntry, Column, MergeResult, Settings } from "@kb/core";
|
||||
|
||||
async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
|
||||
interface AgentLogViewerProps {
|
||||
entries: AgentLogEntry[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
|
||||
import { COLUMNS } from "@kb/core";
|
||||
import { Column } from "./Column";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@hai/core";
|
||||
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
|
||||
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS } from "@kb/core";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { WorktreeGroup } from "./WorktreeGroup";
|
||||
import { InlineCreateCard } from "./InlineCreateCard";
|
||||
|
||||
@@ -8,8 +8,8 @@ export function Header({ onOpenSettings }: HeaderProps) {
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<img src="/logo.svg" alt="hai logo" className="header-logo" width={24} height={24} />
|
||||
<h1 className="logo">hai</h1>
|
||||
<img src="/logo.svg" alt="kb logo" className="header-logo" width={24} height={24} />
|
||||
<h1 className="logo">kb</h1>
|
||||
<span className="logo-sub">board</span>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Link } from "lucide-react";
|
||||
import type { Task, TaskCreateInput } from "@hai/core";
|
||||
import type { Task, TaskCreateInput } from "@kb/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { uploadAttachment } from "../api";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { Settings } from "@hai/core";
|
||||
import type { Settings } from "@kb/core";
|
||||
import { fetchSettings, updateSettings, fetchAuthStatus, loginProvider, logoutProvider, fetchModels } from "../api";
|
||||
import type { AuthProvider, ModelInfo } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -185,7 +185,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
<input
|
||||
id="taskPrefix"
|
||||
type="text"
|
||||
placeholder="HAI"
|
||||
placeholder="KB"
|
||||
value={form.taskPrefix || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
@@ -198,7 +198,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
}}
|
||||
/>
|
||||
{prefixError && <small className="field-error">{prefixError}</small>}
|
||||
{!prefixError && <small>Prefix for new task IDs (e.g. HAI, PROJ)</small>}
|
||||
{!prefixError && <small>Prefix for new task IDs (e.g. KB, PROJ)</small>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -412,7 +412,7 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
|
||||
/>
|
||||
Include task ID in commit scope
|
||||
</label>
|
||||
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(HAI-001): ...</code>)</small>
|
||||
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(KB-001): ...</code>)</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import type { Task } from "@hai/core";
|
||||
import type { Task } from "@kb/core";
|
||||
|
||||
// Mock lucide-react to avoid SVG rendering issues in test env
|
||||
vi.mock("lucide-react", () => ({
|
||||
@@ -19,7 +19,7 @@ import { uploadAttachment } from "../api";
|
||||
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
title: "Test task",
|
||||
column: "in-progress",
|
||||
status: undefined as any,
|
||||
@@ -35,7 +35,7 @@ const noop = () => {};
|
||||
describe("TaskCard", () => {
|
||||
it("renders the card ID text", () => {
|
||||
render(<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} />);
|
||||
expect(screen.getByText("HAI-001")).toBeDefined();
|
||||
expect(screen.getByText("KB-001")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders the status badge when task.status is set", () => {
|
||||
@@ -126,7 +126,7 @@ describe("TaskCard", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-001", file);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-001", file);
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Attached test.png"),
|
||||
"success",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Link, Clock, Layers } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column } from "@hai/core";
|
||||
import type { Task, TaskDetail, Column } from "@kb/core";
|
||||
import { fetchTaskDetail, uploadAttachment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@hai/core";
|
||||
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@hai/core";
|
||||
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult } from "@kb/core";
|
||||
import { COLUMN_LABELS, VALID_TRANSITIONS } from "@kb/core";
|
||||
import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useAgentLogs } from "../hooks/useAgentLogs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Task, TaskDetail } from "@hai/core";
|
||||
import type { Task, TaskDetail } from "@kb/core";
|
||||
import { ClipboardList, GitBranch } from "lucide-react";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { AgentLogViewer } from "../AgentLogViewer";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
|
||||
function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry {
|
||||
return {
|
||||
timestamp: "2026-01-01T00:00:00Z",
|
||||
taskId: "HAI-001",
|
||||
taskId: "KB-001",
|
||||
text: "Hello world",
|
||||
type: "text",
|
||||
...overrides,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Board } from "../Board";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
import { COLUMNS } from "@kb/core";
|
||||
|
||||
// Mock child components so we only test Board's own rendering
|
||||
vi.mock("../Column", () => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Column } from "../Column";
|
||||
import type { Task, Column as ColumnType } from "@hai/core";
|
||||
import type { Task, Column as ColumnType } from "@kb/core";
|
||||
|
||||
// Mock child components to keep tests focused on the Column badge behavior
|
||||
vi.mock("../TaskCard", () => ({
|
||||
@@ -43,7 +43,7 @@ const defaultProps = {
|
||||
|
||||
describe("Column count-flash", () => {
|
||||
it("does not apply count-flash class on initial render", () => {
|
||||
const tasks = [makeTask("HAI-001")];
|
||||
const tasks = [makeTask("KB-001")];
|
||||
render(<Column {...defaultProps} tasks={tasks} />);
|
||||
|
||||
const badge = screen.getByText("1");
|
||||
@@ -52,10 +52,10 @@ describe("Column count-flash", () => {
|
||||
});
|
||||
|
||||
it("applies count-flash class when task count increases", () => {
|
||||
const tasks = [makeTask("HAI-001")];
|
||||
const tasks = [makeTask("KB-001")];
|
||||
const { rerender } = render(<Column {...defaultProps} tasks={tasks} />);
|
||||
|
||||
const moreTasks = [makeTask("HAI-001"), makeTask("HAI-002")];
|
||||
const moreTasks = [makeTask("KB-001"), makeTask("KB-002")];
|
||||
rerender(<Column {...defaultProps} tasks={moreTasks} />);
|
||||
|
||||
const badge = screen.getByText("2");
|
||||
@@ -63,10 +63,10 @@ describe("Column count-flash", () => {
|
||||
});
|
||||
|
||||
it("does not apply count-flash class when task count decreases", () => {
|
||||
const tasks = [makeTask("HAI-001"), makeTask("HAI-002")];
|
||||
const tasks = [makeTask("KB-001"), makeTask("KB-002")];
|
||||
const { rerender } = render(<Column {...defaultProps} tasks={tasks} />);
|
||||
|
||||
const fewerTasks = [makeTask("HAI-001")];
|
||||
const fewerTasks = [makeTask("KB-001")];
|
||||
rerender(<Column {...defaultProps} tasks={fewerTasks} />);
|
||||
|
||||
const badge = screen.getByText("1");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Header } from "../Header";
|
||||
describe("Header", () => {
|
||||
it("renders a logo image with correct src and alt", () => {
|
||||
render(<Header />);
|
||||
const logo = screen.getByAltText("hai logo");
|
||||
const logo = screen.getByAltText("kb logo");
|
||||
expect(logo).toBeDefined();
|
||||
expect(logo.tagName).toBe("IMG");
|
||||
expect((logo as HTMLImageElement).src).toContain("/logo.svg");
|
||||
@@ -13,7 +13,7 @@ describe("Header", () => {
|
||||
|
||||
it("renders the logo before the h1 element", () => {
|
||||
render(<Header />);
|
||||
const logo = screen.getByAltText("hai logo");
|
||||
const logo = screen.getByAltText("kb logo");
|
||||
const h1 = screen.getByRole("heading", { level: 1 });
|
||||
// Logo should be a preceding sibling of the h1
|
||||
expect(logo.compareDocumentPosition(h1) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("../../api", () => ({
|
||||
function renderCard() {
|
||||
const props = {
|
||||
tasks: [],
|
||||
onSubmit: vi.fn().mockResolvedValue({ id: "HAI-001" }),
|
||||
onSubmit: vi.fn().mockResolvedValue({ id: "KB-001" }),
|
||||
onCancel: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { SettingsModal } from "../SettingsModal";
|
||||
import type { Settings } from "@hai/core";
|
||||
import type { Settings } from "@kb/core";
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { Column } from "@hai/core";
|
||||
import type { Column } from "@kb/core";
|
||||
|
||||
/**
|
||||
* Tests for the agent-active class logic in TaskCard.
|
||||
@@ -139,11 +139,11 @@ describe("TaskCard dependency tooltip", () => {
|
||||
}
|
||||
|
||||
it("returns comma-separated dependency IDs when dependencies are present", () => {
|
||||
expect(computeDepTooltip(["HAI-001", "HAI-042"])).toBe("HAI-001, HAI-042");
|
||||
expect(computeDepTooltip(["KB-001", "KB-042"])).toBe("KB-001, KB-042");
|
||||
});
|
||||
|
||||
it("returns single dependency ID when only one dependency", () => {
|
||||
expect(computeDepTooltip(["HAI-010"])).toBe("HAI-010");
|
||||
expect(computeDepTooltip(["KB-010"])).toBe("KB-010");
|
||||
});
|
||||
|
||||
it("returns undefined when dependencies array is empty", () => {
|
||||
@@ -151,12 +151,12 @@ describe("TaskCard dependency tooltip", () => {
|
||||
});
|
||||
|
||||
it("handles many dependencies", () => {
|
||||
const deps = ["HAI-001", "HAI-002", "HAI-003", "HAI-004"];
|
||||
expect(computeDepTooltip(deps)).toBe("HAI-001, HAI-002, HAI-003, HAI-004");
|
||||
const deps = ["KB-001", "KB-002", "KB-003", "KB-004"];
|
||||
expect(computeDepTooltip(deps)).toBe("KB-001, KB-002, KB-003, KB-004");
|
||||
});
|
||||
|
||||
it("data-tooltip attribute contains dependency IDs as a readable string", () => {
|
||||
const deps = ["HAI-005", "HAI-012"];
|
||||
const deps = ["KB-005", "KB-012"];
|
||||
const tooltip = computeDepTooltip(deps);
|
||||
expect(tooltip).toBeDefined();
|
||||
// Each dependency ID should appear in the tooltip
|
||||
@@ -179,7 +179,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
|
||||
}
|
||||
|
||||
it("shows scope badge when blockedBy is set", () => {
|
||||
expect(shouldShowScopeBadge("HAI-003")).toBe(true);
|
||||
expect(shouldShowScopeBadge("KB-003")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show scope badge when blockedBy is undefined", () => {
|
||||
@@ -187,7 +187,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
|
||||
});
|
||||
|
||||
it("shows card-meta when blockedBy is set even with no deps or queued status", () => {
|
||||
expect(shouldShowCardMeta({ blockedBy: "HAI-003" })).toBe(true);
|
||||
expect(shouldShowCardMeta({ blockedBy: "KB-003" })).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show card-meta when no deps, not queued, and no blockedBy", () => {
|
||||
@@ -200,7 +200,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
|
||||
}
|
||||
|
||||
it("generates correct tooltip text", () => {
|
||||
expect(computeScopeTooltip("HAI-005")).toBe("Blocked by HAI-005 (file overlap)");
|
||||
expect(computeScopeTooltip("KB-005")).toBe("Blocked by KB-005 (file overlap)");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
import type { TaskDetail, Column, MergeResult, Task } from "@hai/core";
|
||||
import type { TaskDetail, Column, MergeResult, Task } from "@kb/core";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
uploadAttachment: vi.fn(),
|
||||
@@ -16,7 +16,7 @@ vi.mock("../../hooks/useAgentLogs", () => ({
|
||||
|
||||
function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "HAI-099",
|
||||
id: "KB-099",
|
||||
description: "Test task",
|
||||
column: "in-progress" as Column,
|
||||
dependencies: [],
|
||||
@@ -157,7 +157,7 @@ describe("TaskDetailModal", () => {
|
||||
task={makeTask({
|
||||
title: undefined,
|
||||
description: "Fix the login bug",
|
||||
prompt: "# HAI-099\n\nFix the login bug\n",
|
||||
prompt: "# KB-099\n\nFix the login bug\n",
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
@@ -167,13 +167,13 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// The heading "HAI-099" should be stripped from the markdown
|
||||
// The heading "KB-099" should be stripped from the markdown
|
||||
const markdownBody = container.querySelector(".markdown-body");
|
||||
expect(markdownBody?.innerHTML).not.toContain("HAI-099");
|
||||
expect(markdownBody?.innerHTML).not.toContain("KB-099");
|
||||
// Description appears in the markdown body
|
||||
expect(markdownBody?.textContent).toContain("Fix the login bug");
|
||||
// The detail header shows the ID (not duplicated as markdown heading)
|
||||
expect(container.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
expect(container.querySelector(".detail-id")?.textContent).toBe("KB-099");
|
||||
// The h2 title shows description, not the task ID
|
||||
const h2 = container.querySelector("h2.detail-title");
|
||||
expect(h2?.textContent).toBe("Fix the login bug");
|
||||
@@ -210,7 +210,7 @@ describe("TaskDetailModal", () => {
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(withTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
expect(withTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
|
||||
|
||||
// Without title
|
||||
const { container: withoutTitle } = render(
|
||||
@@ -223,7 +223,7 @@ describe("TaskDetailModal", () => {
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
expect(withoutTitle.querySelector(".detail-id")?.textContent).toBe("KB-099");
|
||||
});
|
||||
|
||||
describe("paste image upload", () => {
|
||||
@@ -267,7 +267,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -394,7 +394,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("HAI-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("KB-099", imageFile);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -418,7 +418,7 @@ describe("TaskDetailModal", () => {
|
||||
it("renders dependency list when dependencies exist", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })}
|
||||
task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -427,16 +427,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("HAI-001")).toBeTruthy();
|
||||
expect(screen.getByText("HAI-002")).toBeTruthy();
|
||||
expect(screen.getByText("KB-001")).toBeTruthy();
|
||||
expect(screen.getByText("KB-002")).toBeTruthy();
|
||||
expect(screen.queryByText("(no dependencies)")).toBeNull();
|
||||
});
|
||||
|
||||
it("can add a dependency via the dropdown", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const allTasks: Task[] = [
|
||||
{ id: "HAI-001", description: "Dep 1", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
|
||||
{ id: "HAI-099", description: "Self", column: "in-progress" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
|
||||
{ id: "KB-001", description: "Dep 1", column: "todo" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
|
||||
{ id: "KB-099", description: "Self", column: "in-progress" as Column, dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "", updatedAt: "" },
|
||||
];
|
||||
|
||||
render(
|
||||
@@ -452,16 +452,16 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Add Dependency"));
|
||||
// Should show HAI-001 in the dropdown but not HAI-099 (self is excluded)
|
||||
// Should show KB-001 in the dropdown but not KB-099 (self is excluded)
|
||||
const dropdown = document.querySelector(".dep-dropdown")!;
|
||||
expect(dropdown).toBeTruthy();
|
||||
expect(dropdown.textContent).toContain("HAI-001");
|
||||
expect(dropdown.textContent).toContain("KB-001");
|
||||
expect(dropdown.querySelectorAll(".dep-dropdown-item")).toHaveLength(1);
|
||||
|
||||
fireEvent.click(screen.getByText("HAI-001"));
|
||||
fireEvent.click(screen.getByText("KB-001"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("HAI-099", { dependencies: ["HAI-001"] });
|
||||
expect(updateTask).toHaveBeenCalledWith("KB-099", { dependencies: ["KB-001"] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ dependencies: ["HAI-001", "HAI-002"] })}
|
||||
task={makeTask({ dependencies: ["KB-001", "KB-002"] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -480,10 +480,10 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const removeButtons = screen.getAllByTitle(/Remove dependency/);
|
||||
fireEvent.click(removeButtons[0]); // Remove HAI-001
|
||||
fireEvent.click(removeButtons[0]); // Remove KB-001
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("HAI-099", { dependencies: ["HAI-002"] });
|
||||
expect(updateTask).toHaveBeenCalledWith("KB-099", { dependencies: ["KB-002"] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ afterEach(() => {
|
||||
|
||||
describe("useAgentLogs", () => {
|
||||
it("does not fetch or connect when enabled=false", () => {
|
||||
const { result } = renderHook(() => useAgentLogs("HAI-001", false));
|
||||
const { result } = renderHook(() => useAgentLogs("KB-001", false));
|
||||
|
||||
expect(mockFetchAgentLogs).not.toHaveBeenCalled();
|
||||
expect(MockEventSource.instances).toHaveLength(0);
|
||||
@@ -60,27 +60,27 @@ describe("useAgentLogs", () => {
|
||||
|
||||
it("fetches historical logs and opens SSE when enabled=true", async () => {
|
||||
const historicalLogs = [
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "old", type: "text" as const },
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "old", type: "text" as const },
|
||||
];
|
||||
mockFetchAgentLogs.mockResolvedValueOnce(historicalLogs);
|
||||
|
||||
const { result } = renderHook(() => useAgentLogs("HAI-001", true));
|
||||
const { result } = renderHook(() => useAgentLogs("KB-001", true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.entries).toEqual(historicalLogs);
|
||||
});
|
||||
|
||||
expect(mockFetchAgentLogs).toHaveBeenCalledWith("HAI-001");
|
||||
expect(mockFetchAgentLogs).toHaveBeenCalledWith("KB-001");
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
expect(MockEventSource.instances[0].url).toBe("/api/tasks/HAI-001/logs/stream");
|
||||
expect(MockEventSource.instances[0].url).toBe("/api/tasks/KB-001/logs/stream");
|
||||
});
|
||||
|
||||
it("appends live SSE entries to historical entries", async () => {
|
||||
mockFetchAgentLogs.mockResolvedValueOnce([
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "old", type: "text" as const },
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "old", type: "text" as const },
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useAgentLogs("HAI-001", true));
|
||||
const { result } = renderHook(() => useAgentLogs("KB-001", true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.entries).toHaveLength(1);
|
||||
@@ -90,7 +90,7 @@ describe("useAgentLogs", () => {
|
||||
act(() => {
|
||||
es._emit("agent:log", {
|
||||
timestamp: "2026-01-01T00:01:00Z",
|
||||
taskId: "HAI-001",
|
||||
taskId: "KB-001",
|
||||
text: "new",
|
||||
type: "text",
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe("useAgentLogs", () => {
|
||||
mockFetchAgentLogs.mockResolvedValueOnce([]);
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ enabled }) => useAgentLogs("HAI-001", enabled),
|
||||
({ enabled }) => useAgentLogs("KB-001", enabled),
|
||||
{ initialProps: { enabled: true } },
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("useAgentLogs", () => {
|
||||
it("closes SSE on unmount", async () => {
|
||||
mockFetchAgentLogs.mockResolvedValueOnce([]);
|
||||
|
||||
const { unmount } = renderHook(() => useAgentLogs("HAI-001", true));
|
||||
const { unmount } = renderHook(() => useAgentLogs("KB-001", true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(MockEventSource.instances).toHaveLength(1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import type { AgentLogEntry } from "@hai/core";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
import { fetchAgentLogs } from "../api";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { Task, Column, TaskCreateInput, MergeResult } from "@hai/core";
|
||||
import type { Task, Column, TaskCreateInput, MergeResult } from "@kb/core";
|
||||
import * as api from "../api";
|
||||
|
||||
export function useTasks() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>hai | board</title>
|
||||
<title>kb | board</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { groupByWorktree, getWorktreeLabel } from "./worktreeGrouping";
|
||||
import type { Task } from "@hai/core";
|
||||
import type { Task } from "@kb/core";
|
||||
|
||||
function makeTask(overrides: Partial<Task> & { id: string }): Task {
|
||||
return {
|
||||
@@ -18,8 +18,8 @@ function makeTask(overrides: Partial<Task> & { id: string }): Task {
|
||||
|
||||
describe("getWorktreeLabel", () => {
|
||||
it("extracts last path segment", () => {
|
||||
expect(getWorktreeLabel(".worktrees/HAI-001")).toBe("HAI-001");
|
||||
expect(getWorktreeLabel("/path/to/hai/hai-001")).toBe("hai-001");
|
||||
expect(getWorktreeLabel(".worktrees/KB-001")).toBe("KB-001");
|
||||
expect(getWorktreeLabel("/path/to/kb/kb-001")).toBe("kb-001");
|
||||
});
|
||||
|
||||
it("extracts humanized worktree names", () => {
|
||||
@@ -31,8 +31,8 @@ describe("getWorktreeLabel", () => {
|
||||
|
||||
describe("groupByWorktree", () => {
|
||||
it("groups active in-progress tasks by worktree", () => {
|
||||
const t1 = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const t2 = makeTask({ id: "HAI-002", worktree: ".worktrees/quiet-robin" });
|
||||
const t1 = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const t2 = makeTask({ id: "KB-002", worktree: ".worktrees/quiet-robin" });
|
||||
|
||||
const groups = groupByWorktree([t1, t2], [t1, t2], 2);
|
||||
|
||||
@@ -44,9 +44,9 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("places queued tasks only in the Up Next group, never in worktree groups", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const queued = makeTask({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("does not create Up Next group when there are no eligible queued tasks", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
|
||||
const groups = groupByWorktree([active], [active], 2);
|
||||
|
||||
@@ -74,11 +74,11 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("does not create Up Next when queued tasks have unsatisfied dependencies", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const blocked = makeTask({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: ["HAI-003"], // HAI-003 doesn't exist or isn't done
|
||||
dependencies: ["KB-003"], // KB-003 doesn't exist or isn't done
|
||||
});
|
||||
|
||||
const groups = groupByWorktree([active], [active, blocked], 2);
|
||||
@@ -87,10 +87,10 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("respects maxConcurrent limit on queued tasks shown", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const q1 = makeTask({ id: "HAI-010", column: "todo" });
|
||||
const q2 = makeTask({ id: "HAI-011", column: "todo" });
|
||||
const q3 = makeTask({ id: "HAI-012", column: "todo" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const q1 = makeTask({ id: "KB-010", column: "todo" });
|
||||
const q2 = makeTask({ id: "KB-011", column: "todo" });
|
||||
const q3 = makeTask({ id: "KB-012", column: "todo" });
|
||||
|
||||
const groups = groupByWorktree([active], [active, q1, q2, q3], 2);
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("places unassigned in-progress tasks in Unassigned group", () => {
|
||||
const unassigned = makeTask({ id: "HAI-001" }); // no worktree
|
||||
const unassigned = makeTask({ id: "KB-001" }); // no worktree
|
||||
|
||||
const groups = groupByWorktree([unassigned], [unassigned], 2);
|
||||
|
||||
@@ -110,15 +110,15 @@ describe("groupByWorktree", () => {
|
||||
});
|
||||
|
||||
it("excludes paused todo tasks from Up Next", () => {
|
||||
const active = makeTask({ id: "HAI-001", worktree: ".worktrees/swift-falcon" });
|
||||
const active = makeTask({ id: "KB-001", worktree: ".worktrees/swift-falcon" });
|
||||
const paused = makeTask({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
paused: true,
|
||||
});
|
||||
const normal = makeTask({
|
||||
id: "HAI-003",
|
||||
id: "KB-003",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
});
|
||||
@@ -127,16 +127,16 @@ describe("groupByWorktree", () => {
|
||||
|
||||
const upNext = groups.find((g) => g.label === "Up Next");
|
||||
expect(upNext).toBeDefined();
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["HAI-003"]);
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("HAI-002");
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).toEqual(["KB-003"]);
|
||||
expect(upNext!.queuedTasks.map((t) => t.id)).not.toContain("KB-002");
|
||||
});
|
||||
|
||||
it("queued tasks with satisfied deps appear in Up Next", () => {
|
||||
const done = makeTask({ id: "HAI-001", column: "done" });
|
||||
const done = makeTask({ id: "KB-001", column: "done" });
|
||||
const queued = makeTask({
|
||||
id: "HAI-002",
|
||||
id: "KB-002",
|
||||
column: "todo",
|
||||
dependencies: ["HAI-001"],
|
||||
dependencies: ["KB-001"],
|
||||
});
|
||||
|
||||
const groups = groupByWorktree([], [done, queued], 2);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Task } from "@hai/core";
|
||||
import type { Task } from "@kb/core";
|
||||
|
||||
export interface WorktreeGroupData {
|
||||
label: string;
|
||||
@@ -8,7 +8,7 @@ export interface WorktreeGroupData {
|
||||
|
||||
/**
|
||||
* Extract a clean display name from a worktree path.
|
||||
* e.g. ".worktrees/HAI-001" → "HAI-001", "/path/to/hai/hai-001" → "hai-001"
|
||||
* e.g. ".worktrees/KB-001" → "KB-001", "/path/to/kb/kb-001" → "kb-001"
|
||||
*/
|
||||
export function getWorktreeLabel(worktreePath: string): string {
|
||||
// Take the last segment of the path
|
||||
@@ -18,8 +18,8 @@ export function getWorktreeLabel(worktreePath: string): string {
|
||||
|
||||
/**
|
||||
* Topological sort of tasks by dependency order.
|
||||
* Mirrors resolveDependencyOrder from @hai/core but inlined to avoid
|
||||
* build alias issues (Vite aliases @hai/core to types.ts only).
|
||||
* Mirrors resolveDependencyOrder from @kb/core but inlined to avoid
|
||||
* build alias issues (Vite aliases @kb/core to types.ts only).
|
||||
*/
|
||||
function resolveDependencyOrder(tasks: Task[]): string[] {
|
||||
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hai/dashboard",
|
||||
"name": "@kb/dashboard",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hai/core": "workspace:*",
|
||||
"@kb/core": "workspace:*",
|
||||
"@types/multer": "^2.1.0",
|
||||
"express": "^5.1.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>hai | board</title>
|
||||
<title>kb | board</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="logo">hai</h1>
|
||||
<h1 class="logo">kb</h1>
|
||||
<span class="logo-sub">board</span>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="add-task-btn">+ New Task</button>
|
||||
@@ -96,7 +96,7 @@
|
||||
>Dependencies
|
||||
<span class="optional">(comma-separated IDs)</span></label
|
||||
>
|
||||
<input type="text" id="task-deps" placeholder="HAI-001, HAI-002" />
|
||||
<input type="text" id="task-deps" placeholder="KB-001, KB-002" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn" data-close="create-modal">
|
||||
|
||||
@@ -2,8 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import http from "node:http";
|
||||
import { createApiRoutes } from "./routes.js";
|
||||
import type { TaskStore, TaskAttachment } from "@hai/core";
|
||||
import type { TaskDetail } from "@hai/core";
|
||||
import type { TaskStore, TaskAttachment } from "@kb/core";
|
||||
import type { TaskDetail } from "@kb/core";
|
||||
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
|
||||
|
||||
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
@@ -24,7 +24,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
}
|
||||
|
||||
const FAKE_TASK_DETAIL: TaskDetail = {
|
||||
id: "HAI-001",
|
||||
id: "KB-001",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
@@ -33,7 +33,7 @@ const FAKE_TASK_DETAIL: TaskDetail = {
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
prompt: "# HAI-001\n\nTest task",
|
||||
prompt: "# KB-001\n\nTest task",
|
||||
};
|
||||
|
||||
/** Helper: send GET and return { status, body } */
|
||||
@@ -117,11 +117,11 @@ describe("GET /tasks/:id", () => {
|
||||
it("returns task detail on success", async () => {
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(FAKE_TASK_DETAIL);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-001");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.id).toBe("HAI-001");
|
||||
expect(res.body.prompt).toBe("# HAI-001\n\nTest task");
|
||||
expect(res.body.id).toBe("KB-001");
|
||||
expect(res.body.prompt).toBe("# KB-001\n\nTest task");
|
||||
});
|
||||
|
||||
it("returns 404 when task genuinely does not exist (ENOENT)", async () => {
|
||||
@@ -129,7 +129,7 @@ describe("GET /tasks/:id", () => {
|
||||
err.code = "ENOENT";
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-999");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-999");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("not found");
|
||||
@@ -139,7 +139,7 @@ describe("GET /tasks/:id", () => {
|
||||
const err = new Error("Unexpected end of JSON input");
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(err);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-001");
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toContain("Unexpected end of JSON input");
|
||||
@@ -167,20 +167,20 @@ describe("POST /tasks/:id/retry", () => {
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTask);
|
||||
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: undefined });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
});
|
||||
|
||||
it("returns 400 when task is not in failed state", async () => {
|
||||
const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(activeTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
const doneTask = { ...FAKE_TASK_DETAIL, column: "done", status: "failed" };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
@@ -216,32 +216,32 @@ describe("PATCH /tasks/:id", () => {
|
||||
}
|
||||
|
||||
it("forwards dependencies to store.updateTask", async () => {
|
||||
const updatedTask = { ...FAKE_TASK_DETAIL, dependencies: ["HAI-002"] };
|
||||
const updatedTask = { ...FAKE_TASK_DETAIL, dependencies: ["KB-002"] };
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/HAI-001", JSON.stringify({ dependencies: ["HAI-002"] }), {
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ dependencies: ["KB-002"] }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
prompt: undefined,
|
||||
dependencies: ["HAI-002"],
|
||||
dependencies: ["KB-002"],
|
||||
});
|
||||
expect(res.body.dependencies).toEqual(["HAI-002"]);
|
||||
expect(res.body.dependencies).toEqual(["KB-002"]);
|
||||
});
|
||||
|
||||
it("forwards title and description without dependencies", async () => {
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({ ...FAKE_TASK_DETAIL, title: "New" });
|
||||
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/HAI-001", JSON.stringify({ title: "New" }), {
|
||||
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ title: "New" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
title: "New",
|
||||
description: undefined,
|
||||
prompt: undefined,
|
||||
@@ -280,14 +280,14 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("fake png content");
|
||||
const { body, boundary } = buildMultipart("file", "screenshot.png", "image/png", content);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.filename).toBe("1234-screenshot.png");
|
||||
expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
|
||||
"HAI-001",
|
||||
"KB-001",
|
||||
"screenshot.png",
|
||||
expect.any(Buffer),
|
||||
"image/png",
|
||||
@@ -302,7 +302,7 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("not an image");
|
||||
const { body, boundary } = buildMultipart("file", "file.txt", "text/plain", content);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ describe("Attachment routes", () => {
|
||||
const content = Buffer.from("small but store rejects");
|
||||
const { body, boundary } = buildMultipart("file", "big.png", "image/png", content);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/attachments", body, {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/attachments", body, {
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||
});
|
||||
|
||||
@@ -327,10 +327,10 @@ describe("Attachment routes", () => {
|
||||
});
|
||||
|
||||
it("DELETE /tasks/:id/attachments/:filename — deletes attachment", async () => {
|
||||
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/HAI-001/attachments/1234-screenshot.png");
|
||||
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001/attachments/1234-screenshot.png");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("HAI-001", "1234-screenshot.png");
|
||||
expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("KB-001", "1234-screenshot.png");
|
||||
});
|
||||
|
||||
it("DELETE /tasks/:id/attachments/:filename — returns 404 for missing", async () => {
|
||||
@@ -338,29 +338,29 @@ describe("Attachment routes", () => {
|
||||
err.code = "ENOENT";
|
||||
(store.deleteAttachment as ReturnType<typeof vi.fn>).mockRejectedValue(err);
|
||||
|
||||
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/HAI-001/attachments/nope.png");
|
||||
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-001/attachments/nope.png");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns agent logs", async () => {
|
||||
const fakeLogs = [
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "HAI-001", text: "Hello", type: "text" },
|
||||
{ timestamp: "2026-01-01T00:00:01Z", taskId: "HAI-001", text: "Read", type: "tool" },
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "Hello", type: "text" },
|
||||
{ timestamp: "2026-01-01T00:00:01Z", taskId: "KB-001", text: "Read", type: "tool" },
|
||||
];
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue(fakeLogs);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual(fakeLogs);
|
||||
expect(store.getAgentLogs).toHaveBeenCalledWith("HAI-001");
|
||||
expect(store.getAgentLogs).toHaveBeenCalledWith("KB-001");
|
||||
});
|
||||
|
||||
it("GET /tasks/:id/logs — returns empty array when no logs", async () => {
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
@@ -369,7 +369,7 @@ describe("Attachment routes", () => {
|
||||
it("GET /tasks/:id/logs — returns 500 on store error", async () => {
|
||||
(store.getAgentLogs as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("disk error"));
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/HAI-001/logs");
|
||||
const res = await GET(buildApp(), "/api/tasks/KB-001/logs");
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toBe("disk error");
|
||||
@@ -631,27 +631,27 @@ describe("Pause/Unpause endpoints", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore({
|
||||
pauseTask: vi.fn().mockResolvedValue({ id: "HAI-001", paused: true }),
|
||||
pauseTask: vi.fn().mockResolvedValue({ id: "KB-001", paused: true }),
|
||||
});
|
||||
});
|
||||
|
||||
it("POST /tasks/:id/pause — pauses a task", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/pause");
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ id: "HAI-001", paused: true });
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("HAI-001", true);
|
||||
expect(res.body).toEqual({ id: "KB-001", paused: true });
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("KB-001", true);
|
||||
});
|
||||
|
||||
it("POST /tasks/:id/unpause — unpauses a task", async () => {
|
||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "HAI-001" });
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/unpause");
|
||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "KB-001" });
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/unpause");
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("HAI-001", false);
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("KB-001", false);
|
||||
});
|
||||
|
||||
it("POST /tasks/:id/pause — returns 500 on error", async () => {
|
||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("not found"));
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/pause");
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toBe("not found");
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { createReadStream } from "node:fs";
|
||||
import type { TaskStore, Column, MergeResult } from "@hai/core";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
import type { TaskStore, Column, MergeResult } from "@kb/core";
|
||||
import { COLUMNS } from "@kb/core";
|
||||
import type { ServerOptions } from "./server.js";
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import express from "express";
|
||||
import { join, dirname } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { TaskStore, MergeResult } from "@hai/core";
|
||||
import type { TaskStore, MergeResult } from "@kb/core";
|
||||
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
|
||||
import { createApiRoutes } from "./routes.js";
|
||||
import { createSSE } from "./sse.js";
|
||||
@@ -27,14 +27,14 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
|
||||
// Serve built React app
|
||||
// Resolution order:
|
||||
// 1. HAI_CLIENT_DIR env override (explicit)
|
||||
// 2. Next to process.execPath (bun-compiled binary: dist/hai + dist/client/)
|
||||
// 1. KB_CLIENT_DIR env override (explicit)
|
||||
// 2. Next to process.execPath (bun-compiled binary: dist/kb + dist/client/)
|
||||
// 3. __dirname/../dist/client (running from src/ via tsx/ts-node)
|
||||
// 4. __dirname/../client (running from dist/ after tsc)
|
||||
// 5. __dirname/../public (fallback for dev)
|
||||
const execDir = dirname(process.execPath);
|
||||
const clientDir = process.env.HAI_CLIENT_DIR
|
||||
? process.env.HAI_CLIENT_DIR
|
||||
const clientDir = process.env.KB_CLIENT_DIR
|
||||
? process.env.KB_CLIENT_DIR
|
||||
: existsSync(join(execDir, "client", "index.html"))
|
||||
? join(execDir, "client")
|
||||
: existsSync(join(__dirname, "..", "dist", "client"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { TaskStore } from "@hai/core";
|
||||
import type { TaskStore } from "@kb/core";
|
||||
|
||||
export function createSSE(store: TaskStore) {
|
||||
return (_req: Request, res: Response) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@hai/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
"@kb/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@hai/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
"@kb/core": resolve(__dirname, "../core/src/types.ts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
||||
Reference in New Issue
Block a user