feat: route profile consumers through sessiond

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 23:09:32 +02:00
parent 141cda93c8
commit 97e0afc6fa
15 changed files with 637 additions and 118 deletions
+154
View File
@@ -0,0 +1,154 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ActiveAgentProfileDescriptor, PiWebConfigResponse, PiWebPluginInfo } from "../shared/apiTypes.js";
import type { SessionDaemonAgentProfileResult } from "../sessiond/sessionDaemonClient.js";
import type { ActiveAgentProfileProvider } from "./activeAgentProfileProvider.js";
import { buildApp } from "./app.js";
import type { PiWebConfigService } from "./configRoutes.js";
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "pi-web-active-profile-app-"));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe("buildApp active profile composition", () => {
it("routes package and package-backed plugin reads through the same refreshable provider", async () => {
const firstAgentDir = join(tempDir, "first-agent");
const secondAgentDir = join(tempDir, "second-agent");
const firstPackageDir = join(tempDir, "first-package");
const secondPackageDir = join(tempDir, "second-package");
await Promise.all([
writePackagePlugin(firstPackageDir, "profile-first"),
writePackagePlugin(secondPackageDir, "profile-second"),
writePiPackageSettings(firstAgentDir, [firstPackageDir]),
writePiPackageSettings(secondAgentDir, [secondPackageDir]),
]);
let result: SessionDaemonAgentProfileResult = { status: "available", profile: activeProfile("a", "first-agent", firstAgentDir) };
const getActiveAgentProfile = vi.fn(() => Promise.resolve(result));
const app = await buildApp({
agentProfileProvider: { getActiveAgentProfile },
config: emptyConfigService(),
clientDist: false,
logger: false,
});
try {
const firstPackages = await app.inject({ method: "GET", url: "/api/pi-packages" });
const firstPlugins = await app.inject({ method: "GET", url: "/api/plugins" });
expect(firstPackages.statusCode).toBe(200);
expect(packageSources(firstPackages.json())).toContain(firstPackageDir);
expect(pluginIds(firstPlugins.json())).toContain("profile-first");
expect(pluginIds(firstPlugins.json())).not.toContain("profile-second");
result = { status: "available", profile: activeProfile("b", "second-agent", secondAgentDir) };
const secondPackages = await app.inject({ method: "GET", url: "/api/pi-packages" });
const secondPlugins = await app.inject({ method: "GET", url: "/api/plugins" });
expect(secondPackages.statusCode).toBe(200);
expect(packageSources(secondPackages.json())).toContain(secondPackageDir);
expect(packageSources(secondPackages.json())).not.toContain(firstPackageDir);
expect(pluginIds(secondPlugins.json())).toContain("profile-second");
expect(pluginIds(secondPlugins.json())).not.toContain("profile-first");
expect(getActiveAgentProfile).toHaveBeenCalledTimes(4);
} finally {
await app.close();
}
});
it.each(["unavailable", "invalid"] as const)("returns 503 instead of falling back when the active profile is %s", async (status) => {
const provider: ActiveAgentProfileProvider = {
getActiveAgentProfile: () => Promise.resolve({ status, error: `${status} daemon profile` }),
};
const app = await buildApp({
agentProfileProvider: provider,
config: emptyConfigService(),
clientDist: false,
logger: false,
});
try {
const packages = await app.inject({ method: "GET", url: "/api/pi-packages" });
const plugins = await app.inject({ method: "GET", url: "/api/plugins" });
const manifest = await app.inject({ method: "GET", url: "/pi-web-plugins/manifest.json" });
expect(packages.statusCode).toBe(503);
expect(packages.json()).toEqual({ error: `Active agent profile is ${status}: ${status} daemon profile` });
expect(plugins.statusCode).toBe(503);
expect(plugins.json()).toEqual({ error: `Active agent profile is ${status}: ${status} daemon profile` });
expect(manifest.statusCode).toBe(503);
expect(manifest.json()).toEqual({ error: `Active agent profile is ${status}: ${status} daemon profile` });
} finally {
await app.close();
}
});
});
function activeProfile(revisionCharacter: string, command: string, dir: string): ActiveAgentProfileDescriptor {
return {
schemaVersion: 1,
revision: `sha256:${revisionCharacter.repeat(64)}`,
command,
dir,
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR"],
};
}
async function writePiPackageSettings(agentDir: string, packages: string[]): Promise<void> {
await mkdir(agentDir, { recursive: true });
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ packages }, null, 2)}\n`, "utf8");
}
async function writePackagePlugin(root: string, pluginId: string): Promise<void> {
await mkdir(root, { recursive: true });
await writeFile(join(root, "package.json"), `${JSON.stringify({
name: `@test/${pluginId}`,
version: "1.0.0",
piWeb: { plugins: [{ id: pluginId, module: "pi-web-plugin.js" }] },
}, null, 2)}\n`, "utf8");
await writeFile(join(root, "pi-web-plugin.js"), "export default {};\n", "utf8");
}
function emptyConfigService(): PiWebConfigService {
const response: PiWebConfigResponse = {
path: join(tempDir, "config.json"),
exists: false,
config: {},
effectiveConfig: {},
envOverrides: {
host: false,
port: false,
allowedHosts: false,
spawnSessions: false,
subsessions: false,
agentCommand: false,
agentDir: false,
agentSessionDir: false,
},
};
return {
read: () => Promise.resolve(response),
write: () => Promise.resolve(response),
};
}
function packageSources(value: unknown): string[] {
if (!isRecord(value) || !Array.isArray(value["packages"])) return [];
return value["packages"].flatMap((entry) => isRecord(entry) && typeof entry["source"] === "string" ? [entry["source"]] : []);
}
function pluginIds(value: unknown): PiWebPluginInfo["id"][] {
if (!isRecord(value) || !Array.isArray(value["plugins"])) return [];
return value["plugins"].flatMap((entry) => isRecord(entry) && typeof entry["id"] === "string" ? [entry["id"]] : []);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}