From e5aacc63f551af7092638c01818ff310370f113d Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 20 Oct 2026 01:01:58 +0300 Subject: [PATCH 1/2] Use Number-scoped test event commands --- README.md | 12 ++--- contracts/test-number-dropin-routes.json | 37 +++++++++++++++ .../2026-10-20-test-number-dropin-routes.md | 26 ++++++++++ docs/internal/launch-docs/cli-trigger.mdx | 12 +++-- .../testing/history-fixture/create.ts | 14 +++--- src/commands/trigger.ts | 47 ++++++++++--------- test/commands/quicklinks.test.ts | 2 +- test/commands/trigger.test.ts | 44 +++++++---------- 8 files changed, 127 insertions(+), 67 deletions(-) create mode 100644 contracts/test-number-dropin-routes.json create mode 100644 docs/internal/2026-10-20-test-number-dropin-routes.md diff --git a/README.md b/README.md index a6f427dd..9ebfd665 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/contracts/test-number-dropin-routes.json b/contracts/test-number-dropin-routes.json new file mode 100644 index 00000000..3c0ec0ae --- /dev/null +++ b/contracts/test-number-dropin-routes.json @@ -0,0 +1,37 @@ +{ + "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 --session [--from-number ]", + "method": "POST", + "path": "/messaging/{session}/test-events", + "requestFields": [ + "event", + "overrides", + "fromNumber" + ] + }, + { + "command": "testing history-fixture create --project ", + "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" + ] +} diff --git a/docs/internal/2026-10-20-test-number-dropin-routes.md b/docs/internal/2026-10-20-test-number-dropin-routes.md new file mode 100644 index 00000000..4db2362b --- /dev/null +++ b/docs/internal/2026-10-20-test-number-dropin-routes.md @@ -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. diff --git a/docs/internal/launch-docs/cli-trigger.mdx b/docs/internal/launch-docs/cli-trigger.mdx index 90e0bb95..e16a76bf 100644 --- a/docs/internal/launch-docs/cli-trigger.mdx +++ b/docs/internal/launch-docs/cli-trigger.mdx @@ -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`. diff --git a/src/commands/testing/history-fixture/create.ts b/src/commands/testing/history-fixture/create.ts index 5cce4637..4f95ee19 100644 --- a/src/commands/testing/history-fixture/create.ts +++ b/src/commands/testing/history-fixture/create.ts @@ -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' @@ -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, diff --git a/src/commands/trigger.ts b/src/commands/trigger.ts index 60dc555b..5f03e477 100644 --- a/src/commands/trigger.ts +++ b/src/commands/trigger.ts @@ -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' @@ -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: @@ -43,31 +44,28 @@ export default class Trigger extends BaseCommand { from: Flags.string({ description: 'Shortcut for --override from=.', }), - '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 ', - '<%= config.bin %> trigger message.received --session my-test-number --text "hi" --project ', - '<%= config.bin %> trigger message.ack --session my-test-number --override ackStatus=read --project ', - '<%= config.bin %> trigger template.status --session my-test-number --override templateStatus=REJECTED --override reason=INVALID_FORMAT --project ', + '<%= 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 { 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) => @@ -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({ + 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) } diff --git a/test/commands/quicklinks.test.ts b/test/commands/quicklinks.test.ts index c05894a5..5e9430f7 100644 --- a/test/commands/quicklinks.test.ts +++ b/test/commands/quicklinks.test.ts @@ -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, diff --git a/test/commands/trigger.test.ts b/test/commands/trigger.test.ts index 67909f7e..785c0502 100644 --- a/test/commands/trigger.test.ts +++ b/test/commands/trigger.test.ts @@ -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, @@ -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: [ @@ -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'}, }) From fa9ff0d5bf843ae0d0c0fe04fa870e3def35ede3 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Wed, 21 Oct 2026 10:35:20 +0300 Subject: [PATCH 2/2] Fix contract pin format and patch MCP server dependency --- contracts/test-number-dropin-routes.json | 11 ++--------- package-lock.json | 18 ++++++++++-------- package.json | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/contracts/test-number-dropin-routes.json b/contracts/test-number-dropin-routes.json index 3c0ec0ae..edd6e6f0 100644 --- a/contracts/test-number-dropin-routes.json +++ b/contracts/test-number-dropin-routes.json @@ -13,11 +13,7 @@ "command": "trigger --session [--from-number ]", "method": "POST", "path": "/messaging/{session}/test-events", - "requestFields": [ - "event", - "overrides", - "fromNumber" - ] + "requestFields": ["event", "overrides", "fromNumber"] }, { "command": "testing history-fixture create --project ", @@ -26,10 +22,7 @@ } ], "sdkTransport": "Released SDK raw.request; no dependency pin or publication required.", - "scopes": [ - "sandbox:read", - "sandbox:write" - ], + "scopes": ["sandbox:read", "sandbox:write"], "historicalReferences": [ "docs/internal/2026-09-22-trigger-fixture-parity.md", "docs/internal/2026-09-23-trigger-launch-docs.md" diff --git a/package-lock.json b/package-lock.json index 2da282d3..d4c1a04c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0-dev.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/server": "2.3.1", "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", "@polymorfa/sdk": "0.1.0-dev.20261004024706", @@ -708,9 +708,10 @@ "dev": true }, "node_modules/@modelcontextprotocol/core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", - "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "license": "Apache-2.0", "dependencies": { "zod": "^4.2.0" }, @@ -719,11 +720,12 @@ } }, "node_modules/@modelcontextprotocol/server": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", - "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", + "integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==", + "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/core": "2.0.0", + "@modelcontextprotocol/core": "2.3.1", "zod": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 97f2c5b9..780699f0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "verify": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build && npm pack --dry-run --ignore-scripts && npm run audit:prod" }, "dependencies": { - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/server": "2.3.1", "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", "@polymorfa/sdk": "0.1.0-dev.20261004024706",