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
54 changes: 53 additions & 1 deletion packages/sdk/typescript/src/workspace-seeder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest';

import { RelayFileClient } from './client.js';

import { seedAclRules, seedWorkflowAcls, seedWorkspace } from './workspace-seeder.js';
import {
createWorkspaceIfNeeded,
seedAclRules,
seedWorkflowAcls,
seedWorkspace,
} from './workspace-seeder.js';

const originalFetch = globalThis.fetch;

Expand Down Expand Up @@ -321,3 +326,50 @@ describe('workspace-seeder', () => {
).rejects.toThrow('failed to seed workspace workspace-http: HTTP 503 relay unavailable');
});
});

describe('createWorkspaceIfNeeded', () => {
afterEach(() => {
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
});

it('treats a 404 collection route as an implicit-workspace deployment', async () => {
// relayfile-cloud addresses each workspace as a Durable Object by name, so it
// exposes no workspace-create endpoint and the workspace materializes on first
// use. A 404 here means "nothing to create", not "creation failed".
const calls: string[] = [];
globalThis.fetch = vi.fn(async (input: any) => {
calls.push(String(input));
return new Response(
JSON.stringify({ code: 'not_found', message: 'Route not found' }),
{ status: 404, headers: { 'content-type': 'application/json' } }
);
}) as unknown as typeof fetch;

await expect(
createWorkspaceIfNeeded('https://file.agentrelay.com', 'tok', 'rw_abc123')
).resolves.toBeUndefined();

// One attempt only: retrying other body shapes cannot conjure an absent route.
expect(calls).toHaveLength(1);
expect(calls[0]).toBe('https://file.agentrelay.com/v1/workspaces');
});

it('still throws on a real failure, naming the endpoint it tried', async () => {
globalThis.fetch = vi.fn(async () =>
new Response('boom', { status: 500 })
) as unknown as typeof fetch;

await expect(
createWorkspaceIfNeeded('https://file.agentrelay.com', 'tok', 'rw_abc123')
).rejects.toThrow(/Failed to create workspace rw_abc123 at https:\/\/file\.agentrelay\.com\/v1\/workspaces/);
});

it('succeeds on 409 (already exists)', async () => {
globalThis.fetch = vi.fn(async () => new Response('', { status: 409 })) as unknown as typeof fetch;

await expect(
createWorkspaceIfNeeded('https://file.agentrelay.com', 'tok', 'rw_abc123')
).resolves.toBeUndefined();
});
});
23 changes: 22 additions & 1 deletion packages/sdk/typescript/src/workspace-seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ export async function createWorkspaceIfNeeded(
{ id: workspace },
];
let lastFailure: string | null = null;
// A 404 on the collection route means this deployment has no workspace-create
// endpoint — not that creation failed. relayfile-cloud addresses each workspace
// as a Durable Object by name (`WORKSPACE_DO.idFromName(workspaceId)`), so the
// workspace comes into existence on the first request addressed to it and there
// is nothing to pre-create. Treating that as fatal made every caller that seeds
// a workspace (notably relayflows' per-agent permission provisioning) fail
// before doing any work.
let sawRouteMissing = false;

for (const body of bodyCandidates) {
try {
Expand All @@ -304,6 +312,12 @@ export async function createWorkspaceIfNeeded(
return;
}

if (response.status === 404) {
// Retrying the other body shapes cannot conjure a route that is absent.
sawRouteMissing = true;
break;
}

const responseBody = await response.text().catch(() => '');
lastFailure = `HTTP ${response.status} ${responseBody}`.trim();
if (response.status < 500 && response.status !== 409) {
Expand All @@ -314,8 +328,15 @@ export async function createWorkspaceIfNeeded(
}
}

if (sawRouteMissing) {
// Implicit-workspace deployment: nothing to create, so seeding can proceed.
return;
}

if (lastFailure) {
throw new Error(`Failed to create workspace ${workspace}: ${lastFailure}`);
throw new Error(
`Failed to create workspace ${workspace} at ${endpoint}: ${lastFailure}`
);
}
}

Expand Down
Loading