Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cloudflare/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ export class CloudArtifactService {
}
}
const { name: _name, ...input } = args;
const payload = { schedule: await backend.schedule(input as ArtifactScheduleInput) };
const active = await backend.activeRevision();
const payload = { schedule: await backend.schedule(input as ArtifactScheduleInput), has_server: active?.has_server ?? snapshot.server_source !== null };
return { ...text(payload), structuredContent: payload };
}
case "artifact_request": {
Expand Down
5 changes: 4 additions & 1 deletion cloudflare/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ test("artifact schedules retain validated revisions, keep host history isolated,
};
const saved = await tool("artifact_schedule", { action: "set", interval_seconds: 3600, request: { path: "/counter", method: "POST" } });
expect(saved.schedule).toMatchObject({ paused: false, interval_seconds: 3600 });
expect(saved.has_server).toBe(true);
await tool("artifact_schedule", { action: "pause" });
const invalid = await client.callTool({ name: "artifact_write", arguments: { name, contents: "export default function {" } });
expect(invalid.isError).toBe(true);
Expand All @@ -776,7 +777,9 @@ export class ArtifactServer extends DurableObject { fetch() { return Response.js
const inspected = await tool("artifact_request", { request: { path: "/" } });
expect(JSON.parse(atob(inspected.response.body))).toEqual([]);
expect((await client.callTool({ name: "artifact_write", arguments: { name, contents: counterClient, server: null } })).isError).not.toBe(true);
expect((await tool("artifact_schedule")).schedule).toMatchObject({ paused: true, next_run_at: null });
const unavailable = await tool("artifact_schedule");
expect(unavailable.schedule).toMatchObject({ paused: true, next_run_at: null });
expect(unavailable.has_server).toBe(false);
expect((await client.callTool({ name: "artifact_schedule", arguments: { name, action: "resume" } })).isError).toBe(true);
}, 120000);

Expand Down
15 changes: 12 additions & 3 deletions e2e/execution-controls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { chromium } from "playwright";
test("schedule loads before editing, preserves request headers and ignores unfinished fields for controls", async () => {
const directory = await mkdtemp(join(tmpdir(), "execution-controls-"));
const entry = join(directory, "fixture.tsx");
await Bun.write(entry, `import {createElement} from ${JSON.stringify(Bun.resolveSync("react", import.meta.dir))};import {createRoot} from ${JSON.stringify(Bun.resolveSync("react-dom/client", import.meta.dir))};import {ExecutionControls} from ${JSON.stringify(new URL("../src/gallery/execution-controls.tsx", import.meta.url).pathname)};createRoot(document.getElementById("root")).render(createElement(ExecutionControls,{workspace:"default",name:"example"}));`);
await Bun.write(entry, `import {createElement} from ${JSON.stringify(Bun.resolveSync("react", import.meta.dir))};import {createRoot} from ${JSON.stringify(Bun.resolveSync("react-dom/client", import.meta.dir))};import {ExecutionControls} from ${JSON.stringify(new URL("../src/gallery/execution-controls.tsx", import.meta.url).pathname)};createRoot(document.getElementById("root")).render(createElement(ExecutionControls,{workspace:"default",name:"example",kind:new URLSearchParams(location.search).has("unsupported")?"artifact":"script"}));`);
let build;
try { build = await Bun.build({ entrypoints: [entry], target: "browser" }); }
finally { await rm(directory, { recursive: true, force: true }); }
Expand All @@ -16,12 +16,16 @@ test("schedule loads before editing, preserves request headers and ignores unfin
type Arguments = { action: string; interval_seconds?: number; request?: { path: string; method: string; headers: [string, string][]; body?: string } };
const calls: Arguments[] = [];
let schedule = { interval_seconds: 7200, paused: false, next_run_at: Date.now() + 7200000, request: { path: "/existing", method: "POST", headers: [["x-example", "first"], ["x-example", "second"]] as [string, string][], body: Buffer.from("prior body").toString("base64") } };
let unsupported = false;
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) {
if (new URL(request.url).pathname === "/client.js") return new Response(js, { headers: { "content-type": "text/javascript" } });
if (new URL(request.url).pathname === "/api/tools") {
const call = await request.json() as { arguments: Arguments }; calls.push(call.arguments);
const call = await request.json() as { name: string; arguments: Arguments };
if (call.name.endsWith("_runs")) return Response.json({ structuredContent: { runs: [] } });
if (unsupported) return Response.json({ structuredContent: { schedule: null, has_server: false } });
calls.push(call.arguments);
if (call.arguments.action === "get") await gate;
if (call.arguments.action === "pause") schedule = { ...schedule, paused: true };
if (call.arguments.action === "resume") schedule = { ...schedule, paused: false };
Expand All @@ -33,7 +37,6 @@ test("schedule loads before editing, preserves request headers and ignores unfin
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage(); await page.goto(server.url.href);
await page.getByText("Schedule", { exact: true }).click();
await page.getByText("Loading schedule…", { exact: true }).waitFor();
expect(await page.getByRole("button", { name: "Save schedule", exact: true }).isEnabled()).toBe(false);
expect(await page.getByLabel("Schedule path").isEnabled()).toBe(false);
Expand All @@ -42,6 +45,7 @@ test("schedule loads before editing, preserves request headers and ignores unfin
expect(await page.getByLabel("Schedule path").inputValue()).toBe("/existing");
expect(await page.getByLabel("Schedule interval", { exact: true }).inputValue()).toBe("2");
expect(await page.getByLabel("Schedule interval unit").inputValue()).toBe("hours");
await page.getByText("Advanced request settings", { exact: true }).click();
expect(await page.getByLabel("Schedule body").inputValue()).toBe("prior body");
await page.getByLabel("Schedule interval unit").selectOption("minutes");
expect(await page.getByLabel("Schedule interval", { exact: true }).inputValue()).toBe("120");
Expand All @@ -60,5 +64,10 @@ test("schedule loads before editing, preserves request headers and ignores unfin
await page.getByRole("button", { name: "Save schedule", exact: true }).click();
await page.getByRole("button", { name: "Pause schedule", exact: true }).click();
expect(calls.filter(call => call.action === "set").at(-1)!.request!.headers).toEqual([["x-new", "value"]]);
unsupported = true;
await page.goto(`${server.url}?unsupported=1`);
await page.getByText("This artifact has no validated server.", { exact: false }).waitFor();
expect(await page.getByRole("button", { name: "Save schedule", exact: true }).count()).toBe(0);
await page.getByText("No runs recorded yet.", { exact: true }).waitFor();
} finally { release(); await browser.close(); server.stop(true); }
}, 30000);
10 changes: 7 additions & 3 deletions e2e/gallery-drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test("gallery retains complete unsaved projects and new scripts and offers confl
page.setDefaultTimeout(5000);
await page.goto(server.url.href);
await page.getByTitle("default/report", { exact: true }).click();
await page.getByRole("group", { name: "Artifact view" }).getByRole("button", { name: "Source", exact: true }).click();
await page.getByRole("tablist", { name: "Artifact view" }).getByRole("tab", { name: "Source", exact: true }).click();
const client = page.getByRole("textbox", { name: "report.artifact.tsx", exact: true });
await client.fill("unsaved client");
await page.getByRole("button", { name: "report.artifact.server.ts", exact: true }).click();
Expand Down Expand Up @@ -65,11 +65,15 @@ test("gallery retains complete unsaved projects and new scripts and offers confl
expect(await page.getByRole("textbox", { name: "Project dependencies" }).inputValue()).toBe('{"example":"latest"}');
expect(await page.getByRole("button", { name: "Save and deploy", exact: true }).isEnabled()).toBe(false);
await page.getByRole("textbox", { name: "Project dependencies" }).fill("{}");
await page.getByRole("combobox", { name: "Version", exact: true }).selectOption("old");
await page.getByRole("button", { name: "More actions", exact: true }).click();
await page.getByRole("menuitem", { name: "Versions", exact: true }).click();
await page.getByRole("menuitem", { name: /^Revision 1 ·/ }).click();
await page.getByRole("button", { name: "Restore and deploy", exact: true }).waitFor();
expect(await client.innerText()).toBe("historical client");
expect(await page.locator(".live-data-note").innerText()).toContain("Historical code uses the current database and files and can change them");
await page.getByRole("combobox", { name: "Version", exact: true }).selectOption("working");
await page.getByRole("button", { name: "More actions", exact: true }).click();
await page.getByRole("menuitem", { name: "Versions", exact: true }).click();
await page.getByRole("menuitem", { name: "Working copy", exact: true }).click();
expect(await client.innerText()).toBe("unsaved client");
await page.getByRole("button", { name: "Save and deploy", exact: true }).click();
await page.getByRole("button", { name: "Compare saved project" }).waitFor();
Expand Down
6 changes: 3 additions & 3 deletions e2e/gallery-live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test("live changes update clean editors, preserve dirty drafts, and reconcile af
const connected = page.waitForEvent("websocket").then(socket => socket.waitForEvent("framereceived", { predicate: event => event.payload === "ready" }));
await page.goto(server.url.href);
await connected;
await page.getByRole("button", { name: "Source", exact: true }).click();
await page.getByRole("tab", { name: "Source", exact: true }).click();
const editor = page.getByRole("textbox", { name: "report.artifact.tsx", exact: true });
await editor.waitFor();
source = "agent update"; revision = "two";
Expand Down Expand Up @@ -100,10 +100,10 @@ test("live changes update clean editors, preserve dirty drafts, and reconcile af
expect(await reports.getAttribute("aria-expanded")).toBe("false");
await reports.click();
await nested.click();
await page.getByRole("button", { name: "Source", exact: true }).click();
await page.getByRole("tab", { name: "Source", exact: true }).click();
await page.getByRole("textbox", { name: "missed.artifact.tsx", exact: true }).waitFor();
await page.getByTitle("default/report", { exact: true }).click();
await page.getByRole("button", { name: "Source", exact: true }).click();
await page.getByRole("tab", { name: "Source", exact: true }).click();
expect(await editor.innerText()).toBe("my unsaved changes");
} finally { await browser.close(); server.stop(true); }
}, 30_000);
Loading
Loading