Fix sandbox command-session and lazy ACP model recovery

This commit is contained in:
Dotta committed 2026-09-09 04:44:59 -05:00
1 parent 17ddb89618
commit cb381c6a5f
8 files changed
+255 -19

No files matched your search

+28 -2
View File
@@ -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", () => {
+25
View File
@@ -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) });