diff --git a/packages/deploy/src/login.test.ts b/packages/deploy/src/login.test.ts index e55bcfb4..cb4ad010 100644 --- a/packages/deploy/src/login.test.ts +++ b/packages/deploy/src/login.test.ts @@ -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 = { diff --git a/packages/deploy/src/login.ts b/packages/deploy/src/login.ts index b7187678..b0d4229b 100644 --- a/packages/deploy/src/login.ts +++ b/packages/deploy/src/login.ts @@ -158,6 +158,8 @@ async function resolveWorkspaceDescriptor(args: { return resolveActiveWorkspace({ apiUrl: args.apiUrl, interactive: false + }).catch((error) => { + throw workspaceNotFoundError(error, undefined); }); } @@ -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 ` 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');