Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b1e6caa
Add test for workflowDeployBody's catalog-offering shape
TheGreatAxios Sep 6, 2026
fae77c0
e2e: deploy workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
b03ce16
Fix formatting
TheGreatAxios Sep 6, 2026
f981798
Add tests for deploying default workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
1ce35c4
Seeding: deploy default workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
2587cd2
longevity-sim: deploy workflows by catalog offering ids
TheGreatAxios Sep 6, 2026
2430449
Fix formatting: prettier
TheGreatAxios Sep 6, 2026
b65781e
Merge remote-tracking branch 'origin/cl-7461-seed-deploy-offering-ids…
TheGreatAxios Sep 6, 2026
602ad68
sandbox-sidecar: add tests for generation-0 ensure/destroy requests
TheGreatAxios Sep 6, 2026
e01feda
sandbox-sidecar: accept the allocation service's initial generation
TheGreatAxios Sep 6, 2026
c6da1fa
sandbox-sidecar: fix formatting in provisioner test
TheGreatAxios Sep 6, 2026
72d5be0
Merge remote-tracking branch 'origin/cl-7107-repin-and-re-vendor-audi…
TheGreatAxios Sep 6, 2026
824c2b6
Add tests for resolved catalog offerings and modelSourceFor's trimmed…
TheGreatAxios Sep 6, 2026
ec87551
Seed deploys against a tenant's resolved catalog offerings, not just …
TheGreatAxios Sep 6, 2026
dd1fe50
Merge remote-tracking branch 'origin/cl-7461-seed-deploy-offering-ids…
TheGreatAxios Sep 6, 2026
bcb8703
Add a regression test pinning the shared-sidecar auth gap (CL-7472)
TheGreatAxios Sep 6, 2026
ccbe497
e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
TheGreatAxios Sep 6, 2026
308ab25
ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
TheGreatAxios Sep 6, 2026
d8d59d7
Merge the re-pin branch into cl-7472-sidecar-ws
TheGreatAxios Sep 6, 2026
72e5540
e2e: lint fixes
TheGreatAxios Sep 6, 2026
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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ jobs:
run: bun run test:e2e
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/workbench
E2E_LOG_DIR: ${{ runner.temp }}/e2e-logs

- name: Upload hub/sidecar logs
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-logs
path: ${{ runner.temp }}/e2e-logs
if-no-files-found: ignore

isolation:
runs-on: ubuntu-latest
Expand Down
37 changes: 10 additions & 27 deletions scripts/e2e/channel-reconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,9 @@ import {
e2eDatabaseUrl,
expectStatus,
freePort,
provisionSidecar,
startHub,
startSidecar,
type ApiResult,
type HubHandle,
type SpawnedApp,
} from "./harness.ts";

const databaseUrl = e2eDatabaseUrl();
Expand Down Expand Up @@ -76,7 +73,6 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
let hubPort: number;
let sessionSecret: string;
let hub: HubHandle;
let sidecar: SpawnedApp;
let api: ApiCall;
let cookies: string[];
let tenantId: string;
Expand All @@ -93,10 +89,6 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
expect(report.action).toBe("migrated");
expect(report.migrations).toBeGreaterThan(0);

const sidecarId = "workbench-reconnect-e2e-sidecar";
const sidecarToken = crypto.randomUUID();
await provisionSidecar(url, sidecarId, sidecarToken);

hubPort = freePort();
sessionSecret = Buffer.from(
crypto.getRandomValues(new Uint8Array(32)),
Expand All @@ -110,14 +102,6 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
});
track(hub);

sidecar = startSidecar({
hubPort,
sidecarId,
token: sidecarToken,
dataDir: await tempDir("e2e-workbench-reconnect-sidecar-data-"),
});
track(sidecar);

api = createHubAPI(hub.baseUrl);

