mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
Fix sandbox command-session and lazy ACP model recovery
This commit is contained in:
1 parent
17ddb89618
commit
cb381c6a5f
8 files changed
+255
-19
No files matched your search
@@ -1,5 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isStagingOrigin, assertDeployedAdapterExclusions, deployedAgentEngine } from "./deployed-stack.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { isStagingOrigin, assertDeployedAdapterExclusions, deployedAgentEngine, findDeployedWorkFile } from "./deployed-stack.js";
|
||||
|
||||
describe("deployed work-file pagination", () => {
|
||||
it.each([false, true])("finds a later-page entry while preserving trash=%s", async (trash) => {
|
||||
const file = { path: "nested/empty.sh", byteSize: 0, executable: true };
|
||||
const json = vi.fn().mockResolvedValueOnce({ files: [{ path: "unrelated" }], nextCursor: "next-page" })
|
||||
.mockResolvedValueOnce({ files: [file], nextCursor: null });
|
||||
expect(await findDeployedWorkFile({ json }, "/api/folder", file.path, trash)).toEqual(file);
|
||||
expect(json.mock.calls.map(([url]) => url)).toEqual([
|
||||
`/api/folder?limit=200&trash=${trash}`,
|
||||
`/api/folder?limit=200&trash=${trash}&cursor=next-page`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns missing only after the final page", async () => {
|
||||
const json = vi.fn().mockResolvedValueOnce({ files: [], nextCursor: "next-page" })
|
||||
.mockResolvedValueOnce({ files: [], nextCursor: null });
|
||||
expect(await findDeployedWorkFile({ json }, "/api/folder", "missing")).toBeUndefined();
|
||||
expect(json).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rejects a repeated cursor instead of looping", async () => {
|
||||
const json = vi.fn().mockResolvedValue({ files: [], nextCursor: "same-page" });
|
||||
await expect(findDeployedWorkFile({ json }, "/api/folder", "missing")).rejects.toThrow("repeated its cursor");
|
||||
expect(json).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deployed stack target", () => {
|
||||
it("requires an explicit HTTPS staging tenant and rejects credential-bearing URLs", () => {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
import fs from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import type { WorkFolderListing } from "../../packages/shared/src/work-folders.js";
|
||||
|
||||
/** Find one entry without assuming a shared folder fits in the first page. */
|
||||
export async function findDeployedWorkFile(
|
||||
api: Pick<DeployedStackApi, "json">,
|
||||
folderPath: string,
|
||||
filePath: string,
|
||||
trash = false,
|
||||
) {
|
||||
const cursors = new Set<string>();
|
||||
let cursor: string | null = null;
|
||||
do {
|
||||
const query = new URLSearchParams({ limit: "200", trash: String(trash) });
|
||||
if (cursor) query.set("cursor", cursor);
|
||||
const listing: WorkFolderListing = await api.json(`${folderPath}?${query}`);
|
||||
const file = listing.files.find((entry) => entry.path === filePath);
|
||||
if (file) return file;
|
||||
cursor = listing.nextCursor;
|
||||
if (cursor) {
|
||||
assert(!cursors.has(cursor), "Work-folder listing repeated its cursor");
|
||||
cursors.add(cursor);
|
||||
}
|
||||
} while (cursor);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// A separate target contract deliberately has no local-server fallback.
|
||||
export function isStagingOrigin(value: string) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { test, expect } from "@playwright/test";
|
||||
import type { EnvironmentCapabilities } from "../../packages/shared/src/environment-support.js";
|
||||
import type { SandboxWorkFolderManifest, WorkFolderListing, WorkFolderSyncStatus } from "../../packages/shared/src/work-folders.js";
|
||||
import type { SandboxWorkFolderManifest, WorkFolderSyncStatus } from "../../packages/shared/src/work-folders.js";
|
||||
import { QUALIFIED_ACPX_RUNNER_MODELS } from "../../server/src/services/native-runtime/provider-profile.js";
|
||||
import { pollUntil } from "./api.js";
|
||||
import { DeployedStackApi, deployedAgentEngine, loadDeployedStack } from "./deployed-stack.js";
|
||||
import { DeployedStackApi, deployedAgentEngine, findDeployedWorkFile, loadDeployedStack } from "./deployed-stack.js";
|
||||
|
||||
import { repoAcceptancePrompt } from "./work-folder-acceptance-prompts.js";
|
||||
|
||||
@@ -60,15 +60,13 @@ for (const [scope, owner] of [["task", stack.taskId], ["agent", stack.agentId],
|
||||
});
|
||||
expect((await write()).ok).toBe(true);
|
||||
expect((await write()).ok).toBe(true);
|
||||
const listing = await api.json<WorkFolderListing>(base);
|
||||
const file = listing.files.find((entry) => entry.path === filename)!;
|
||||
const file = (await findDeployedWorkFile(api, base, filename))!;
|
||||
expect(file).toMatchObject({ byteSize: 0, executable: true, deletedAt: null });
|
||||
const download = await api.request(`${base}/content?path=${encodeURIComponent(filename)}`);
|
||||
expect(download.status).toBe(200); expect((await download.arrayBuffer()).byteLength).toBe(0);
|
||||
await api.json(`${base}/operations`, "POST", { action: "delete", path: filename });
|
||||
expect((await api.request(`${base}/content?path=${encodeURIComponent(filename)}`)).status).toBe(404);
|
||||
const trash = await api.json<WorkFolderListing>(`${base}?trash=true`);
|
||||
expect(trash.files.some((entry) => entry.id === file.id)).toBe(true);
|
||||
expect((await findDeployedWorkFile(api, base, filename, true))?.id).toBe(file.id);
|
||||
await api.json(`${base}/operations`, "POST", { action: "restore", fileId: file.id });
|
||||
expect((await api.request(`${base}/content?path=${encodeURIComponent(filename)}`)).status).toBe(200);
|
||||
});
|
||||
@@ -109,8 +107,7 @@ for (const profile of stack.profiles) {
|
||||
const read = await api.request(`${scoped}/content?path=${encodeURIComponent(`roundtrip-${nonce}/message.txt`)}`);
|
||||
expect(read.status, `${profile.id} ${scope} durable bytes`).toBe(200);
|
||||
expect(await read.text()).toBe(nonce);
|
||||
const listing = await api.json<WorkFolderListing>(scoped);
|
||||
expect(listing.files.find(file => file.path === `roundtrip-${nonce}/empty.sh`)).toMatchObject({byteSize:0, executable:true});
|
||||
expect(await findDeployedWorkFile(api, scoped, `roundtrip-${nonce}/empty.sh`)).toMatchObject({byteSize:0, executable:true});
|
||||
}
|
||||
|
||||
await api.json(`/api/issues/${issue.id}`, "PATCH", { status: "todo", description: repoAcceptancePrompt(nonce, true) });
|
||||
|
||||
Reference in new issue
Block a user