Archived
test(server): split app route specs
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { mkdtemp, realpath, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { Readable } from "node:stream";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { afterEach, beforeEach } from "vitest";
|
||||
import { buildApp } from "./app.js";
|
||||
import { ProjectService } from "./projects/projectService.js";
|
||||
import { ProjectStore } from "./storage/projectStore.js";
|
||||
import type { MachineClient } from "./machines/machineClient.js";
|
||||
import { MachineService } from "./machines/machineService.js";
|
||||
import { MachineStore } from "./machines/machineStore.js";
|
||||
import { WorkspaceService } from "./workspaces/workspaceService.js";
|
||||
import type { PiPackageService } from "./piPackageService.js";
|
||||
import type { SessionProxyDaemon } from "./sessiond/sessionProxyRoutes.js";
|
||||
import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js";
|
||||
import type { PiPackageInfo, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
|
||||
|
||||
interface AppTestContext {
|
||||
readonly app: FastifyInstance;
|
||||
readonly tempDir: string;
|
||||
readonly projectDir: string;
|
||||
remoteClient: MachineClient | undefined;
|
||||
readonly sessionDaemonRequests: CapturedSessionDaemonRequest[];
|
||||
readonly piPackageRequests: CapturedPiPackageRequest[];
|
||||
piWebConfig: PiWebConfigValues;
|
||||
}
|
||||
|
||||
let app: FastifyInstance | undefined;
|
||||
let tempDir: string | undefined;
|
||||
let projectDir: string | undefined;
|
||||
let remoteClient: MachineClient | undefined;
|
||||
let sessionDaemonRequests: CapturedSessionDaemonRequest[] = [];
|
||||
let piPackageRequests: CapturedPiPackageRequest[] = [];
|
||||
let piWebConfig: PiWebConfigValues = {};
|
||||
|
||||
export const appTestContext: AppTestContext = {
|
||||
get app() {
|
||||
if (app === undefined) throw new Error("App test harness was not initialized");
|
||||
return app;
|
||||
},
|
||||
get tempDir() {
|
||||
if (tempDir === undefined) throw new Error("App test tempDir was not initialized");
|
||||
return tempDir;
|
||||
},
|
||||
get projectDir() {
|
||||
if (projectDir === undefined) throw new Error("App test projectDir was not initialized");
|
||||
return projectDir;
|
||||
},
|
||||
get remoteClient() {
|
||||
return remoteClient;
|
||||
},
|
||||
set remoteClient(client) {
|
||||
remoteClient = client;
|
||||
},
|
||||
get sessionDaemonRequests() {
|
||||
return sessionDaemonRequests;
|
||||
},
|
||||
get piPackageRequests() {
|
||||
return piPackageRequests;
|
||||
},
|
||||
get piWebConfig() {
|
||||
return piWebConfig;
|
||||
},
|
||||
set piWebConfig(config) {
|
||||
piWebConfig = config;
|
||||
},
|
||||
};
|
||||
|
||||
export function registerAppTestHooks(): void {
|
||||
beforeEach(async () => {
|
||||
tempDir = await realpath(await mkdtemp(join(tmpdir(), "pi-web-app-test-")));
|
||||
projectDir = join(tempDir, "project");
|
||||
remoteClient = undefined;
|
||||
sessionDaemonRequests = [];
|
||||
piPackageRequests = [];
|
||||
piWebConfig = {};
|
||||
app = await buildApp({
|
||||
projects: new ProjectService(new ProjectStore(join(tempDir, "projects.json"))),
|
||||
workspaces: new WorkspaceService(),
|
||||
machines: new MachineService(new MachineStore(join(tempDir, "machines.json")), {
|
||||
remoteClientFactory: () => {
|
||||
if (remoteClient === undefined) throw new Error("No remote machine client configured");
|
||||
return remoteClient;
|
||||
},
|
||||
now: () => new Date("2026-05-25T00:00:00.000Z"),
|
||||
localRuntime: () => Promise.resolve({
|
||||
packageName: "@jmfederico/pi-web",
|
||||
generatedAt: "2026-05-25T00:00:00.000Z",
|
||||
components: {
|
||||
web: { component: "web", label: "PI WEB", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
|
||||
sessiond: { component: "sessiond", label: "PI WEB Session Daemon", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
|
||||
},
|
||||
capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived],
|
||||
}),
|
||||
}),
|
||||
sessionDaemon: fakeSessionDaemon(),
|
||||
config: fakeConfigService(),
|
||||
piPackages: fakePiPackageService(),
|
||||
piWebPlugins: {
|
||||
manifest: () => Promise.resolve({ plugins: [{ id: "fake", module: "/pi-web-plugins/fake/plugin.js?v=1", source: "test", scope: "local", machineSpecific: false }] }),
|
||||
plugins: () => Promise.resolve({ plugins: [{ id: "fake", module: "/pi-web-plugins/fake/plugin.js?v=1", source: "test", scope: "local", machineSpecific: false, enabled: true }] }),
|
||||
readAsset: (pluginId, assetPath) => Promise.resolve(pluginId === "fake" && assetPath === "plugin.js" ? { content: Buffer.from("export default {};"), contentType: "application/javascript; charset=utf-8" } : undefined),
|
||||
},
|
||||
clientDist: false,
|
||||
logger: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const appToClose = app;
|
||||
const tempDirToRemove = tempDir;
|
||||
app = undefined;
|
||||
tempDir = undefined;
|
||||
projectDir = undefined;
|
||||
remoteClient = undefined;
|
||||
sessionDaemonRequests = [];
|
||||
piPackageRequests = [];
|
||||
piWebConfig = {};
|
||||
|
||||
if (appToClose !== undefined) await appToClose.close();
|
||||
if (tempDirToRemove !== undefined) await rm(tempDirToRemove, { recursive: true, force: true });
|
||||
});
|
||||
}
|
||||
|
||||
export interface CapturedSessionDaemonRequest {
|
||||
method: string;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
interface CapturedPiPackageRequest {
|
||||
action: "list" | "install" | "remove" | "update";
|
||||
source?: string;
|
||||
scope?: "user" | "project";
|
||||
}
|
||||
|
||||
function fakeConfigService() {
|
||||
return {
|
||||
read: () => piWebConfigResponse(piWebConfig),
|
||||
write: (config: PiWebConfigValues) => {
|
||||
piWebConfig = config;
|
||||
return piWebConfigResponse(config);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function fullPiWebConfig(): PiWebConfigValues {
|
||||
return {
|
||||
host: "127.0.0.1",
|
||||
port: 8504,
|
||||
allowedHosts: ["gateway.example.test"],
|
||||
shortcuts: { "core:view.chat": "mod+1" },
|
||||
plugins: { info: { enabled: true, settings: { note: "remote" } } },
|
||||
pathAccess: { allowedPaths: ["/srv/repos"] },
|
||||
uploads: { defaultFolder: "uploads" },
|
||||
maxUploadBytes: 1024,
|
||||
spawnSessions: false,
|
||||
subsessions: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectedMachinePiWebConfig(): PiWebConfigValues {
|
||||
return {
|
||||
plugins: { info: { enabled: true, settings: { note: "remote" } } },
|
||||
pathAccess: { allowedPaths: ["/srv/repos"] },
|
||||
uploads: { defaultFolder: "uploads" },
|
||||
maxUploadBytes: 1024,
|
||||
spawnSessions: false,
|
||||
subsessions: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function piWebConfigResponse(config: PiWebConfigValues): PiWebConfigResponse {
|
||||
return {
|
||||
path: join(appTestContext.tempDir, "config.json"),
|
||||
exists: false,
|
||||
config,
|
||||
effectiveConfig: config,
|
||||
envOverrides: { host: false, port: false, allowedHosts: false, spawnSessions: false, subsessions: false },
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineConfigWriteBody {
|
||||
config: PiWebConfigValues;
|
||||
}
|
||||
|
||||
export function configFromMachineConfigWriteBody(body: unknown): PiWebConfigValues {
|
||||
if (!isMachineConfigWriteBody(body)) throw new Error("Expected machine config write body");
|
||||
return body.config;
|
||||
}
|
||||
|
||||
function isMachineConfigWriteBody(value: unknown): value is MachineConfigWriteBody {
|
||||
if (!isRecord(value)) return false;
|
||||
return isRecord(value["config"]);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function fakePiPackageService(): PiPackageService {
|
||||
const packages: PiPackageInfo[] = [{ source: "npm:@acme/tools", scope: "user", filtered: false, installedPath: "/tmp/pi-tools" }];
|
||||
return {
|
||||
list: () => {
|
||||
piPackageRequests.push({ action: "list" });
|
||||
return Promise.resolve({ packages });
|
||||
},
|
||||
install: (source) => {
|
||||
piPackageRequests.push({ action: "install", source });
|
||||
return Promise.resolve({ action: "install", source, packages });
|
||||
},
|
||||
remove: (source, scope = "user") => {
|
||||
piPackageRequests.push({ action: "remove", source, scope });
|
||||
return Promise.resolve({ action: "remove", source, scope, removed: true, packages });
|
||||
},
|
||||
update: (source) => {
|
||||
piPackageRequests.push({ action: "update", ...(source === undefined ? {} : { source }) });
|
||||
return Promise.resolve({ action: "update", ...(source === undefined ? {} : { source }), packages });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeSessionDaemon(): SessionProxyDaemon {
|
||||
return {
|
||||
request: (method, path, body) => {
|
||||
const captured = { method, path, ...(body === undefined ? {} : { body }) } satisfies CapturedSessionDaemonRequest;
|
||||
sessionDaemonRequests.push(captured);
|
||||
return Promise.resolve({
|
||||
statusCode: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(captured),
|
||||
});
|
||||
},
|
||||
connectWebSocket: () => { throw new Error("WebSocket not configured for test"); },
|
||||
};
|
||||
}
|
||||
|
||||
export function fakeRemoteClient(overrides: Partial<MachineClient>): MachineClient {
|
||||
return {
|
||||
request: () => Promise.resolve({ statusCode: 200, headers: {}, body: Readable.from([]) }),
|
||||
requestJson: () => Promise.resolve({ statusCode: 200, headers: {}, body: undefined }),
|
||||
connectWebSocket: () => { throw new Error("WebSocket not configured for test"); },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user