mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 13:42:50 +00:00
fix(server): preserve each refresh caller workspace recovery
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
@@ -60,17 +61,19 @@ function heldHistoryProvider() {
|
||||
return { provider, historyStarted, releaseHistory, resumes: () => resumes };
|
||||
}
|
||||
|
||||
async function setup(provider: AgentClient, agentId?: string) {
|
||||
async function setup(provider: AgentClient, agentId?: string, appVersions = ["0.14.0", "0.14.0"]) {
|
||||
const cwd = mkdtempSync(path.join(artifactRoot, "concurrent-refresh-"));
|
||||
const daemon = await createTestPaseoDaemon({ agentClients: { claude: provider } });
|
||||
const clients = [0, 1].map(() => new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }));
|
||||
const clients = appVersions.map(
|
||||
(appVersion) => new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion }),
|
||||
);
|
||||
resources.push({ daemon, clients, cwd });
|
||||
await Promise.all(clients.map((client) => client.connect()));
|
||||
await Promise.all(clients.map((client) => client.fetchAgents({ subscribe: {} })));
|
||||
const agent = await daemon.daemon.agentManager.createAgent({ provider: "claude", cwd }, agentId, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
return { clients, agent };
|
||||
return { clients, agent, cwd };
|
||||
}
|
||||
|
||||
test("refreshes from separate connections share the reload until history is ready", async () => {
|
||||
@@ -149,3 +152,56 @@ test("concurrent refreshes share a failure and a later retry starts a new reload
|
||||
await expect(clients[0].refreshAgent(agent.id)).resolves.toMatchObject({ agentId: agent.id });
|
||||
expect(resumes).toBe(2);
|
||||
});
|
||||
|
||||
test("a legacy caller restores its workspace when joining a modern refresh", async () => {
|
||||
const provider = heldHistoryProvider();
|
||||
const {
|
||||
clients: [modern, legacy],
|
||||
cwd,
|
||||
} = await setup(provider.provider, undefined, ["0.1.105", "0.1.104"]);
|
||||
// An independent repository prevents archive from owning the enclosing test checkout.
|
||||
execFileSync("git", ["init", "-b", "main", cwd], { stdio: "pipe" });
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"-c",
|
||||
"user.name=Paseo Test",
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"fixture",
|
||||
],
|
||||
{ cwd, stdio: "pipe" },
|
||||
);
|
||||
const created = await modern.createWorkspace({ source: { kind: "directory", path: cwd } });
|
||||
expect(created.error).toBeNull();
|
||||
const workspace = created.workspace!;
|
||||
const agent = await modern.createAgent({ provider: "claude", cwd, workspaceId: workspace.id });
|
||||
await modern.archiveWorkspace(workspace.id);
|
||||
expect((await modern.fetchWorkspaces()).entries.some((entry) => entry.id === workspace.id)).toBe(
|
||||
false,
|
||||
);
|
||||
const first = modern.refreshAgent(agent.id);
|
||||
await provider.historyStarted.promise;
|
||||
expect((await modern.fetchWorkspaces()).entries.some((entry) => entry.id === workspace.id)).toBe(
|
||||
false,
|
||||
);
|
||||
const joining = legacy.refreshAgent(agent.id);
|
||||
const outcomes = Promise.allSettled([first, joining]);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await legacy.fetchWorkspaces()).entries.some((entry) => entry.id === workspace.id),
|
||||
{ timeout: 5000 },
|
||||
)
|
||||
.toBe(true);
|
||||
} finally {
|
||||
provider.releaseHistory.resolve();
|
||||
}
|
||||
expect((await outcomes).map((result) => result.status)).toEqual(["fulfilled", "fulfilled"]);
|
||||
});
|
||||
|
||||
@@ -4761,6 +4761,7 @@ export class Session {
|
||||
this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
|
||||
|
||||
try {
|
||||
await this.restoreOwningWorkspaceForLegacyAgentRefresh(agentId);
|
||||
// Runtime replacement is serialized, but the reply also needs provider history.
|
||||
// Another replacement during hydration can remove the agent before that reply.
|
||||
// Share the whole reload and hydration across connections, including failures.
|
||||
@@ -4819,7 +4820,6 @@ export class Session {
|
||||
}
|
||||
|
||||
private async refreshAgentFromPersistence(agentId: string): Promise<ManagedAgent> {
|
||||
await this.restoreOwningWorkspaceForLegacyAgentRefresh(agentId);
|
||||
await unarchiveAgentState(this.agentStorage, this.agentManager, agentId);
|
||||
let snapshot: ManagedAgent;
|
||||
const existing = this.agentManager.getAgent(agentId);
|
||||
|
||||
Reference in New Issue
Block a user