Skip to content
Draft
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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2112,12 +2112,12 @@ polymorfa listen --project PROJECT_ID --forward-to http://127.0.0.1:3000/webhook
Trigger events in another:

```shell
polymorfa trigger --list --project PROJECT_ID
polymorfa trigger message.received --project PROJECT_ID --session my-test-number --text "hi"
polymorfa trigger message.received --project PROJECT_ID --session my-test-number \
polymorfa trigger --list
polymorfa trigger message.received --session my-test-number --text "hi"
polymorfa trigger message.received --session my-test-number \
--override mediaType=image --override caption=look
polymorfa trigger message.ack --project PROJECT_ID --session my-test-number --override ackStatus=read
polymorfa trigger message.failed --project PROJECT_ID --session my-test-number
polymorfa trigger message.ack --session my-test-number --override ackStatus=read
polymorfa trigger message.failed --session my-test-number
polymorfa trigger call.received --project PROJECT_ID --session my-test-number --override video=true
polymorfa trigger session.restriction_updated --project PROJECT_ID --session my-test-number \
--override restrictionActive=false
Expand All @@ -2136,7 +2136,7 @@ and `template.status`. `--override key=value`
sets one payload field and can be repeated; `--text` and `--from` are
shortcuts. `video` and `restrictionActive` accept only `true` or `false`;
`durationSeconds` accepts a whole number. Set `restrictionActive=false` to
test the end of a restriction. `--from-session OTHER_TEST_NUMBER` sends a real simulated text from
test the end of a restriction. `--from-number OTHER_TEST_NUMBER` sends a real simulated text from
another Test number in the project instead, producing an ordinary
`message.received` event you can reply to. Use `--json` for one JSON document.

Expand Down
30 changes: 30 additions & 0 deletions contracts/test-number-dropin-routes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"repository": "polymorfa/polymorfa",
"commit": "79e62733b1b3d523618232a3794e26df64b7f486",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "dcbd21ba3dafe0c30e52c260ccef759eaca1c0a11d6cc092f2a3ef68bf50f4d3",
"operations": [
{
"command": "trigger --list",
"method": "GET",
"path": "/messaging/test-events/fixtures"
},
{
"command": "trigger <event> --session <numberId> [--from-number <numberId>]",
"method": "POST",
"path": "/messaging/{session}/test-events",
"requestFields": ["event", "overrides", "fromNumber"]
},
{
"command": "testing history-fixture create --project <projectId>",
"method": "POST",
"path": "/messaging/projects/{projectId}/test-history-fixtures"
}
],
"sdkTransport": "Released SDK raw.request; no dependency pin or publication required.",
"scopes": ["sandbox:read", "sandbox:write"],
"historicalReferences": [
"docs/internal/2026-09-22-trigger-fixture-parity.md",
"docs/internal/2026-09-23-trigger-launch-docs.md"
]
}
26 changes: 26 additions & 0 deletions docs/internal/2026-10-20-test-number-dropin-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Number-scoped test routes

Source contract: `contracts/test-number-dropin-routes.json`, API PR
https://github.com/polymorfa/polymorfa/pull/792. The API base is #647.

`trigger` sends the Number ID in the path, never body `session`. The optional
`--from-number` is another Test Number ID, serialized as `fromNumber`;
`--from-session` has no alias. Trigger/list require no project flag. History
fixture creation keeps its project flag because it precedes Number creation.

These commands use the installed SDK's public `raw.request` transport and keep
its auth, timeout, retries and errors. The existing package pin stays valid;
there is no unpublished SDK dependency. No credential handling changes.

API: consumed and pinned; no API implementation here. SDK: transport reused,
typed methods updated separately. CLI: commands/help/examples/tests updated.
Docs: README and internal launch sample updated; public docs app handoff stays
with #783. Feature releases: no flag or availability change. Admin: no new
state/action and no direct dependency. Historical September notes remain
evidence of their original snapshot; their old names do not describe aliases.

Scope names remain `sandbox:read` / `sandbox:write`. Renaming affects CLI README
and internal launch/reference notes, SDK contracts/scope inputs, API scope
parsing and enforcement, Console key pickers and public documentation. No
spelling or numeric scope bit changes are included. No publication or
deployment is performed.
12 changes: 7 additions & 5 deletions docs/internal/launch-docs/cli-trigger.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
## Trigger with the CLI

Use a server API key with `sandbox:write` and a project with Test numbers access. Listing fixtures needs `sandbox:read`.
Use a team key or project token (including a Test key) with `sandbox:write` and a project with Test numbers access. Listing fixtures needs `sandbox:read`.

```sh
polymorfa trigger --list --project PROJECT_ID
polymorfa trigger message.received --project PROJECT_ID --session my-test-number --text "hi"
polymorfa trigger session.restriction_updated --project PROJECT_ID --session my-test-number \
polymorfa trigger --list
polymorfa trigger message.received --session my-test-number --text "hi"
polymorfa trigger session.restriction_updated --session my-test-number \
--override restrictionActive=false
```

The command prints the event type and ID returned by the API. The event also appears in event history and reaches subscribed webhooks with `source: "test"`. A real number is refused without sending an event.

To send a simulated message from another Test number in the same project, use `--from-session OTHER_TEST_NUMBER` with `message.received`. That sends a message you can reply to; the named fixtures above only generate events.
To send a simulated message from another Test number in the same project, use `--from-number OTHER_TEST_NUMBER` with `message.received`. That sends a message you can reply to; the named fixtures above only generate events.

The Number ID in `--session` determines the project. Fixture listing is a static catalog and needs no project selector. The trigger sends `fromNumber` as an optional Number ID; body `session` is not sent. Real Numbers return `404 resource_not_found`.
14 changes: 6 additions & 8 deletions src/commands/testing/history-fixture/create.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {Flags} from '@oclif/core'
import {
PolymorfaConfigurationError,
type TestingHistoryMessage,
} from '@polymorfa/sdk'
import {PolymorfaConfigurationError} from '@polymorfa/sdk'
import {readJsonInput} from '../../../api/request.js'
import {BaseCommand} from '../../../base-command.js'

Expand Down Expand Up @@ -36,10 +33,11 @@ export default class TestingHistoryFixtureCreate extends BaseCommand {
)
const response = await (
await this.messagingClient(flags, {serverApiKeyOnly: 'Testing history'})
).testing.createHistoryFixture(
flags.project,
input as {messages: TestingHistoryMessage[]},
)
).raw.request<{fixtureId: string}>({
method: 'POST',
path: `/messaging/projects/${encodeURIComponent(flags.project)}/test-history-fixtures`,
body: input,
})
this.writeValue(
{data: response.data, metadata: response.metadata},
response.data.fixtureId,
Expand Down
47 changes: 26 additions & 21 deletions src/commands/trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
TEST_EVENT_FIXTURES,
type TestEventFixture,
type TestEventOverrides,
type TriggerTestEventRequest,
type TriggerTestEventResponse,
type TestEventFixtureInfo,
} from '@polymorfa/sdk'
import {BaseCommand} from '../base-command.js'

Expand All @@ -27,10 +28,10 @@ export default class Trigger extends BaseCommand {
static override flags = {
list: Flags.boolean({
description: 'List fixtures and the overrides each accepts.',
exclusive: ['override', 'session', 'text', 'from', 'from-session'],
exclusive: ['override', 'session', 'text', 'from', 'from-number'],
}),
session: Flags.string({
description: 'Test number session name in the project.',
description: 'Test Number ID.',
}),
override: Flags.string({
description:
Expand All @@ -43,31 +44,28 @@ export default class Trigger extends BaseCommand {
from: Flags.string({
description: 'Shortcut for --override from=<E.164 phone>.',
}),
'from-session': Flags.string({
'from-number': Flags.string({
description:
'message.received only: send a simulated message from this other Test number instead of generating an event.',
}),
}
static override examples = [
'<%= config.bin %> trigger --list --project <projectId>',
'<%= config.bin %> trigger message.received --session my-test-number --text "hi" --project <projectId>',
'<%= config.bin %> trigger message.ack --session my-test-number --override ackStatus=read --project <projectId>',
'<%= config.bin %> trigger template.status --session my-test-number --override templateStatus=REJECTED --override reason=INVALID_FORMAT --project <projectId>',
'<%= config.bin %> trigger --list',
'<%= config.bin %> trigger message.received --session my-test-number --text "hi"',
'<%= config.bin %> trigger message.ack --session my-test-number --override ackStatus=read',
'<%= config.bin %> trigger template.status --session my-test-number --override templateStatus=REJECTED --override reason=INVALID_FORMAT',
]

async run(): Promise<void> {
const {args, flags} = await this.parse(Trigger)
if (!flags.project)
throw new PolymorfaConfigurationError(
'Select a project with --project.',
'project',
)
const client = await this.messagingClient(flags, {
serverApiKeyOnly: 'Test event',
})

if (flags.list) {
const response = await client.testing.listEventFixtures(flags.project)
const response = await client.raw.request<{
fixtures: TestEventFixtureInfo[]
}>({method: 'GET', path: '/messaging/test-events/fixtures'})
const human = response.data.fixtures
.map(
(fixture) =>
Expand Down Expand Up @@ -110,21 +108,28 @@ export default class Trigger extends BaseCommand {
)
overrides[key] = value
}
const request: TriggerTestEventRequest = {
session: flags.session,
const request: {
event: TestEventFixture
overrides?: TestEventOverrides
fromNumber?: string
} = {
event: args.event as TestEventFixture,
...(Object.keys(overrides).length > 0 ? {overrides} : {}),
...(flags['from-session'] === undefined
...(flags['from-number'] === undefined
? {}
: {fromSession: flags['from-session']}),
: {fromNumber: flags['from-number']}),
}
const response = await client.testing.triggerEvent(flags.project, request)
const response = await client.raw.request<TriggerTestEventResponse>({
method: 'POST',
path: `/messaging/${encodeURIComponent(flags.session)}/test-events`,
body: request,
})
const result = response.data
const human =
result.delivery === 'generated'
? `Triggered ${result.event} ${String(result.eventId)} on ${result.session} (source: test).\n` +
`A running \`polymorfa listen --project ${flags.project}\` prints it as: ${result.event} ${String(result.eventId)}`
: `Sent a simulated message to ${result.session} from ${String(flags['from-session'])}.\n` +
`A running \`polymorfa listen${flags.project ? ` --project ${flags.project}` : ''}\` prints it as: ${result.event} ${String(result.eventId)}`
: `Sent a simulated message to ${result.session} from ${String(flags['from-number'])}.\n` +
`The ${result.event} event arrives as Test number activity (source: runtime).`
this.writeValue({data: result, metadata: response.metadata}, human, flags)
}
Expand Down
2 changes: 1 addition & 1 deletion test/commands/quicklinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test('creates a QuickLink and uploads separate test history with exact project s
)
expect(server.requests.map(({method, url}) => `${method} ${url}`)).toEqual([
'POST /messaging/quicklinks',
`POST /messaging/testing/${projectId}/history-fixtures`,
`POST /messaging/projects/${projectId}/test-history-fixtures`,
])
expect(JSON.parse(server.requests[0]!.body)).toEqual({
projectId,
Expand Down
44 changes: 18 additions & 26 deletions test/commands/trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,17 @@ test('triggers message.received with text and typed overrides', async () => {
)
expect(result.stdout).toContain(`message.received ${eventId}`)
expect(requests.map(({method, url}) => `${method} ${url}`)).toEqual([
`POST /messaging/testing/${projectId}/events`,
`POST /messaging/test-a/test-events`,
])
expect(JSON.parse(requests[0]!.body)).toEqual({
session: 'test-a',
event: 'message.received',
overrides: {pushName: 'Ada', text: 'hi'},
})
})

test('sends a simulated message with --from-session and prints JSON', async () => {
test('sends a simulated message with --from-number and prints JSON', async () => {
const {result, requests} = await run(
`trigger message.received --project ${projectId} --session test-a --from-session test-b --json`,
`trigger message.received --project ${projectId} --session test-a --from-number test-b --json`,
[
{
status: 202,
Expand All @@ -83,42 +82,36 @@ test('sends a simulated message with --from-session and prints JSON', async () =
data: {delivery: 'simulated', eventId: null},
})
expect(JSON.parse(requests[0]!.body)).toEqual({
session: 'test-a',
event: 'message.received',
fromSession: 'test-b',
fromNumber: 'test-b',
})
})

test('lists fixtures', async () => {
const {result, requests} = await run(
`trigger --list --project ${projectId}`,
[
{
body: {
fixtures: [
{
name: 'message.ack',
description: 'Delivery or read receipt for an outbound message',
overrides: ['from', 'ackStatus', 'messageId'],
},
],
},
const {result, requests} = await run(`trigger --list`, [
{
body: {
fixtures: [
{
name: 'message.ack',
description: 'Delivery or read receipt for an outbound message',
overrides: ['from', 'ackStatus', 'messageId'],
},
],
},
],
)
},
])
expect(result.error).toBeUndefined()
expect(result.stdout).toContain('message.ack')
expect(result.stdout).toContain('overrides: from, ackStatus, messageId')
expect(requests[0]!.url).toBe(
`/messaging/testing/${projectId}/events/fixtures`,
)
expect(requests[0]!.url).toBe(`/messaging/test-events/fixtures`)
})

test.each(['true', 'false'])(
'triggers the listed restriction fixture with restrictionActive=%s',
async (active) => {
const fixture = 'session.restriction_updated'
const listed = await run(`trigger --list --project ${projectId}`, [
const listed = await run(`trigger --list`, [
{
body: {
fixtures: [
Expand Down Expand Up @@ -151,7 +144,6 @@ test.each(['true', 'false'])(
expect(result.error, result.stderr).toBeUndefined()
expect(requests).toHaveLength(1)
expect(JSON.parse(requests[0]!.body)).toEqual({
session: 'test-a',
event: fixture,
overrides: {restrictionActive: active === 'true'},
})
Expand Down
Loading