Merge remote-tracking branch 'origin/main' into pr-36-generic-agent-config

# Conflicts:
#	docs/config.html
#	docs/config.md
#	src/cli.test.ts
#	src/cli.ts
#	src/client/src/components/settings/SettingsSessiondPanel.ts
#	src/client/src/components/settings/settingsConfigDraft.test.ts
#	src/client/src/components/settings/settingsConfigDraft.ts
#	src/server/app.test.ts
#	src/server/app.ts
#	src/server/configRoutes.test.ts
#	src/server/configRoutes.ts
#	src/server/piWebPluginService.test.ts
#	src/server/piWebPluginService.ts
#	src/server/piWebStatus.test.ts
#	src/server/piWebStatus.ts
#	src/server/piWebStatusCache.ts
#	src/server/sessions/authService.test.ts
#	src/server/sessions/piSessionService.ts
#	src/server/sessions/sessionRoutes.test.ts
This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 20:30:08 +02:00
289 changed files with 29164 additions and 6419 deletions
+86 -2
View File
@@ -2,7 +2,7 @@ import { resolve } from "node:path";
import Fastify, { type FastifyInstance } from "fastify";
import fastifyWebsocket from "@fastify/websocket";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { SessionCleanupExecuteResponse, SessionCleanupPreviewResponse } from "../../shared/apiTypes.js";
import type { MessagePage, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkMutationRef, SessionCleanupExecuteResponse, SessionCleanupPreviewResponse } from "../../shared/apiTypes.js";
import { SessionEventHub } from "../realtime/sessionEventHub.js";
import { PiSessionService, type PiSessionManagerGateway } from "./piSessionService.js";
import type { SessionRouteLookup, SessionRouteService } from "./sessionService.js";
@@ -56,6 +56,32 @@ describe("session routes", () => {
}
});
it("omits thinking signatures from browser history without mutating service messages", async () => {
const routeApp = Fastify({ logger: false });
await routeApp.register(fastifyWebsocket);
const eventHub = new SessionEventHub();
const routeService = new CapturingRouteSessionService();
const thinkingBlock = { type: "thinking", thinking: "private chain", thinkingSignature: "opaque-provider-payload", redacted: true };
const message = { role: "assistant", content: [thinkingBlock, { type: "text", text: "visible answer" }] };
routeService.messagesResponse = { messages: [message], start: 0, total: 1 };
registerSessionRoutes(routeApp, routeService, eventHub);
try {
const response = await routeApp.inject({ method: "GET", url: "/sessions/session-1/messages?limit=20" });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "private chain", redacted: true }, { type: "text", text: "visible answer" }] }],
start: 0,
total: 1,
});
expect(thinkingBlock.thinkingSignature).toBe("opaque-provider-payload");
} finally {
await routeService.dispose();
await routeApp.close();
}
});
it("forwards prompt attachments and supports the save-attachments route", async () => {
const routeApp = Fastify({ logger: false });
await routeApp.register(fastifyWebsocket);
@@ -179,13 +205,59 @@ describe("session routes", () => {
await routeApp.close();
}
});
it("routes bulk archive and delete requests with normalized session refs", async () => {
const routeApp = Fastify({ logger: false });
await routeApp.register(fastifyWebsocket);
const eventHub = new SessionEventHub();
const routeService = new CapturingRouteSessionService();
registerSessionRoutes(routeApp, routeService, eventHub);
try {
const requestCwd = resolve("/repo");
const archiveResponse = await routeApp.inject({ method: "POST", url: "/sessions/bulk/archive", payload: { sessions: [{ id: "s1", cwd: requestCwd }, { id: "s2" }] } });
const deleteResponse = await routeApp.inject({ method: "POST", url: "/sessions/bulk/delete-archived", payload: { sessions: [{ id: "s1", cwd: requestCwd }] } });
expect(archiveResponse.statusCode).toBe(200);
expect(archiveResponse.json()).toMatchObject({ archived: true, archivedSessionIds: ["s1", "s2"], failures: [] });
expect(deleteResponse.statusCode).toBe(200);
expect(deleteResponse.json()).toMatchObject({ deleted: true, deletedSessionIds: ["s1"], failures: [] });
expect(routeService.bulkArchiveCalls).toEqual([[{ id: "s1", cwd: requestCwd }, { id: "s2" }]]);
expect(routeService.bulkDeleteCalls).toEqual([[{ id: "s1", cwd: requestCwd }]]);
} finally {
await routeService.dispose();
await routeApp.close();
}
});
it("rejects malformed bulk mutation bodies before calling the service", async () => {
const routeApp = Fastify({ logger: false });
await routeApp.register(fastifyWebsocket);
const eventHub = new SessionEventHub();
const routeService = new CapturingRouteSessionService();
registerSessionRoutes(routeApp, routeService, eventHub);
try {
const response = await routeApp.inject({ method: "POST", url: "/sessions/bulk/archive", payload: { sessions: [{ cwd: "/repo" }] } });
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({ error: "id field must be a string" });
expect(routeService.bulkArchiveCalls).toEqual([]);
} finally {
await routeService.dispose();
await routeApp.close();
}
});
});
class CapturingRouteSessionService implements SessionRouteService {
readonly calls: unknown[] = [];
readonly reloadCalls: SessionRouteLookup[] = [];
messagesResponse: unknown[] | MessagePage = [];
readonly cleanupPreviewCalls: NormalizedSessionCleanupRequest[] = [];
readonly cleanupCalls: NormalizedSessionCleanupRequest[] = [];
readonly bulkArchiveCalls: SessionBulkMutationRef[][] = [];
readonly bulkDeleteCalls: SessionBulkMutationRef[][] = [];
reloadError: Error | undefined;
cleanupPreview(request: NormalizedSessionCleanupRequest): Promise<SessionCleanupPreviewResponse> {
@@ -198,6 +270,16 @@ class CapturingRouteSessionService implements SessionRouteService {
return Promise.resolve({ generatedAt: "2026-06-25T00:00:00.000Z", thresholds: request.thresholds, projects: [], totals: { archiveCount: 0, deleteCount: 0 }, archivedSessionIds: [], deletedSessionIds: [] });
}
archiveMany(refs: readonly SessionBulkMutationRef[]): Promise<SessionBulkArchiveResponse> {
this.bulkArchiveCalls.push([...refs]);
return Promise.resolve({ archived: true, archivedSessionIds: refs.map((ref) => ref.id), failures: [], generatedAt: "2026-06-25T00:00:00.000Z" });
}
deleteArchivedMany(refs: readonly SessionBulkMutationRef[]): Promise<SessionBulkDeleteArchivedResponse> {
this.bulkDeleteCalls.push([...refs]);
return Promise.resolve({ deleted: true, deletedSessionIds: refs.map((ref) => ref.id), failures: [], generatedAt: "2026-06-25T00:00:00.000Z" });
}
reload(lookup: SessionRouteLookup): Promise<void> {
this.reloadCalls.push(lookup);
if (this.reloadError !== undefined) return Promise.reject(this.reloadError);
@@ -210,7 +292,9 @@ class CapturingRouteSessionService implements SessionRouteService {
list(): never { throw unusedRouteMethod("list"); }
start(): never { throw unusedRouteMethod("start"); }
messages(): Promise<unknown[]> { return Promise.resolve([]); }
messages(): Promise<unknown[] | MessagePage> {
return Promise.resolve(this.messagesResponse);
}
status(lookup: SessionRouteLookup) {
this.calls.push(lookup);