Archived
fix: preserve legacy session route lookups
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { PI_WEB_CAPABILITIES } from "../../../shared/capabilities";
|
||||
import type { TerminalCommandRun, Workspace } from "../../../shared/apiTypes";
|
||||
import { machinesApi, piWebApi, terminalsApi, workspacesApi } from "./clients";
|
||||
import { machinesApi, piWebApi, sessionsApi, terminalsApi, workspacesApi } from "./clients";
|
||||
|
||||
const workspace: Workspace = {
|
||||
id: "w/1",
|
||||
@@ -60,6 +60,30 @@ describe("machine-scoped runtime API", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session API compatibility", () => {
|
||||
it("keeps legacy session-id calls free of cwd context", async () => {
|
||||
const fetchMock = stubJsonFetch({ accepted: true });
|
||||
|
||||
await sessionsApi.prompt("s 1", "hello", "followUp", "remote a");
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(JSON.parse(requestBody(init))).toEqual({ text: "hello", streamingBehavior: "followUp" });
|
||||
});
|
||||
|
||||
it("adds cwd context when session refs include a workspace", async () => {
|
||||
const fetchMock = stubJsonFetch({ accepted: true });
|
||||
|
||||
await sessionsApi.prompt({ id: "s 1", cwd: "/repo" }, "hello", undefined, "remote a");
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(JSON.parse(requestBody(init))).toEqual({ cwd: "/repo", text: "hello" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("machine-scoped terminal command-run API", () => {
|
||||
it("deletes workspaces through the selected machine scope", async () => {
|
||||
const fetchMock = stubJsonFetch(commandRun);
|
||||
|
||||
@@ -42,17 +42,40 @@ import { machineGitDiffUrl, messageUrl } from "./urls";
|
||||
|
||||
const machinePrefix = (machineId = "local") => `/api/machines/${encodeURIComponent(machineId)}`;
|
||||
|
||||
function sessionBaseUrl(session: SessionRef, machineId = "local"): string {
|
||||
return `${machinePrefix(machineId)}/sessions/${encodeURIComponent(session.id)}`;
|
||||
type SessionLookup = SessionRef | string;
|
||||
|
||||
function sessionId(session: SessionLookup): string {
|
||||
return typeof session === "string" ? session : session.id;
|
||||
}
|
||||
|
||||
function sessionUrl(session: SessionRef, endpoint: string, machineId = "local"): string {
|
||||
function sessionCwd(session: SessionLookup): string | undefined {
|
||||
return typeof session === "string" ? undefined : session.cwd;
|
||||
}
|
||||
|
||||
function sessionBaseUrl(session: SessionLookup, machineId = "local"): string {
|
||||
return `${machinePrefix(machineId)}/sessions/${encodeURIComponent(sessionId(session))}`;
|
||||
}
|
||||
|
||||
function sessionUrl(session: SessionLookup, endpoint: string, machineId = "local"): string {
|
||||
return `${sessionBaseUrl(session, machineId)}/${endpoint}`;
|
||||
}
|
||||
|
||||
function sessionQueryUrl(session: SessionRef, endpoint: string, machineId = "local"): string {
|
||||
const query = new URLSearchParams({ cwd: session.cwd }).toString();
|
||||
return `${sessionUrl(session, endpoint, machineId)}?${query}`;
|
||||
function sessionQueryUrl(session: SessionLookup, endpoint: string, machineId = "local"): string {
|
||||
return `${sessionUrl(session, endpoint, machineId)}${sessionQuery(session)}`;
|
||||
}
|
||||
|
||||
function sessionBaseQueryUrl(session: SessionLookup, machineId = "local"): string {
|
||||
return `${sessionBaseUrl(session, machineId)}${sessionQuery(session)}`;
|
||||
}
|
||||
|
||||
function sessionQuery(session: SessionLookup): string {
|
||||
const cwd = sessionCwd(session);
|
||||
return cwd === undefined || cwd === "" ? "" : `?${new URLSearchParams({ cwd }).toString()}`;
|
||||
}
|
||||
|
||||
function sessionBody(session: SessionLookup, fields: Record<string, unknown> = {}): string {
|
||||
const cwd = sessionCwd(session);
|
||||
return JSON.stringify(cwd === undefined || cwd === "" ? fields : { cwd, ...fields });
|
||||
}
|
||||
|
||||
export const piWebApi = {
|
||||
@@ -98,26 +121,26 @@ export const workspacesApi = {
|
||||
export const sessionsApi = {
|
||||
sessions: (cwd: string, machineId = "local") => request(`${machinePrefix(machineId)}/sessions?cwd=${encodeURIComponent(cwd)}`, arrayOf(parseSessionInfo)),
|
||||
startSession: (cwd: string, machineId = "local") => request(`${machinePrefix(machineId)}/sessions`, parseSessionInfo, { method: "POST", body: JSON.stringify({ cwd }) }),
|
||||
messages: (session: SessionRef, options?: { limit?: number; before?: number }, machineId = "local") => request(messageUrl(session, options, machineId), parseMessagePage),
|
||||
status: (session: SessionRef, machineId = "local") => request(sessionQueryUrl(session, "status", machineId), parseSessionStatus),
|
||||
models: (session: SessionRef, machineId = "local") => request(sessionQueryUrl(session, "models", machineId), parseModelSelectionResponse),
|
||||
setModel: (session: SessionRef, provider: string, modelId: string, machineId = "local") => request(sessionUrl(session, "model", machineId), parseSessionStatus, { method: "POST", body: JSON.stringify({ cwd: session.cwd, provider, modelId }) }),
|
||||
cycleModel: (session: SessionRef, direction: "forward" | "backward", machineId = "local") => request(sessionUrl(session, "model/cycle", machineId), parseSessionStatus, { method: "POST", body: JSON.stringify({ cwd: session.cwd, direction }) }),
|
||||
thinkingLevels: (session: SessionRef, machineId = "local") => request(sessionQueryUrl(session, "thinking-levels", machineId), parseThinkingLevelsResponse),
|
||||
setThinkingLevel: (session: SessionRef, level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", machineId = "local") => request(sessionUrl(session, "thinking-level", machineId), parseSessionStatus, { method: "POST", body: JSON.stringify({ cwd: session.cwd, level }) }),
|
||||
cycleThinkingLevel: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "thinking-level/cycle", machineId), parseSessionStatus, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
commands: (session: SessionRef, machineId = "local") => request(sessionQueryUrl(session, "commands", machineId), arrayOf(parseSlashCommand)),
|
||||
prompt: (session: SessionRef, text: string, streamingBehavior?: "steer" | "followUp", machineId = "local") => request(sessionUrl(session, "prompt", machineId), parseAccepted, { method: "POST", body: JSON.stringify(streamingBehavior === undefined ? { cwd: session.cwd, text } : { cwd: session.cwd, text, streamingBehavior }) }),
|
||||
shell: (session: SessionRef, text: string, machineId = "local") => request(sessionUrl(session, "shell", machineId), parseAccepted, { method: "POST", body: JSON.stringify({ cwd: session.cwd, text }) }),
|
||||
runCommand: (session: SessionRef, text: string, machineId = "local") => request(sessionUrl(session, "commands/run", machineId), parseCommandResult, { method: "POST", body: JSON.stringify({ cwd: session.cwd, text }) }),
|
||||
respondToCommand: (session: SessionRef, requestId: string, value: string, machineId = "local") => request(sessionUrl(session, "commands/respond", machineId), parseCommandResult, { method: "POST", body: JSON.stringify({ cwd: session.cwd, requestId, value }) }),
|
||||
abort: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "abort", machineId), parseAborted, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
stop: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "stop", machineId), parseStopped, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
archive: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "archive", machineId), parseArchived, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
archiveWithDescendants: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "archive-tree", machineId), parseArchived, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
restore: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "restore", machineId), parseRestored, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
deleteArchived: (session: SessionRef, machineId = "local") => request(`${sessionBaseUrl(session, machineId)}?${new URLSearchParams({ cwd: session.cwd }).toString()}`, parseDeleted, { method: "DELETE" }),
|
||||
detachParent: (session: SessionRef, machineId = "local") => request(sessionUrl(session, "detach-parent", machineId), parseDetached, { method: "POST", body: JSON.stringify({ cwd: session.cwd }) }),
|
||||
messages: (session: SessionLookup, options?: { limit?: number; before?: number }, machineId = "local") => request(messageUrl(session, options, machineId), parseMessagePage),
|
||||
status: (session: SessionLookup, machineId = "local") => request(sessionQueryUrl(session, "status", machineId), parseSessionStatus),
|
||||
models: (session: SessionLookup, machineId = "local") => request(sessionQueryUrl(session, "models", machineId), parseModelSelectionResponse),
|
||||
setModel: (session: SessionLookup, provider: string, modelId: string, machineId = "local") => request(sessionUrl(session, "model", machineId), parseSessionStatus, { method: "POST", body: sessionBody(session, { provider, modelId }) }),
|
||||
cycleModel: (session: SessionLookup, direction: "forward" | "backward", machineId = "local") => request(sessionUrl(session, "model/cycle", machineId), parseSessionStatus, { method: "POST", body: sessionBody(session, { direction }) }),
|
||||
thinkingLevels: (session: SessionLookup, machineId = "local") => request(sessionQueryUrl(session, "thinking-levels", machineId), parseThinkingLevelsResponse),
|
||||
setThinkingLevel: (session: SessionLookup, level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", machineId = "local") => request(sessionUrl(session, "thinking-level", machineId), parseSessionStatus, { method: "POST", body: sessionBody(session, { level }) }),
|
||||
cycleThinkingLevel: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "thinking-level/cycle", machineId), parseSessionStatus, { method: "POST", body: sessionBody(session) }),
|
||||
commands: (session: SessionLookup, machineId = "local") => request(sessionQueryUrl(session, "commands", machineId), arrayOf(parseSlashCommand)),
|
||||
prompt: (session: SessionLookup, text: string, streamingBehavior?: "steer" | "followUp", machineId = "local") => request(sessionUrl(session, "prompt", machineId), parseAccepted, { method: "POST", body: sessionBody(session, streamingBehavior === undefined ? { text } : { text, streamingBehavior }) }),
|
||||
shell: (session: SessionLookup, text: string, machineId = "local") => request(sessionUrl(session, "shell", machineId), parseAccepted, { method: "POST", body: sessionBody(session, { text }) }),
|
||||
runCommand: (session: SessionLookup, text: string, machineId = "local") => request(sessionUrl(session, "commands/run", machineId), parseCommandResult, { method: "POST", body: sessionBody(session, { text }) }),
|
||||
respondToCommand: (session: SessionLookup, requestId: string, value: string, machineId = "local") => request(sessionUrl(session, "commands/respond", machineId), parseCommandResult, { method: "POST", body: sessionBody(session, { requestId, value }) }),
|
||||
abort: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "abort", machineId), parseAborted, { method: "POST", body: sessionBody(session) }),
|
||||
stop: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "stop", machineId), parseStopped, { method: "POST", body: sessionBody(session) }),
|
||||
archive: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "archive", machineId), parseArchived, { method: "POST", body: sessionBody(session) }),
|
||||
archiveWithDescendants: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "archive-tree", machineId), parseArchived, { method: "POST", body: sessionBody(session) }),
|
||||
restore: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "restore", machineId), parseRestored, { method: "POST", body: sessionBody(session) }),
|
||||
deleteArchived: (session: SessionLookup, machineId = "local") => request(sessionBaseQueryUrl(session, machineId), parseDeleted, { method: "DELETE" }),
|
||||
detachParent: (session: SessionLookup, machineId = "local") => request(sessionUrl(session, "detach-parent", machineId), parseDetached, { method: "POST", body: sessionBody(session) }),
|
||||
authProviders: (options?: { mode?: "login" | "logout"; authType?: "oauth" | "api_key"; machineId?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.mode !== undefined) params.set("mode", options.mode);
|
||||
|
||||
@@ -30,6 +30,14 @@ describe("machine-scoped socket urls", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps legacy session socket urls usable without cwd", () => {
|
||||
sessionEvents("s1");
|
||||
|
||||
expect(webSocketUrls).toEqual([
|
||||
"wss://pi.example.test/api/machines/local/sessions/s1/events",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the requested machine scope for terminal sockets", () => {
|
||||
terminalSocket("p 1", "w/1", "t?1", { cols: 120, rows: 40 }, "remote-a");
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { SessionRef } from "../../../shared/apiTypes";
|
||||
|
||||
export function sessionEvents(session: SessionRef, machineId = "local"): WebSocket {
|
||||
const query = new URLSearchParams({ cwd: session.cwd }).toString();
|
||||
return new WebSocket(`${webSocketBaseUrl()}${machinePrefix(machineId)}/sessions/${encodeURIComponent(session.id)}/events?${query}`);
|
||||
type SessionLookup = SessionRef | string;
|
||||
|
||||
export function sessionEvents(session: SessionLookup, machineId = "local"): WebSocket {
|
||||
const cwd = typeof session === "string" ? undefined : session.cwd;
|
||||
const query = cwd === undefined || cwd === "" ? "" : `?${new URLSearchParams({ cwd }).toString()}`;
|
||||
const sessionId = typeof session === "string" ? session : session.id;
|
||||
return new WebSocket(`${webSocketBaseUrl()}${machinePrefix(machineId)}/sessions/${encodeURIComponent(sessionId)}/events${query}`);
|
||||
}
|
||||
|
||||
export function globalSessionEvents(machineId = "local"): WebSocket {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { SessionRef } from "../../../shared/apiTypes";
|
||||
|
||||
type SessionLookup = SessionRef | string;
|
||||
|
||||
function sessionId(session: SessionLookup): string {
|
||||
return typeof session === "string" ? session : session.id;
|
||||
}
|
||||
|
||||
function sessionCwd(session: SessionLookup): string | undefined {
|
||||
return typeof session === "string" ? undefined : session.cwd;
|
||||
}
|
||||
|
||||
export function gitDiffUrl(projectId: string, workspaceId: string, options?: { path?: string; staged?: boolean }): string {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.path !== undefined) params.set("path", options.path);
|
||||
@@ -16,11 +26,14 @@ export function machineGitDiffUrl(machineId: string, projectId: string, workspac
|
||||
return `/api/machines/${encodeURIComponent(machineId)}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/git/diff${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
export function messageUrl(session: SessionRef, options?: { limit?: number; before?: number }, machineId = "local"): string {
|
||||
const params = new URLSearchParams({ cwd: session.cwd });
|
||||
export function messageUrl(session: SessionLookup, options?: { limit?: number; before?: number }, machineId = "local"): string {
|
||||
const params = new URLSearchParams();
|
||||
const cwd = sessionCwd(session);
|
||||
if (cwd !== undefined && cwd !== "") params.set("cwd", cwd);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.before !== undefined) params.set("before", String(options.before));
|
||||
return `/api/machines/${encodeURIComponent(machineId)}/sessions/${encodeURIComponent(session.id)}/messages?${params.toString()}`;
|
||||
const query = params.toString();
|
||||
return `/api/machines/${encodeURIComponent(machineId)}/sessions/${encodeURIComponent(sessionId(session))}/messages${query === "" ? "" : `?${query}`}`;
|
||||
}
|
||||
|
||||
export function workspaceImagePreviewUrl(projectId: string, workspaceId: string, path: string, options?: { modifiedAt?: string; machineId?: string }): string {
|
||||
|
||||
@@ -181,10 +181,10 @@ describe("SessionController", () => {
|
||||
...defaultApi,
|
||||
startSession: () => Promise.resolve(replacementSession),
|
||||
messages: (session) => {
|
||||
if (session.id === oldSession.id) return Promise.reject(new Error("Session not found"));
|
||||
if (sessionLookupId(session) === oldSession.id) return Promise.reject(new Error("Session not found"));
|
||||
return Promise.resolve(emptyPage);
|
||||
},
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -221,7 +221,7 @@ describe("SessionController", () => {
|
||||
...defaultApi,
|
||||
respondToCommand: () => Promise.resolve({ type: "done", message: "Session forked", session: replacementSession, promptDraft: "fork me" }),
|
||||
messages: () => Promise.resolve(emptyPage),
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -244,7 +244,7 @@ describe("SessionController", () => {
|
||||
...defaultApi,
|
||||
archive: () => Promise.resolve({ archived: true }),
|
||||
messages: () => Promise.resolve(emptyPage),
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -273,7 +273,7 @@ describe("SessionController", () => {
|
||||
...defaultApi,
|
||||
archiveWithDescendants: () => Promise.resolve({ archived: true, sessionIds: [oldSession.id, childSession.id], archivedCount: 2, skippedAlreadyArchivedCount: 0 }),
|
||||
messages: () => Promise.resolve(emptyPage),
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -299,11 +299,11 @@ describe("SessionController", () => {
|
||||
const api: typeof defaultApi = {
|
||||
...defaultApi,
|
||||
archive: (session) => {
|
||||
archivedIds.push(session.id);
|
||||
archivedIds.push(sessionLookupId(session));
|
||||
return Promise.resolve({ archived: true });
|
||||
},
|
||||
messages: () => Promise.resolve(emptyPage),
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -336,11 +336,11 @@ describe("SessionController", () => {
|
||||
const api: typeof defaultApi = {
|
||||
...defaultApi,
|
||||
deleteArchived: (session) => {
|
||||
deletedIds.push(session.id);
|
||||
deletedIds.push(sessionLookupId(session));
|
||||
return Promise.resolve({ deleted: true });
|
||||
},
|
||||
messages: () => Promise.resolve(emptyPage),
|
||||
status: (session) => Promise.resolve(status(session.id)),
|
||||
status: (session) => Promise.resolve(status(sessionLookupId(session))),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
@@ -364,7 +364,7 @@ describe("SessionController", () => {
|
||||
const api: typeof defaultApi = {
|
||||
...defaultApi,
|
||||
deleteArchived: (session) => {
|
||||
deletedIds.push(session.id);
|
||||
deletedIds.push(sessionLookupId(session));
|
||||
return Promise.resolve({ deleted: true });
|
||||
},
|
||||
};
|
||||
@@ -413,3 +413,7 @@ describe("SessionController", () => {
|
||||
function sessionKey(sessionId: string): string {
|
||||
return machineSessionKey("local", sessionId);
|
||||
}
|
||||
|
||||
function sessionLookupId(session: string | SessionRef): string {
|
||||
return typeof session === "string" ? session : session.id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user