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
73 changes: 73 additions & 0 deletions packages/deploy/src/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <deployment API token> (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')));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
await cleanup();
}
});
26 changes: 26 additions & 0 deletions packages/deploy/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <deployment API token> (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,
Expand Down
Loading