feat(KB-618): add multi-project dashboard support
- Add project API methods and types for multi-project support - Add server-side project management routes for multi-project support - Update test coverage for dashboard API and routes
This commit is contained in:
@@ -52,7 +52,7 @@ const FAKE_DETAIL: TaskDetail = {
|
|||||||
log: [],
|
log: [],
|
||||||
createdAt: "2026-01-01T00:00:00.000Z",
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
prompt: "# FN-001",
|
prompt: "# KB-001",
|
||||||
};
|
};
|
||||||
|
|
||||||
function mockFetchResponse(
|
function mockFetchResponse(
|
||||||
@@ -141,7 +141,7 @@ describe("updateTask", () => {
|
|||||||
const result = await updateTask("FN-001", { dependencies: ["FN-002"] });
|
const result = await updateTask("FN-001", { dependencies: ["FN-002"] });
|
||||||
|
|
||||||
expect(result.dependencies).toEqual(["FN-002"]);
|
expect(result.dependencies).toEqual(["FN-002"]);
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ dependencies: ["FN-002"] }),
|
body: JSON.stringify({ dependencies: ["FN-002"] }),
|
||||||
@@ -182,7 +182,7 @@ describe("task comments api", () => {
|
|||||||
const result = await fetchTaskComments("FN-001");
|
const result = await fetchTaskComments("FN-001");
|
||||||
|
|
||||||
expect(result).toEqual(comments);
|
expect(result).toEqual(comments);
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -193,7 +193,7 @@ describe("task comments api", () => {
|
|||||||
const result = await addTaskComment("FN-001", "Hello", "user");
|
const result = await addTaskComment("FN-001", "Hello", "user");
|
||||||
|
|
||||||
expect(result).toEqual(FAKE_TASK);
|
expect(result).toEqual(FAKE_TASK);
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ text: "Hello", author: "user" }),
|
body: JSON.stringify({ text: "Hello", author: "user" }),
|
||||||
@@ -205,7 +205,7 @@ describe("task comments api", () => {
|
|||||||
|
|
||||||
await updateTaskComment("FN-001", "c1", "Updated");
|
await updateTaskComment("FN-001", "c1", "Updated");
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments/c1", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments/c1", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ text: "Updated" }),
|
body: JSON.stringify({ text: "Updated" }),
|
||||||
@@ -217,7 +217,7 @@ describe("task comments api", () => {
|
|||||||
|
|
||||||
await deleteTaskComment("FN-001", "c1");
|
await deleteTaskComment("FN-001", "c1");
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments/c1", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments/c1", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
@@ -529,7 +529,7 @@ describe("addSteeringComment", () => {
|
|||||||
expect(result.id).toBe("FN-001");
|
expect(result.id).toBe("FN-001");
|
||||||
expect(result.steeringComments).toHaveLength(1);
|
expect(result.steeringComments).toHaveLength(1);
|
||||||
expect(result.steeringComments![0].text).toBe("Please handle the edge case");
|
expect(result.steeringComments![0].text).toBe("Please handle the edge case");
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/steer", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/steer", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ text: "Please handle the edge case" }),
|
body: JSON.stringify({ text: "Please handle the edge case" }),
|
||||||
@@ -794,7 +794,7 @@ describe("approvePlan", () => {
|
|||||||
|
|
||||||
expect(result.column).toBe("todo");
|
expect(result.column).toBe("todo");
|
||||||
expect(result.status).toBeUndefined();
|
expect(result.status).toBeUndefined();
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/approve-plan", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/approve-plan", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@@ -828,7 +828,7 @@ describe("rejectPlan", () => {
|
|||||||
|
|
||||||
expect(result.column).toBe("triage");
|
expect(result.column).toBe("triage");
|
||||||
expect(result.status).toBeUndefined();
|
expect(result.status).toBeUndefined();
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/reject-plan", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/reject-plan", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@@ -854,7 +854,7 @@ describe("refineTask", () => {
|
|||||||
|
|
||||||
const FAKE_REFINED_TASK: Task = {
|
const FAKE_REFINED_TASK: Task = {
|
||||||
id: "FN-002",
|
id: "FN-002",
|
||||||
description: "Refinement of FN-001",
|
description: "Refinement of KB-001",
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: ["FN-001"],
|
dependencies: ["FN-001"],
|
||||||
steps: [],
|
steps: [],
|
||||||
@@ -872,7 +872,7 @@ describe("refineTask", () => {
|
|||||||
expect(result.id).toBe("FN-002");
|
expect(result.id).toBe("FN-002");
|
||||||
expect(result.column).toBe("triage");
|
expect(result.column).toBe("triage");
|
||||||
expect(result.dependencies).toContain("FN-001");
|
expect(result.dependencies).toContain("FN-001");
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/refine", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/refine", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ feedback: "Need to add more tests and improve error handling" }),
|
body: JSON.stringify({ feedback: "Need to add more tests and improve error handling" }),
|
||||||
@@ -1171,7 +1171,7 @@ describe("Git Management API", () => {
|
|||||||
const response = await archiveTask("FN-001");
|
const response = await archiveTask("FN-001");
|
||||||
|
|
||||||
expect(response.column).toBe("archived");
|
expect(response.column).toBe("archived");
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/archive", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/archive", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@@ -1192,7 +1192,7 @@ describe("Git Management API", () => {
|
|||||||
const response = await unarchiveTask("FN-001");
|
const response = await unarchiveTask("FN-001");
|
||||||
|
|
||||||
expect(response.column).toBe("done");
|
expect(response.column).toBe("done");
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/unarchive", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/unarchive", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@@ -1228,7 +1228,7 @@ describe("Git Management API", () => {
|
|||||||
const response = await fetchWorkspaceFileList("FN-001", "src");
|
const response = await fetchWorkspaceFileList("FN-001", "src");
|
||||||
|
|
||||||
expect(response).toEqual(payload);
|
expect(response).toEqual(payload);
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files?workspace=FN-001&path=src", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files?workspace=KB-001&path=src", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1252,7 +1252,7 @@ describe("Git Management API", () => {
|
|||||||
const response = await saveWorkspaceFileContent("FN-001", "src/index.ts", "hello");
|
const response = await saveWorkspaceFileContent("FN-001", "src/index.ts", "hello");
|
||||||
|
|
||||||
expect(response).toEqual(payload);
|
expect(response).toEqual(payload);
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files/src%2Findex.ts?workspace=FN-001", {
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files/src%2Findex.ts?workspace=KB-001", {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ content: "hello" }),
|
body: JSON.stringify({ content: "hello" }),
|
||||||
@@ -2155,3 +2155,4 @@ describe("fetchProjectConfig", () => {
|
|||||||
expect(result.rootDir).toBe("/path/to/project");
|
expect(result.rootDir).toBe("/path/to/project");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import type {
|
|||||||
FeatureCreateInput,
|
FeatureCreateInput,
|
||||||
MissionStatus,
|
MissionStatus,
|
||||||
MilestoneStatus,
|
MilestoneStatus,
|
||||||
SliceStatus,
|
|
||||||
FeatureStatus,
|
|
||||||
InterviewState,
|
InterviewState,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
@@ -119,11 +117,9 @@ function validateOrderedIds(body: unknown): string[] {
|
|||||||
|
|
||||||
// ── Async Handler Wrapper ───────────────────────────────────────────────────
|
// ── Async Handler Wrapper ───────────────────────────────────────────────────
|
||||||
|
|
||||||
type TypedRequest = Request<Record<string, string>>;
|
function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
|
||||||
|
|
||||||
function asyncHandler(fn: (req: TypedRequest, res: Response, next: NextFunction) => Promise<void>) {
|
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
Promise.resolve(fn(req as TypedRequest, res, next)).catch(next);
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
// @vitest-environment node
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll, afterEach } from "vitest";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { EventEmitter } from "node:events";
|
|
||||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { execFileSync } from "node:child_process";
|
|
||||||
import { createApiRoutes } from "./routes.js";
|
import { createApiRoutes } from "./routes.js";
|
||||||
import { GitHubClient } from "./github.js";
|
import { GitHubClient } from "./github.js";
|
||||||
import { githubRateLimiter } from "./github-poll.js";
|
import { githubRateLimiter } from "./github-poll.js";
|
||||||
@@ -17,7 +10,6 @@ import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
|
|||||||
import { __resetPlanningState } from "./planning.js";
|
import { __resetPlanningState } from "./planning.js";
|
||||||
import { __resetSubtaskBreakdownState } from "./subtask-breakdown.js";
|
import { __resetSubtaskBreakdownState } from "./subtask-breakdown.js";
|
||||||
import * as terminalServiceModule from "./terminal-service.js";
|
import * as terminalServiceModule from "./terminal-service.js";
|
||||||
import { get as performGet, request as performRequest } from "./test-request.js";
|
|
||||||
|
|
||||||
// Mock @fusion/core for gh CLI auth checks
|
// Mock @fusion/core for gh CLI auth checks
|
||||||
vi.mock("@fusion/core", async () => {
|
vi.mock("@fusion/core", async () => {
|
||||||
@@ -41,18 +33,6 @@ function createMockGlobalSettingsStore() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockMissionStore() {
|
|
||||||
return {
|
|
||||||
createSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active" }),
|
|
||||||
getSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active", answers: [] }),
|
|
||||||
updateSession: vi.fn().mockResolvedValue(undefined),
|
|
||||||
addAnswer: vi.fn().mockResolvedValue(undefined),
|
|
||||||
deleteSession: vi.fn().mockResolvedValue(undefined),
|
|
||||||
listSessions: vi.fn().mockResolvedValue([]),
|
|
||||||
generatePlan: vi.fn().mockResolvedValue({ plan: "Test plan", steps: [] }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||||
return {
|
return {
|
||||||
getTask: vi.fn(),
|
getTask: vi.fn(),
|
||||||
@@ -83,7 +63,6 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
|||||||
getWorkflowStep: vi.fn(),
|
getWorkflowStep: vi.fn(),
|
||||||
updateWorkflowStep: vi.fn(),
|
updateWorkflowStep: vi.fn(),
|
||||||
deleteWorkflowStep: vi.fn(),
|
deleteWorkflowStep: vi.fn(),
|
||||||
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
@@ -101,11 +80,28 @@ const FAKE_TASK_DETAIL: TaskDetail = {
|
|||||||
prompt: "# KB-001\n\nTest task",
|
prompt: "# KB-001\n\nTest task",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Helper: send GET and return { status, body } */
|
||||||
async function GET(app: express.Express, path: string): Promise<{ status: number; body: any }> {
|
async function GET(app: express.Express, path: string): Promise<{ status: number; body: any }> {
|
||||||
const res = await performGet(app, path);
|
return new Promise((resolve, reject) => {
|
||||||
return { status: res.status, body: res.body };
|
const server = app.listen(0, () => {
|
||||||
|
const addr = server.address() as { port: number };
|
||||||
|
http.get(`http://127.0.0.1:${addr.port}${path}`, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => (data += chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
server.close();
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode!, body: JSON.parse(data) });
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode!, body: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on("error", (err) => { server.close(); reject(err); });
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Helper: send a request with method/body and return { status, body } */
|
||||||
async function REQUEST(
|
async function REQUEST(
|
||||||
app: express.Express,
|
app: express.Express,
|
||||||
method: string,
|
method: string,
|
||||||
@@ -113,8 +109,30 @@ async function REQUEST(
|
|||||||
body?: Buffer | string,
|
body?: Buffer | string,
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
): Promise<{ status: number; body: any }> {
|
): Promise<{ status: number; body: any }> {
|
||||||
const res = await performRequest(app, method, path, body, headers);
|
return new Promise((resolve, reject) => {
|
||||||
return { status: res.status, body: res.body };
|
const server = app.listen(0, () => {
|
||||||
|
const addr = server.address() as { port: number };
|
||||||
|
const url = new URL(`http://127.0.0.1:${addr.port}${path}`);
|
||||||
|
const req = http.request(
|
||||||
|
{ hostname: url.hostname, port: url.port, path: url.pathname, method, headers },
|
||||||
|
(res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => (data += chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
server.close();
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode!, body: JSON.parse(data) });
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode!, body: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on("error", (err) => { server.close(); reject(err); });
|
||||||
|
if (body) req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a minimal multipart/form-data body */
|
/** Build a minimal multipart/form-data body */
|
||||||
@@ -238,19 +256,13 @@ describe("POST /tasks", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(store.createTask).toHaveBeenCalledWith(
|
expect(store.createTask).toHaveBeenCalledWith({
|
||||||
expect.objectContaining({
|
title: undefined,
|
||||||
title: undefined,
|
description: "Big initiative",
|
||||||
description: "Big initiative",
|
column: undefined,
|
||||||
column: undefined,
|
dependencies: undefined,
|
||||||
dependencies: undefined,
|
breakIntoSubtasks: true,
|
||||||
breakIntoSubtasks: true,
|
});
|
||||||
summarize: false,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
settings: { autoSummarizeTitles: undefined },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards model overrides when both provider and id are supplied", async () => {
|
it("forwards model overrides when both provider and id are supplied", async () => {
|
||||||
@@ -279,23 +291,17 @@ describe("POST /tasks", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(store.createTask).toHaveBeenCalledWith(
|
expect(store.createTask).toHaveBeenCalledWith({
|
||||||
expect.objectContaining({
|
title: undefined,
|
||||||
title: undefined,
|
description: "Use explicit models",
|
||||||
description: "Use explicit models",
|
column: undefined,
|
||||||
column: undefined,
|
dependencies: undefined,
|
||||||
dependencies: undefined,
|
breakIntoSubtasks: undefined,
|
||||||
breakIntoSubtasks: undefined,
|
modelProvider: "anthropic",
|
||||||
modelProvider: "anthropic",
|
modelId: "claude-sonnet-4-5",
|
||||||
modelId: "claude-sonnet-4-5",
|
validatorModelProvider: "openai",
|
||||||
validatorModelProvider: "openai",
|
validatorModelId: "gpt-4o",
|
||||||
validatorModelId: "gpt-4o",
|
});
|
||||||
summarize: false,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
settings: { autoSummarizeTitles: undefined },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes partial model overrides back to defaults", async () => {
|
it("normalizes partial model overrides back to defaults", async () => {
|
||||||
@@ -318,23 +324,17 @@ describe("POST /tasks", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(store.createTask).toHaveBeenCalledWith(
|
expect(store.createTask).toHaveBeenCalledWith({
|
||||||
expect.objectContaining({
|
title: undefined,
|
||||||
title: undefined,
|
description: "Ignore partial model selection",
|
||||||
description: "Ignore partial model selection",
|
column: undefined,
|
||||||
column: undefined,
|
dependencies: undefined,
|
||||||
dependencies: undefined,
|
breakIntoSubtasks: undefined,
|
||||||
breakIntoSubtasks: undefined,
|
modelProvider: undefined,
|
||||||
modelProvider: undefined,
|
modelId: undefined,
|
||||||
modelId: undefined,
|
validatorModelProvider: undefined,
|
||||||
validatorModelProvider: undefined,
|
validatorModelId: undefined,
|
||||||
validatorModelId: undefined,
|
});
|
||||||
summarize: false,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
settings: { autoSummarizeTitles: undefined },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when model fields are not strings", async () => {
|
it("returns 400 when model fields are not strings", async () => {
|
||||||
@@ -595,8 +595,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined, error: undefined });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: undefined });
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when task is not in failed state", async () => {
|
it("returns 400 when task is not in failed state", async () => {
|
||||||
@@ -623,8 +623,8 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined, error: undefined });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: undefined });
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -655,7 +655,7 @@ describe("POST /tasks/:id/duplicate", () => {
|
|||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.id).toBe("FN-002");
|
expect(res.body.id).toBe("FN-002");
|
||||||
expect(res.body.column).toBe("triage");
|
expect(res.body.column).toBe("triage");
|
||||||
expect(store.duplicateTask).toHaveBeenCalledWith("KB-001");
|
expect(store.duplicateTask).toHaveBeenCalledWith("FN-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 404 when source task not found", async () => {
|
it("returns 404 when source task not found", async () => {
|
||||||
@@ -712,8 +712,8 @@ describe("POST /tasks/:id/refine", () => {
|
|||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.id).toBe("FN-002");
|
expect(res.body.id).toBe("FN-002");
|
||||||
expect(res.body.column).toBe("triage");
|
expect(res.body.column).toBe("triage");
|
||||||
expect(store.refineTask).toHaveBeenCalledWith("KB-001", "Need improvements");
|
expect(store.refineTask).toHaveBeenCalledWith("FN-001", "Need improvements");
|
||||||
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Refinement requested", "Need improvements");
|
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Refinement requested", "Need improvements");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates refinement task from in-review task and returns 201", async () => {
|
it("creates refinement task from in-review task and returns 201", async () => {
|
||||||
@@ -727,7 +727,7 @@ describe("POST /tasks/:id/refine", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.column).toBe("triage");
|
expect(res.body.column).toBe("triage");
|
||||||
expect(store.refineTask).toHaveBeenCalledWith("KB-001", "Fix edge cases");
|
expect(store.refineTask).toHaveBeenCalledWith("FN-001", "Fix edge cases");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when task is not in done or in-review column", async () => {
|
it("returns 400 when task is not in done or in-review column", async () => {
|
||||||
@@ -834,7 +834,7 @@ describe("POST /tasks/:id/archive", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.column).toBe("archived");
|
expect(res.body.column).toBe("archived");
|
||||||
expect(store.archiveTask).toHaveBeenCalledWith("KB-001");
|
expect(store.archiveTask).toHaveBeenCalledWith("FN-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when task is not in done column", async () => {
|
it("returns 400 when task is not in done column", async () => {
|
||||||
@@ -886,7 +886,7 @@ describe("POST /tasks/:id/unarchive", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.column).toBe("done");
|
expect(res.body.column).toBe("done");
|
||||||
expect(store.unarchiveTask).toHaveBeenCalledWith("KB-001");
|
expect(store.unarchiveTask).toHaveBeenCalledWith("FN-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when task is not in archived column", async () => {
|
it("returns 400 when task is not in archived column", async () => {
|
||||||
@@ -1260,7 +1260,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||||
title: undefined,
|
title: undefined,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
@@ -1281,7 +1281,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||||
title: "New",
|
title: "New",
|
||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
@@ -1312,7 +1312,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||||
title: undefined,
|
title: undefined,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
@@ -1361,7 +1361,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||||
title: undefined,
|
title: undefined,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
@@ -1411,7 +1411,7 @@ describe("Attachment routes", () => {
|
|||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.filename).toBe("1234-screenshot.png");
|
expect(res.body.filename).toBe("1234-screenshot.png");
|
||||||
expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
|
expect((store.addAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
|
||||||
"KB-001",
|
"FN-001",
|
||||||
"screenshot.png",
|
"screenshot.png",
|
||||||
expect.any(Buffer),
|
expect.any(Buffer),
|
||||||
"image/png",
|
"image/png",
|
||||||
@@ -1454,7 +1454,7 @@ describe("Attachment routes", () => {
|
|||||||
const res = await REQUEST(buildApp(), "DELETE", "/api/tasks/KB-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(res.status).toBe(200);
|
||||||
expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("KB-001", "1234-screenshot.png");
|
expect((store.deleteAttachment as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith("FN-001", "1234-screenshot.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("DELETE /tasks/:id/attachments/:filename — returns 404 for missing", async () => {
|
it("DELETE /tasks/:id/attachments/:filename — returns 404 for missing", async () => {
|
||||||
@@ -1478,7 +1478,7 @@ describe("Attachment routes", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toEqual(fakeLogs);
|
expect(res.body).toEqual(fakeLogs);
|
||||||
expect(store.getAgentLogs).toHaveBeenCalledWith("KB-001");
|
expect(store.getAgentLogs).toHaveBeenCalledWith("FN-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("GET /tasks/:id/logs — returns empty array when no logs", async () => {
|
it("GET /tasks/:id/logs — returns empty array when no logs", async () => {
|
||||||
@@ -1763,14 +1763,14 @@ describe("Pause/Unpause endpoints", () => {
|
|||||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toEqual({ id: "FN-001", paused: true });
|
expect(res.body).toEqual({ id: "FN-001", paused: true });
|
||||||
expect(store.pauseTask).toHaveBeenCalledWith("KB-001", true);
|
expect(store.pauseTask).toHaveBeenCalledWith("FN-001", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /tasks/:id/unpause — unpauses a task", async () => {
|
it("POST /tasks/:id/unpause — unpauses a task", async () => {
|
||||||
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "FN-001" });
|
(store.pauseTask as ReturnType<typeof vi.fn>).mockResolvedValue({ id: "FN-001" });
|
||||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/unpause");
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/unpause");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.pauseTask).toHaveBeenCalledWith("KB-001", false);
|
expect(store.pauseTask).toHaveBeenCalledWith("FN-001", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /tasks/:id/pause — returns 500 on error", async () => {
|
it("POST /tasks/:id/pause — returns 500 on error", async () => {
|
||||||
@@ -1807,7 +1807,7 @@ describe("Pause/Unpause endpoints", () => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.addTaskComment).toHaveBeenCalledWith("KB-001", "Hello", "user");
|
expect(store.addTaskComment).toHaveBeenCalledWith("FN-001", "Hello", "user");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("PATCH /tasks/:id/comments/:commentId — updates a task comment", async () => {
|
it("PATCH /tasks/:id/comments/:commentId — updates a task comment", async () => {
|
||||||
@@ -1821,7 +1821,7 @@ describe("Pause/Unpause endpoints", () => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.updateTaskComment).toHaveBeenCalledWith("KB-001", "c1", "Updated");
|
expect(store.updateTaskComment).toHaveBeenCalledWith("FN-001", "c1", "Updated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("DELETE /tasks/:id/comments/:commentId — deletes a task comment", async () => {
|
it("DELETE /tasks/:id/comments/:commentId — deletes a task comment", async () => {
|
||||||
@@ -1833,7 +1833,7 @@ describe("Pause/Unpause endpoints", () => {
|
|||||||
|
|
||||||
const res = await REQUEST(app, "DELETE", "/api/tasks/KB-001/comments/c1");
|
const res = await REQUEST(app, "DELETE", "/api/tasks/KB-001/comments/c1");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.deleteTaskComment).toHaveBeenCalledWith("KB-001", "c1");
|
expect(store.deleteTaskComment).toHaveBeenCalledWith("FN-001", "c1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1863,7 +1863,7 @@ describe("Pause/Unpause endpoints", () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toEqual(mockComment);
|
expect(res.body).toEqual(mockComment);
|
||||||
expect(store.addSteeringComment).toHaveBeenCalledWith(
|
expect(store.addSteeringComment).toHaveBeenCalledWith(
|
||||||
"KB-001",
|
"FN-001",
|
||||||
"Please handle the edge case",
|
"Please handle the edge case",
|
||||||
"user"
|
"user"
|
||||||
);
|
);
|
||||||
@@ -3160,10 +3160,19 @@ describe("POST /github/issues/batch-import", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles rate limit (429) with retry and eventual success", async () => {
|
it("handles rate limit (429) with retry and eventual success", async () => {
|
||||||
const throttledSpy = vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
|
fetchSpy
|
||||||
success: true,
|
.mockResolvedValueOnce({
|
||||||
data: mockGitHubIssue(1, "Issue After Rate Limit"),
|
ok: false,
|
||||||
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
|
status: 429,
|
||||||
|
statusText: "Too Many Requests",
|
||||||
|
headers: new Headers({ "Retry-After": "1" }),
|
||||||
|
json: () => Promise.resolve({ message: "Rate limited" }),
|
||||||
|
} as Response)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: () => Promise.resolve(mockGitHubIssue(1, "Issue After Rate Limit")),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3177,7 +3186,7 @@ describe("POST /github/issues/batch-import", () => {
|
|||||||
expect(res.body.results).toHaveLength(1);
|
expect(res.body.results).toHaveLength(1);
|
||||||
expect(res.body.results[0].success).toBe(true);
|
expect(res.body.results[0].success).toBe(true);
|
||||||
expect(res.body.results[0].taskId).toBeDefined();
|
expect(res.body.results[0].taskId).toBeDefined();
|
||||||
expect(throttledSpy).toHaveBeenCalledTimes(1);
|
expect(fetchSpy).toHaveBeenCalledTimes(2); // Initial 429 + 1 retry
|
||||||
}, 10000); // Increase timeout for retry delay
|
}, 10000); // Increase timeout for retry delay
|
||||||
|
|
||||||
it("returns error after max retries exceeded on 429", async () => {
|
it("returns error after max retries exceeded on 429", async () => {
|
||||||
@@ -3203,7 +3212,8 @@ describe("POST /github/issues/batch-import", () => {
|
|||||||
expect(res.body.results[0].success).toBe(false);
|
expect(res.body.results[0].success).toBe(false);
|
||||||
expect(res.body.results[0].error).toContain("rate limit");
|
expect(res.body.results[0].error).toContain("rate limit");
|
||||||
expect(res.body.results[0].retryAfter).toBe(1);
|
expect(res.body.results[0].retryAfter).toBe(1);
|
||||||
expect(fetchSpy.mock.calls.length).toBeGreaterThanOrEqual(4);
|
// Initial attempt + 3 retries = 4 calls
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(4);
|
||||||
}, 15000); // Increase timeout for multiple retries
|
}, 15000); // Increase timeout for multiple retries
|
||||||
|
|
||||||
it("processes issues sequentially (not parallel)", async () => {
|
it("processes issues sequentially (not parallel)", async () => {
|
||||||
@@ -3653,31 +3663,9 @@ describe("POST /tasks/:id/reject-plan", () => {
|
|||||||
|
|
||||||
describe("Git Management endpoints", () => {
|
describe("Git Management endpoints", () => {
|
||||||
let store: TaskStore;
|
let store: TaskStore;
|
||||||
let gitRepoDir: string;
|
|
||||||
let gitTestRoot: string;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
gitTestRoot = mkdtempSync(join(tmpdir(), "kb-dashboard-git-"));
|
|
||||||
const remoteDir = join(gitTestRoot, "remote.git");
|
|
||||||
gitRepoDir = join(gitTestRoot, "repo");
|
|
||||||
|
|
||||||
mkdirSync(gitRepoDir, { recursive: true });
|
|
||||||
execFileSync("git", ["init", "--bare", remoteDir]);
|
|
||||||
execFileSync("git", ["init", gitRepoDir]);
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "config", "user.email", "kb-tests@example.com"]);
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "config", "user.name", "KB Tests"]);
|
|
||||||
writeFileSync(join(gitRepoDir, "README.md"), "# Test Repo\n");
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "add", "README.md"]);
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "commit", "-m", "Initial commit"]);
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "remote", "add", "origin", remoteDir]);
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "push", "-u", "origin", "HEAD"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Use the actual project root so git commands work
|
store = createMockStore();
|
||||||
store = createMockStore({
|
|
||||||
getRootDir: vi.fn().mockReturnValue(process.cwd()),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildApp() {
|
function buildApp() {
|
||||||
@@ -4783,25 +4771,39 @@ describe("Terminal WebSocket close handler", () => {
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
setupTerminalWebSocket(app, server);
|
setupTerminalWebSocket(app, server);
|
||||||
class FakeWebSocket extends EventEmitter {
|
|
||||||
send = vi.fn();
|
|
||||||
close = vi.fn(() => this.emit("close"));
|
|
||||||
terminate = vi.fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = new FakeWebSocket();
|
await new Promise<void>((resolve, reject) => {
|
||||||
const wss = (app as express.Express & { terminalWsServer?: EventEmitter }).terminalWsServer;
|
server.listen(0, () => {
|
||||||
expect(wss).toBeTruthy();
|
const addr = server.address() as { port: number };
|
||||||
|
const { WebSocket: WsClient } = require("ws");
|
||||||
|
const ws = new WsClient(`ws://127.0.0.1:${addr.port}/api/terminal/ws?sessionId=term-ws-test`);
|
||||||
|
|
||||||
wss!.emit("connection", ws, {
|
ws.on("open", () => {
|
||||||
url: "/api/terminal/ws?sessionId=term-ws-test",
|
// Close the WebSocket - this should trigger killSession
|
||||||
headers: { host: "127.0.0.1" },
|
ws.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
// Give the close handler time to execute
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
expect(killSessionMock).toHaveBeenCalledWith("term-ws-test");
|
||||||
|
server.close();
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
server.close();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (err: Error) => {
|
||||||
|
server.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.close();
|
|
||||||
|
|
||||||
expect(killSessionMock).toHaveBeenCalledWith("term-ws-test");
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4834,25 +4836,32 @@ describe("Terminal WebSocket close handler", () => {
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
setupTerminalWebSocket(app, server);
|
setupTerminalWebSocket(app, server);
|
||||||
class FakeWebSocket extends EventEmitter {
|
|
||||||
send = vi.fn();
|
|
||||||
close = vi.fn(() => this.emit("close"));
|
|
||||||
terminate = vi.fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = new FakeWebSocket();
|
await new Promise<void>((resolve, reject) => {
|
||||||
const wss = (app as express.Express & { terminalWsServer?: EventEmitter }).terminalWsServer;
|
server.listen(0, () => {
|
||||||
expect(wss).toBeTruthy();
|
const addr = server.address() as { port: number };
|
||||||
|
const { WebSocket: WsClient } = require("ws");
|
||||||
|
const ws = new WsClient(`ws://127.0.0.1:${addr.port}/api/terminal/ws?sessionId=term-ws-err`);
|
||||||
|
|
||||||
wss!.emit("connection", ws, {
|
ws.on("open", () => {
|
||||||
url: "/api/terminal/ws?sessionId=term-ws-err",
|
// Force-terminate the connection to trigger error/close
|
||||||
headers: { host: "127.0.0.1" },
|
ws.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// After termination, give the handler time to run
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
expect(killSessionMock).toHaveBeenCalledWith("term-ws-err");
|
||||||
|
server.close();
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
server.close();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.emit("error", new Error("synthetic websocket failure"));
|
|
||||||
|
|
||||||
expect(killSessionMock).toHaveBeenCalledWith("term-ws-err");
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user