Stabilize dashboard tests against constructor and request-harness edge cases. - update dashboard route and service tests to use constructor-safe mock implementations and reset targeted spies between cases - make the test request helper resume its mock socket, always report successful writes, and preserve end callbacks - tighten board workflow route assertions around explicit workflow-column flag disabling and cache-bounded slim task snapshots Files changed: .../__tests__/auth-middleware-integration.test.ts | 4 ++-- .../src/__tests__/browse-directory-routes.test.ts | 4 ++-- packages/dashboard/src/__tests__/chat.test.ts | 3 ++- .../src/__tests__/discovery-routes.test.ts | 4 ++-- .../src/__tests__/docker-node-routes.test.ts | 4 ++-- .../__tests__/experiment-routes.finalize.test.ts | 2 +- .../src/__tests__/github-issue-comment.test.ts | 4 ++-- .../dashboard/src/__tests__/github-poll.test.ts | 4 ++-- .../__tests__/github-source-issue-close.test.ts | 4 ++-- .../github-source-issue-reconciler.test.ts | 4 ++-- .../src/__tests__/github-tracking-comments.test.ts | 4 ++-- .../src/__tests__/github-tracking-delete.test.ts | 4 ++-- .../src/__tests__/github-tracking-hook.test.ts | 4 ++-- ...ithub-tracking-periodic-reconcile-sweep.test.ts | 12 +++++----- .../__tests__/github-tracking-reconciler.test.ts | 4 ++-- .../src/__tests__/github-tracking-state.test.ts | 4 ++-- .../src/__tests__/github-tracking-unlink.test.ts | 4 ++-- .../src/__tests__/github-tracking.test.ts | 4 ++-- .../dashboard/src/__tests__/mesh-routes.test.ts | 4 ++-- .../dashboard/src/__tests__/node-routes.test.ts | 4 ++-- .../src/__tests__/pi-extensions-routes.test.ts | 10 ++++----- .../src/__tests__/plugin-routes.routes.test.ts | 4 ++-- .../dashboard/src/__tests__/plugin-routes.test.ts | 4 ++-- .../__tests__/project-pause-resume-routes.test.ts | 4 ++-- .../dashboard/src/__tests__/project-routes.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-agents.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-auth.test.ts | 6 +++-- .../src/__tests__/routes-automation.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-git.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-github.test.ts | 4 ++-- .../src/__tests__/routes-planning-tracking.test.ts | 1 + .../src/__tests__/routes-planning.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-proxy.test.ts | 8 +++---- .../src/__tests__/routes-settings.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-system.test.ts | 7 ++++-- .../src/__tests__/routes-tasks-ops.test.ts | 4 ++-- .../dashboard/src/__tests__/routes-tasks.test.ts | 4 ++-- .../src/__tests__/session-resume-history.test.ts | 1 + .../dashboard/src/__tests__/setup-routes.test.ts | 8 +++---- .../shared-branch-group-entry-points.test.ts | 4 ++-- .../routes/__tests__/board-workflows-route.test.ts | 26 +++++++++++++--------- .../src/routes/__tests__/custom-providers.test.ts | 4 ++-- .../routes/__tests__/docker-node-routes.test.ts | 4 ++-- .../__tests__/register-docker-node-routes.test.ts | 6 ++--- .../register-docker-provisioning-routes.test.ts | 10 ++++----- packages/dashboard/src/test-request.ts | 7 ++++-- 46 files changed, 126 insertions(+), 109 deletions(-) Fusion-Task-Id: FN-6146 Fusion-Task-Lineage: 989e9da3-92ed-43bd-a505-6114de4f1120
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
import http from "node:http";
|
|
import type { Socket } from "node:net";
|
|
import { PassThrough } from "node:stream";
|
|
|
|
type TestResponse = {
|
|
status: number;
|
|
body: unknown;
|
|
headers: http.OutgoingHttpHeaders;
|
|
};
|
|
|
|
class MockSocket extends PassThrough {
|
|
public writable = true;
|
|
public readable = true;
|
|
public remoteAddress = "127.0.0.1";
|
|
public encrypted = false;
|
|
|
|
setTimeout(): this {
|
|
return this;
|
|
}
|
|
|
|
setNoDelay(): this {
|
|
return this;
|
|
}
|
|
|
|
setKeepAlive(): this {
|
|
return this;
|
|
}
|
|
|
|
destroySoon(): void {
|
|
this.destroy();
|
|
}
|
|
}
|
|
|
|
function normalizeBody(body: Buffer | string | undefined): Buffer | undefined {
|
|
if (body === undefined) return undefined;
|
|
return Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
}
|
|
|
|
export async function request(
|
|
app: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
|
method: string,
|
|
path: string,
|
|
body?: Buffer | string,
|
|
headers: Record<string, string> = {},
|
|
rawBody: Buffer | undefined = undefined,
|
|
): Promise<TestResponse> {
|
|
const normalizedBody = normalizeBody(body);
|
|
const socket = new MockSocket();
|
|
socket.resume();
|
|
const req = new http.IncomingMessage(socket as unknown as Socket);
|
|
const res = new http.ServerResponse(req);
|
|
const chunks: Buffer[] = [];
|
|
|
|
req.method = method;
|
|
req.url = path;
|
|
req.httpVersion = "1.1";
|
|
req.headers = Object.fromEntries(
|
|
Object.entries({
|
|
host: "127.0.0.1",
|
|
...headers,
|
|
...(normalizedBody ? { "content-length": String(normalizedBody.length) } : {}),
|
|
}).map(([key, value]) => [key.toLowerCase(), value]),
|
|
);
|
|
|
|
res.assignSocket(socket as unknown as Socket);
|
|
|
|
const originalWrite = res.write.bind(res);
|
|
const originalEnd = res.end.bind(res);
|
|
|
|
res.write = ((chunk: string | Buffer, encoding?: BufferEncoding | ((error?: Error | null) => void), cb?: (error?: Error | null) => void) => {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof encoding === "string" ? encoding : undefined));
|
|
originalWrite(chunk as never, encoding as never, cb);
|
|
return true;
|
|
}) as typeof res.write;
|
|
|
|
res.end = ((chunk?: string | Buffer, encoding?: BufferEncoding | (() => void), cb?: () => void) => {
|
|
if (chunk !== undefined) {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof encoding === "string" ? encoding : undefined));
|
|
}
|
|
const callback = typeof encoding === "function" ? encoding : cb;
|
|
return originalEnd(chunk as never, encoding as never, callback as never);
|
|
}) as typeof res.end;
|
|
|
|
const response = new Promise<TestResponse>((resolve, reject) => {
|
|
res.on("finish", () => {
|
|
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
const contentType = res.getHeader("content-type");
|
|
const shouldParseJson = typeof contentType === "string" && contentType.includes("application/json");
|
|
|
|
try {
|
|
resolve({
|
|
status: res.statusCode,
|
|
body: shouldParseJson && rawBody.length > 0 ? JSON.parse(rawBody) : rawBody,
|
|
headers: res.getHeaders(),
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
res.on("error", reject);
|
|
});
|
|
|
|
if (rawBody) {
|
|
(req as http.IncomingMessage & { rawBody?: Buffer }).rawBody = rawBody;
|
|
}
|
|
|
|
app(req, res);
|
|
|
|
process.nextTick(() => {
|
|
if (normalizedBody) {
|
|
req.emit("data", normalizedBody);
|
|
}
|
|
req.complete = true;
|
|
req.emit("end");
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
export async function get(
|
|
app: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
|
path: string,
|
|
): Promise<TestResponse> {
|
|
return request(app, "GET", path);
|
|
}
|