mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 02:15:02 +00:00
perf(workspaces): stop duplicating setup output in progress snapshots
Keep the required combined log field empty for setup clients, which display per-command output. Preserve concatenation only for legacy agent bootstrap timeline cards whose released renderers still read it directly.
This commit is contained in:
@@ -604,12 +604,18 @@ async function expectMissingLocalMergeTarget({ client, cwd }: RestorableWorkspac
|
||||
).toContain("No local merge target is recorded");
|
||||
}
|
||||
|
||||
function setupCommandOutput(detail: { commands: Array<{ log: string }> }): string {
|
||||
return detail.commands.map((command) => command.log).join("\n");
|
||||
}
|
||||
|
||||
// Setup output is opt-in even for multiple sockets sharing one logical client.
|
||||
test("setup details follow one workspace/source and restore after reconnect with minimal directory status", async () => {
|
||||
const daemon = await createTestPaseoDaemon({ mcpEnabled: true });
|
||||
const { repoDir, tempRoot } = createGitRepoWithBranch();
|
||||
const clients: SocketDaemonClient[] = [];
|
||||
const sockets = new Map<string, WebSocket[]>();
|
||||
const setupWireTypes = new Set<string>();
|
||||
const setupWireDetails: Array<{ log: string; commands: Array<{ log: string }> }> = [];
|
||||
const observations: Array<{ release(): Promise<void> }> = [];
|
||||
const workspaces: Array<{ id: string; workspaceDirectory?: string }> = [];
|
||||
const mcp = new Client({ name: "setup-test", version: "1.0.0" });
|
||||
@@ -653,6 +659,24 @@ const timer = setInterval(() => {
|
||||
reconnect: { baseDelayMs: 10, maxDelayMs: 10 },
|
||||
webSocketFactory: (url) => {
|
||||
const socket = new WebSocket(url);
|
||||
socket.on("message", (data, isBinary) => {
|
||||
if (isBinary) return;
|
||||
const frame = JSON.parse(data.toString());
|
||||
const message = frame.message ?? frame;
|
||||
if (message.type === "workspace_setup_progress") {
|
||||
setupWireTypes.add(message.type);
|
||||
setupWireDetails.push(message.payload.detail);
|
||||
}
|
||||
if (
|
||||
message.type === "workspace_setup_status_response" ||
|
||||
message.type === "workspace.setup.subscribe.response"
|
||||
) {
|
||||
if (message.payload.snapshot) {
|
||||
setupWireTypes.add(message.type);
|
||||
setupWireDetails.push(message.payload.snapshot.detail);
|
||||
}
|
||||
}
|
||||
});
|
||||
sockets.set(clientId, [...(sockets.get(clientId) ?? []), socket]);
|
||||
return socket as unknown as WebSocketLike;
|
||||
},
|
||||
@@ -683,7 +707,10 @@ const timer = setInterval(() => {
|
||||
})
|
||||
.toMatchObject({
|
||||
status: "running",
|
||||
detail: { log: expect.stringContaining("setup started") },
|
||||
detail: {
|
||||
log: "",
|
||||
commands: [expect.objectContaining({ log: expect.stringContaining("setup started") })],
|
||||
},
|
||||
});
|
||||
};
|
||||
for (const worktreeSlug of ["noisy-a", "noisy-b", "noisy-c"]) {
|
||||
@@ -712,7 +739,10 @@ const timer = setInterval(() => {
|
||||
const initial = await details.ready;
|
||||
expect(initial.snapshot).toMatchObject({
|
||||
status: "running",
|
||||
detail: { log: expect.stringContaining("setup started") },
|
||||
detail: {
|
||||
log: "",
|
||||
commands: [expect.objectContaining({ log: expect.stringContaining("setup started") })],
|
||||
},
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
@@ -745,7 +775,7 @@ const timer = setInterval(() => {
|
||||
viewerMessages.filter(
|
||||
(message) =>
|
||||
message.type === "workspace_setup_progress" &&
|
||||
message.payload.detail.log.includes("wave-0:"),
|
||||
setupCommandOutput(message.payload.detail).includes("wave-0:"),
|
||||
).length,
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
@@ -776,10 +806,12 @@ const timer = setInterval(() => {
|
||||
.filter(
|
||||
(message) =>
|
||||
message.type === "workspace_setup_progress" &&
|
||||
message.payload.detail.log.includes("wave-0:"),
|
||||
setupCommandOutput(message.payload.detail).includes("wave-0:"),
|
||||
)
|
||||
.map((message) =>
|
||||
message.type === "workspace_setup_progress" ? message.payload.detail.log : "",
|
||||
message.type === "workspace_setup_progress"
|
||||
? setupCommandOutput(message.payload.detail)
|
||||
: "",
|
||||
);
|
||||
expect(waveLogs.length).toBe(new Set(waveLogs).size);
|
||||
const restoredSnapshots: unknown[] = [];
|
||||
@@ -792,7 +824,12 @@ const timer = setInterval(() => {
|
||||
await expect.poll(() => restoredSnapshots.length, { timeout: 10000 }).toBe(2);
|
||||
expect(details.subscriptionId).not.toBe(beforeReconnect);
|
||||
expect(restoredSnapshots[1]).toMatchObject({
|
||||
snapshot: { detail: { log: expect.stringContaining("wave-0:") } },
|
||||
snapshot: {
|
||||
detail: {
|
||||
log: "",
|
||||
commands: [expect.objectContaining({ log: expect.stringContaining("wave-0:") })],
|
||||
},
|
||||
},
|
||||
});
|
||||
const subscriptionId = details.subscriptionId;
|
||||
await details.release();
|
||||
@@ -804,7 +841,8 @@ const timer = setInterval(() => {
|
||||
// Wait until the daemon has consumed the output before checking wire silence.
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await viewer.fetchWorkspaceSetupStatus(workspace.id)).snapshot?.detail.log,
|
||||
async () =>
|
||||
(await viewer.fetchWorkspaceSetupStatus(workspace.id)).snapshot?.detail.commands[0]?.log,
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
@@ -815,8 +853,22 @@ const timer = setInterval(() => {
|
||||
);
|
||||
const resumed = viewer.observeWorkspaceSetup(workspace.id);
|
||||
observations.push(resumed);
|
||||
expect((await resumed.ready).snapshot?.detail.log).toContain("wave-1:");
|
||||
expect((await resumed.ready).snapshot?.detail.commands[0]?.log).toContain("wave-1:");
|
||||
expect(resumed.subscriptionId).not.toBe(subscriptionId);
|
||||
expect(setupWireTypes).toEqual(
|
||||
new Set([
|
||||
"workspace_setup_progress",
|
||||
"workspace_setup_status_response",
|
||||
"workspace.setup.subscribe.response",
|
||||
]),
|
||||
);
|
||||
expect(setupWireDetails.length).toBeGreaterThan(0);
|
||||
expect(setupWireDetails.every((detail) => detail.log === "")).toBe(true);
|
||||
expect(
|
||||
setupWireDetails.some((detail) =>
|
||||
detail.commands.some((command) => command.log.includes("wave-1:")),
|
||||
),
|
||||
).toBe(true);
|
||||
writeFileSync(path.join(workspaces[1]!.workspaceDirectory!, "finish.txt"), "fail");
|
||||
await expect
|
||||
.poll(
|
||||
@@ -849,3 +901,58 @@ const timer = setInterval(() => {
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
test("legacy agent git creation retains setup output in its readable chat timeline", async () => {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
const { repoDir, tempRoot } = createGitRepoWithBranch();
|
||||
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
|
||||
try {
|
||||
writeFileSync(
|
||||
path.join(repoDir, "paseo.json"),
|
||||
JSON.stringify({ worktree: { setup: ["echo legacy-setup-output"] } }),
|
||||
);
|
||||
execFileSync("git", ["add", "paseo.json"], { cwd: repoDir });
|
||||
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "setup fixture"], {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
await client.connect();
|
||||
const agent = await client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: repoDir,
|
||||
git: {
|
||||
createWorktree: true,
|
||||
createNewBranch: true,
|
||||
baseBranch: "main",
|
||||
newBranchName: "legacy-setup",
|
||||
worktreeSlug: "legacy-setup",
|
||||
},
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 });
|
||||
return timeline.entries
|
||||
.map((entry) => entry.item)
|
||||
.find(
|
||||
(item) =>
|
||||
item.type === "tool_call" &&
|
||||
item.name === "paseo_worktree_setup" &&
|
||||
item.status === "completed",
|
||||
);
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
.toMatchObject({
|
||||
detail: {
|
||||
type: "worktree_setup",
|
||||
log: expect.stringContaining("legacy-setup-output"),
|
||||
commands: [expect.objectContaining({ log: "legacy-setup-output\n" })],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
|
||||
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
||||
import { runAsyncWorktreeBootstrap, spawnWorkspaceScript } from "./worktree-bootstrap.js";
|
||||
import {
|
||||
buildWorktreeSetupDetail,
|
||||
runAsyncWorktreeBootstrap,
|
||||
spawnWorkspaceScript,
|
||||
} from "./worktree-bootstrap.js";
|
||||
import { ensureWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
|
||||
import { ScriptRouteStore } from "./script-proxy.js";
|
||||
import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js";
|
||||
@@ -63,6 +67,30 @@ async function createBootstrapWorktreeForTest(
|
||||
return { worktree, shouldBootstrap: true };
|
||||
}
|
||||
|
||||
describe("workspace setup detail", () => {
|
||||
it("serializes output only in command logs while retaining the legacy string field", () => {
|
||||
const detail = buildWorktreeSetupDetail({
|
||||
worktree: { worktreePath: "/workspace", branchName: "feature" },
|
||||
results: [
|
||||
{
|
||||
command: "echo ready",
|
||||
cwd: "/workspace",
|
||||
stdout: "ready\n",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
durationMs: 10,
|
||||
},
|
||||
],
|
||||
});
|
||||
const serialized = JSON.stringify(detail);
|
||||
expect(JSON.parse(serialized)).toMatchObject({
|
||||
log: "",
|
||||
commands: [{ log: "ready\n", status: "completed", exitCode: 0 }],
|
||||
});
|
||||
expect(serialized.split("ready\\n")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runAsyncWorktreeBootstrap", () => {
|
||||
let tempDir: string;
|
||||
let repoDir: string;
|
||||
|
||||
@@ -325,18 +325,15 @@ export function buildWorktreeSetupDetail(input: {
|
||||
...(result.durationMs > 0 ? { durationMs: result.durationMs } : {}),
|
||||
};
|
||||
});
|
||||
const renderedLog = buildWorktreeSetupLog({
|
||||
results: input.results,
|
||||
outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
type: "worktree_setup",
|
||||
worktreePath: input.worktree.worktreePath,
|
||||
branchName: input.worktree.branchName,
|
||||
log: renderedLog.log,
|
||||
// COMPAT(workspaceSetupCombinedLog): added in v0.12.0, remove after 2027-04-20 once legacy wire schemas no longer require log.
|
||||
// Setup clients render commands[].log; retain the required wire field without duplicating output.
|
||||
log: "",
|
||||
commands,
|
||||
...(renderedLog.truncated || anyCommandTruncated ? { truncated: true } : {}),
|
||||
...(anyCommandTruncated ? { truncated: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -354,6 +351,10 @@ function buildSetupTimelineItem(input: {
|
||||
outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
|
||||
});
|
||||
|
||||
// Legacy agent creation still emits timeline cards whose released renderers read only detail.log.
|
||||
// Keep their output here rather than concatenating every workspace setup snapshot.
|
||||
detail.log = buildWorktreeSetupLog(input).log;
|
||||
|
||||
if (input.status === "running") {
|
||||
return {
|
||||
type: "tool_call",
|
||||
|
||||
@@ -983,7 +983,7 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
const setupOutputMessage = runningMessages.find((message) =>
|
||||
message.payload.detail.commands[0]?.log.includes("phase-one"),
|
||||
);
|
||||
expect(setupOutputMessage?.payload.detail.log).toContain("phase-one");
|
||||
expect(setupOutputMessage?.payload.detail.log).toBe("");
|
||||
expect(setupOutputMessage?.payload.detail.commands[0]).toMatchObject({
|
||||
index: 1,
|
||||
command: "sh -c \"printf 'phase-one\\\\n'; sleep 0.1; printf 'phase-two\\\\n'\"",
|
||||
@@ -1001,7 +1001,7 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
branchName: "feature-running-setup",
|
||||
},
|
||||
});
|
||||
expect(progressMessages.at(-1)?.payload.detail.log).toContain("phase-two");
|
||||
expect(progressMessages.at(-1)?.payload.detail.log).toBe("");
|
||||
expect(progressMessages.at(-1)?.payload.detail.commands[0]).toMatchObject({
|
||||
index: 1,
|
||||
command: "sh -c \"printf 'phase-one\\\\n'; sleep 0.1; printf 'phase-two\\\\n'\"",
|
||||
|
||||
Reference in New Issue
Block a user