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
21 changes: 21 additions & 0 deletions packages/deploy/src/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ test('resolveWorkspaceToken fails clearly when workspace resolve returns non-JSO
});
});

test('resolveWorkspaceToken surfaces stale-workspace guidance on 404', async () => {
await withCloudSessionEnv(async () => {
const restoreFetch = withTrappedFetch(async () =>
new Response('Workspace not found', { status: 404 })
);
try {
await assert.rejects(
resolveWorkspaceToken({
workspace: 'rw_stale',
cloudUrl: 'https://cloud.example.test',
io: createBufferedIO(),
noPrompt: true
}),
/agent-relay workspace list.*agent-relay workspace switch/s
);
} finally {
restoreFetch();
}
});
});

test('resolveWorkspaceToken without a cloud session fails with login guidance', () => {
const home = mkdtempSync(path.join(os.tmpdir(), 'wf-no-cloud-session-'));
const env: NodeJS.ProcessEnv = {
Expand Down
30 changes: 29 additions & 1 deletion packages/deploy/src/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ async function resolveWorkspaceDescriptor(args: {
return resolveActiveWorkspace({
apiUrl: args.apiUrl,
interactive: false
}).catch((error) => {
throw workspaceNotFoundError(error, undefined);
});
}

Expand All @@ -171,12 +173,38 @@ async function resolveWorkspaceDescriptor(args: {
);
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`workspace resolve failed for ${workspace}: ${response.status} ${text}`.trim());
throw workspaceNotFoundError(
new Error(`workspace resolve failed for ${workspace}: ${response.status} ${text}`.trim()),
workspace,
response.status
);
}
const payload = await response.json().catch(() => null);
return normalizeWorkspaceDescriptor(payload, session.auth.apiUrl || args.apiUrl);
}

/**
* The active-workspace pointer stored locally (`agent-relay workspace switch`)
* can go stale if the workspace was deleted or expired server-side; the raw
* 404 from @agent-relay/cloud gives no hint that the *fix* is to re-pick a
* workspace rather than retry. Detect that case and append actionable guidance.
*/
function workspaceNotFoundError(error: unknown, workspace: string | undefined, status?: number): Error {
const message = error instanceof Error ? error.message : String(error);
const is404 = status === 404 || /:\s*404\b/.test(message) || /workspace not found/i.test(message);
if (!is404) {
return error instanceof Error ? error : new Error(message);
}
const diagnosis = workspace
? `workspace "${workspace}" was not found server-side (deleted, expired, revoked, or never provisioned).`
: 'the active workspace was not found server-side (deleted, expired, or never provisioned) — the local workspace pointer is stale.';
return new Error(
`${message}\n\n${diagnosis} ` +
'Run `agent-relay workspace list` to see valid workspaces, then `agent-relay workspace switch <name>` to pick one, ' +
'or `agentworkforce deploy --mode cloud` to provision a new one.'
);
}

function normalizeWorkspaceDescriptor(payload: unknown, apiUrl: string): ActiveWorkspaceDescriptor {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('workspace resolve returned an invalid descriptor');
Expand Down
Loading