diff --git a/packages/deploy/src/deploy.test.ts b/packages/deploy/src/deploy.test.ts index 2ab6d49..d90399f 100644 --- a/packages/deploy/src/deploy.test.ts +++ b/packages/deploy/src/deploy.test.ts @@ -2489,3 +2489,76 @@ test('deploy merges explicit --input with picker-collected values for the launch await cleanup(); } }); + +test('cloud deploy advertises the trigger URL in both addressing forms', async () => { + // The whole point of printing this is that a user hand-copies it into their + // app, so the exact path is load-bearing: a future change to the route shape + // must not silently mis-advertise it. + const { personaPath, cleanup } = await withTempPersona(basePersonaJson()); + const io = createBufferedIO(); + try { + await deploy( + { + personaPath, + mode: 'cloud', + cloudUrl: 'https://cloud.example.test/', + io + }, + { + workspaceAuth: { + async resolveWorkspace() { + return { workspace: 'ws-test', token: 'tok' }; + } + }, + providerConfigKeys: { async resolve() { return undefined; } }, + integrations: { + async isConnected() { return true; }, + async connect() { return { connectionId: 'conn-github' }; } + }, + bundle: successfulBundleStager(), + modes: { + cloud: { + async launch() { + return { + id: 'agent-uuid-1', + async stop() { /* no-op */ }, + done: Promise.resolve({ code: 0 }) + }; + } + } + } + } + ); + + const base = 'https://cloud.example.test/api/v1/workspaces/ws-test/deployments'; + const lines = io.messages.map((m) => m.message); + + // Name form leads: unique per workspace and stable across a redeploy. + assert.ok( + lines.includes(`trigger from your app: POST ${base}/demo/trigger`), + `missing name-form trigger URL in:\n${lines.join('\n')}` + ); + // Id form is the portable fallback for a compatible backend that only + // implements the original uuid-keyed route. + assert.ok( + lines.includes(` by id (portable): POST ${base}/agent-uuid-1/trigger`), + `missing id-form trigger URL in:\n${lines.join('\n')}` + ); + // The auth and body lines are part of the same copied instructions: a URL + // without them is not actually usable, so regressions there matter too. + assert.ok( + lines.includes( + ' auth: Bearer (dashboard \u2192 Workspace \u2192 Deployment API tokens)' + ), + `missing auth guidance in:\n${lines.join('\n')}` + ); + assert.ok( + lines.includes(' body: any JSON object; it reaches the handler as the event payload'), + `missing body guidance in:\n${lines.join('\n')}` + ); + // The trailing slash on cloudUrl must not produce a double slash. + assert.ok(!lines.some((line) => line.includes('test//api'))); + } finally { + await cleanup(); + } +}); diff --git a/packages/deploy/src/deploy.ts b/packages/deploy/src/deploy.ts index ebd5f36..518a64a 100644 --- a/packages/deploy/src/deploy.ts +++ b/packages/deploy/src/deploy.ts @@ -376,6 +376,32 @@ export async function deploy(opts: DeployOptions, resolvers: DeployResolvers = { }); io.info(`launched: ${mode}/${handle.id}`); + // Print the URL an external app POSTs to in order to wake this agent. + // + // Deploy is the moment someone is wiring the agent up, so it is the moment + // the URL is worth knowing. Without this, the only way to learn it was to be + // told the shape by hand — the endpoint is not otherwise advertised anywhere + // the deploying user looks. + // + // Prints BOTH addressing forms, because which one works depends on the + // backend. Agent Workforce cloud resolves an agent by its deployed NAME, and + // that is the better thing for an app to hard-code: it is unique per + // workspace and survives a destroy/redeploy that changes the uuid. But + // `--cloud-url` may point at any compatible runtime (see modes/cloud), and a + // backend that only implements the original uuid-keyed route would 404 on the + // name form. Printing one URL means printing one that is wrong for somebody; + // the id line is the portable fallback. + if (mode === 'cloud' && cloudUrl && workspace) { + const base = + `${cloudUrl.replace(/\/+$/, '')}/api/v1/workspaces/${encodeURIComponent(workspace)}/deployments`; + io.info( + `trigger from your app: POST ${base}/${encodeURIComponent(activePreflight.persona.id)}/trigger`, + ); + io.info(` by id (portable): POST ${base}/${encodeURIComponent(handle.id)}/trigger`); + io.info(' auth: Bearer (dashboard → Workspace → Deployment API tokens)'); + io.info(' body: any JSON object; it reaches the handler as the event payload'); + } + return { deploymentId: activePreflight.persona.id, mode,