const email = `workbench-reconnect-e2e-${crypto.randomUUID()}@example.invalid`;
Expand Down Expand Up @@ -159,9 +143,9 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
const deadline = Date.now() + 60_000;
let res: ApiResult;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before workbench creation; output:\n${sidecar.output()}`,
`hub exited before workbench creation; output:\n${hub.output()}`,
);
}
res = await api(
Expand All @@ -174,7 +158,7 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
if (Date.now() > deadline) {
throw new Error(
`workbench never became launchable (hub kept answering 500): ` +
`${JSON.stringify(res.data)}\nsidecar output:\n${sidecar.output()}`,
`${JSON.stringify(res.data)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(1000);
Expand Down Expand Up @@ -244,10 +228,10 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
// hub process on the same port against the same database, exactly
// as a real restart looks from the sidecar's and the browser's
// side: same DB rows, same session cookie, new process. The port
// must stay the same — the sidecar's own `HUB_WS_URL` is fixed at
// its boot and is never reconfigured here (only the hub restarts),
// so a different port would leave the sidecar dialing a hub that
// is no longer there.
// must stay the same — the process-provisioner-spawned sidecar's
// own `HUB_WS_URL` is fixed at its boot and is never reconfigured
// here (only the hub restarts), so a different port would leave the
// sidecar dialing a hub that is no longer there.
await hub.stop();

const restarted = await startHub({
Expand All @@ -269,18 +253,17 @@ describe.skipIf(databaseUrl === undefined)("workbench reconnect e2e", () => {
const deadline = Date.now() + 180_000;
let posted: ApiResult;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited after hub restart; output:\n${sidecar.output()}`,
`hub exited after hub restart; output:\n${hub.output()}`,
);
}
posted = await postMessage(after);
if (posted.status === 201) break;
if (Date.now() > deadline) {
throw new Error(
`message after hub restart never accepted: status=${posted.status} ` +
`${JSON.stringify(posted.data)}\nsidecar output:\n${sidecar.output()}` +
`\nhub output:\n${hub.output()}`,
`${JSON.stringify(posted.data)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(1000);
Expand Down
61 changes: 23 additions & 38 deletions scripts/e2e/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,11 @@ import {
e2eDatabaseUrl,
expectStatus,
freePort,
provisionSidecar,
pushWorkflowSource,
workflowDeployBody,
startHub,
startSidecar,
type ApiResult,
type HubHandle,
type SpawnedApp,
} from "./harness.ts";

const databaseUrl = e2eDatabaseUrl();
Expand Down Expand Up @@ -139,7 +136,6 @@ const textPart = (text: string): Part[] => [{ kind: "text", text }];

describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
let hub: HubHandle;
let sidecar: SpawnedApp;
let api: ApiCall;
let user1: SignedUpUser;
let user2: SignedUpUser;
Expand All @@ -159,10 +155,6 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
expect(report.action).toBe("migrated");
expect(report.migrations).toBeGreaterThan(0);

const sidecarId = "chat-e2e-sidecar";
const sidecarToken = crypto.randomUUID();
await provisionSidecar(url, sidecarId, sidecarToken);

hub = await startHub({
databaseUrl: url,
port: freePort(),
Expand All @@ -173,16 +165,6 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
});
track(hub);

sidecar = startSidecar({
hubPort: new URL(hub.baseUrl).port
? Number(new URL(hub.baseUrl).port)
: 80,
sidecarId,
token: sidecarToken,
dataDir: await tempDir("e2e-chat-sidecar-data-"),
});
track(sidecar);

api = createHubAPI(hub.baseUrl);

// Two independent browser-shaped accounts, each carrying its own
Expand Down Expand Up @@ -358,15 +340,15 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
),
});

// Retries while the hub still answers 502 (the sidecar's dial-in
// may not have completed yet), matching `createWorkbench`'s own
// retry loop below.
// Retries while the hub still answers 502 (the process
// provisioner's spawned sidecar may not have dialed in yet),
// matching `createWorkbench`'s own retry loop below.
const echoDeployDeadline = Date.now() + 60_000;
let echoDeployed: ApiResult;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before echo deploy; output:\n${sidecar.output()}`,
`hub exited before echo deploy; output:\n${hub.output()}`,
);
}
echoDeployed = await api(
Expand All @@ -384,7 +366,7 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
if (Date.now() > echoDeployDeadline) {
throw new Error(
`echo workflow never became deployable (hub kept answering 502): ` +
`${JSON.stringify(echoDeployed.data)}\nsidecar output:\n${sidecar.output()}`,
`${JSON.stringify(echoDeployed.data)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(1000);
Expand All @@ -393,22 +375,22 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
}, 120_000);

// Launching a workbench is the go/no-go signal for the whole suite: it
// launches the anchor instance in-process, which needs the
// sidecar's dial-in to have completed. `packages/chat/src/routes.ts`
// has no 502-style retry translation of its own (unlike the native
// workflow deploy route), so a launch attempted before the sidecar
// connects fails with an uncaught 500 — retried here directly until
// the sidecar is ready, exactly as the walking skeleton retries a
// 502 for the native deploy route.
// launches the anchor instance in-process, which needs its
// process-provisioner-spawned sidecar's dial-in to have completed.
// `packages/chat/src/routes.ts` has no 502-style retry translation of
// its own (unlike the native workflow deploy route), so a launch
// attempted before the sidecar connects fails with an uncaught 500 —
// retried here directly until the sidecar is ready, exactly as the
// walking skeleton retries a 502 for the native deploy route.
async function createWorkbench(
body: Record<string, unknown>,
): Promise<ApiResult> {
const deadline = Date.now() + 60_000;
let res: ApiResult;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before workbench creation; output:\n${sidecar.output()}`,
`hub exited before workbench creation; output:\n${hub.output()}`,
);
}
res = await api(
Expand All @@ -421,7 +403,7 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
if (Date.now() > deadline) {
throw new Error(
`workbench never became launchable (hub kept answering 500): ` +
`${JSON.stringify(res.data)}\nhub output:\n${hub.output()}\nsidecar output:\n${sidecar.output()}`,
`${JSON.stringify(res.data)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(1000);
Expand Down Expand Up @@ -1021,10 +1003,13 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => {
// temporarily reverting the self-anchor fix locally: the sidecar log
// then carries ten `enqueueInbox failed, withholding ack` lines, one
// per rejected pack, for this same suite; with the fix applied, zero.
// Every process-provisioner-spawned sidecar inherits the hub
// process's own stdio (`process-runner.ts`'s `stdout: "inherit"`), so
// `hub.output()` already carries every sidecar's log lines.
test("no chat/folded run's workflow-run pack was ever permanently rejected", () => {
const sidecarOutput = sidecar.output();
expect(sidecarOutput).not.toContain("enqueueInbox failed");
expect(sidecarOutput).not.toContain("withholding ack");
expect(sidecarOutput).not.toContain("rejecting inbound mail");
const hubOutput = hub.output();
expect(hubOutput).not.toContain("enqueueInbox failed");
expect(hubOutput).not.toContain("withholding ack");
expect(hubOutput).not.toContain("rejecting inbound mail");
});
});
36 changes: 9 additions & 27 deletions scripts/e2e/greeting-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,9 @@ import {
expectStatus,
freePort,
hop,
provisionSidecar,
startHub,
startSidecar,
type ApiResult,
type HubHandle,
type SpawnedApp,
} from "./harness.ts";

const { tempDir, track } = createCleanupHarness();
Expand Down Expand Up @@ -135,10 +132,6 @@ describe.skipIf(databaseUrl === undefined)(
expect(report.action).toBe("migrated");
});

const sidecarId = "greeting-delivery-sidecar";
const sidecarToken = crypto.randomUUID();
await provisionSidecar(url, sidecarId, sidecarToken);

const hub: HubHandle = await hop("hub boot", async () =>
startHub({
databaseUrl: url,
Expand All @@ -151,16 +144,6 @@ describe.skipIf(databaseUrl === undefined)(
);
track(hub);

const sidecar: SpawnedApp = startSidecar({
hubPort: new URL(hub.baseUrl).port
? Number(new URL(hub.baseUrl).port)
: 80,
sidecarId,
token: sidecarToken,
dataDir: await tempDir("e2e-greeting-delivery-sidecar-data-"),
});
track(sidecar);

const hubApi: ApiCall = createHubAPI(hub.baseUrl);

const user = await hop("sign-up", () =>
Expand Down Expand Up @@ -236,9 +219,9 @@ describe.skipIf(databaseUrl === undefined)(
async () => {
const deadline = Date.now() + 60_000;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before ensureSeeded could run; output:\n${sidecar.output()}`,
`hub exited before ensureSeeded could run; output:\n${hub.output()}`,
);
}
try {
Expand Down Expand Up @@ -269,9 +252,9 @@ describe.skipIf(databaseUrl === undefined)(
async function deploySeededWorkflows(): Promise<void> {
const deadline = Date.now() + 60_000;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before default workflows could deploy; output:\n${sidecar.output()}`,
`hub exited before default workflows could deploy; output:\n${hub.output()}`,
);
}
try {
Expand Down Expand Up @@ -345,9 +328,9 @@ describe.skipIf(databaseUrl === undefined)(
const deadline = Date.now() + 60_000;
let res: ApiResult;
for (;;) {
if (sidecar.exited()) {
if (hub.exited()) {
throw new Error(
`sidecar exited before chat creation; output:\n${sidecar.output()}`,
`hub exited before chat creation; output:\n${hub.output()}`,
);
}
res = await api(
Expand All @@ -361,7 +344,7 @@ describe.skipIf(databaseUrl === undefined)(
if (Date.now() > deadline) {
throw new Error(
`chat never became mintable (hub kept answering 500): ` +
`${JSON.stringify(res.data)}\nsidecar output:\n${sidecar.output()}`,
`${JSON.stringify(res.data)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(200);
Expand Down Expand Up @@ -437,7 +420,7 @@ describe.skipIf(databaseUrl === undefined)(
throw new Error(
`no agent-authored message landed in chat ${chatId} within ` +
`60s of mint with zero user messages sent; messages seen: ` +
`${JSON.stringify(items)}\nsidecar output:\n${sidecar.output()}`,
`${JSON.stringify(items)}\nhub output:\n${hub.output()}`,
);
}
await Bun.sleep(200);
Expand Down Expand Up @@ -516,8 +499,7 @@ describe.skipIf(databaseUrl === undefined)(
throw new Error(
`no answer to turn ${turn} within 180s; agent messages: ` +
`${JSON.stringify(agentTexts)}\nhub output (tail):\n` +
`${hub.output().slice(-60000)}\nsidecar output (tail):\n` +
`${sidecar.output().slice(-6000)}`,
`${hub.output().slice(-60000)}`,
);
}
await Bun.sleep(2000);
Expand Down
Loading
Loading