Guides
How to Test an AI Music API Integration in Node.js
- Written by
- Sonilo Team
- Published

Test an AI music API integration in three layers: run the request and response contract against a local mock, read the live account configuration without generating media, then allow exactly one production smoke job after an explicit trial or budget check. Keep the production generation path disabled by default, and fail the test when a task reports success without a nonempty HTTPS audio result.
By Sonilo Team · Facts verified August 26, 2026
Disclosure: Sonilo publishes this guide and operates the API used as the concrete example. The `api-contract-gate-v1` module and its fixtures were tested locally. The tests did not call a production account, generate music, use a free trial, inspect an invoice, measure provider availability or latency, or evaluate audio quality.
Separate contract confidence from generation confidence
One end-to-end generation is a weak test suite. It can prove that one input completed once, but it does not cheaply reproduce authentication failures, insufficient balance, invalid input, rate limiting, malformed success responses, or timeouts. Repeating generation to reach those branches can also consume trial allowance or paid capacity without improving the client contract.
Use three layers with different permissions:
| Layer | Network target | Generation allowed | Release question |
|---|---|---|---|
| Local contract | A loopback mock server | No | Does the client send, classify, poll, and validate the documented shapes? |
| Account preflight | Read-only production account endpoint | No | Is the service enabled, and does an eligible trial remain when the smoke run requires one? |
| Live smoke | Production generation and task endpoints | Exactly one explicitly approved job | Can the current account create and retrieve one real, nonempty output? |
The Sonilo API reference identifies `https://api.sonilo.com/v1` as the runtime base. It documents `POST /v1/video-to-music` for video-guided music, `GET /v1/account/services` for current account limits, and `GET /v1/tasks/{task_id}` for an asynchronous result. Documentation hosts are not runtime API hosts.
This structure is not a provider availability benchmark. It is an application release gate. The local layer protects request and response logic. The account layer protects configuration assumptions. The final smoke checks one real integration path without turning routine tests into an uncontrolled generation loop.
Freeze the current contract into local fixtures
Start from the machine-readable interface, not from a copied response in an old ticket. The current OpenAPI 1.0.0 document defines the async Video-to-Music submission as multipart form data with `mode=async`, a `202` response containing `task_id` and `status=processing`, and a task response with `processing`, `succeeded`, or `failed` state.
The current Video-to-Music reference requires exactly one video source. A client can send a file or `video_url`; the tested example uses an HTTPS URL so a small JSON-and-form fixture can exercise the boundary without storing customer media. It also pins `variants_num=1`, because additional variants are separate generated directions and can change cost.
Create fixtures for behavior, not every optional field. The minimum useful set is:
| Fixture | Required shape | Client decision |
|---|---|---|
| Accepted | HTTP 202, nonempty `task_id`, `status=processing` | Persist the ID and begin bounded reads |
| Processing | Matching `task_id`, `status=processing` | Read again within the existing poll budget |
| Succeeded | Matching ID, success state, nonempty `audio` array | Validate every requested output before exposure |
| Failed | Terminal state plus provider error when present | Stop and preserve the classification |
| Error | Non-2xx status plus JSON `code` and `message` | Map the provider class without inventing success data |
The Retrieve Task reference says music tasks created with `mode=async` are read through the exact task path. On success, the tested gate requires at least one audio entry with an HTTPS URL, a positive integer file size, and an `audio/*` content type. That is an application acceptance rule built from the documented media fields. It does not prove that the bytes decode, sound good, or remain available indefinitely.
If your production client uses the default streaming mode instead, test its NDJSON framing separately. The Node.js NDJSON workflow owns byte decoding, line boundaries, ordered chunk assembly, and terminal completion. Do not make one fixture pretend that streaming and asynchronous tasks are the same interface.
Block production generation in the client
Environment naming alone is not a safety control. A test process can inherit a real base URL and a real key. Put the guard inside the function that performs the generation POST so an accidental production destination fails before `fetch` runs.
The tested gate uses this rule:
- `const PRODUCTION_ORIGIN = "https://api.sonilo.com";`
- `function requireGenerationApproval(baseUrl, allowLiveGeneration) {`
- ` const origin = new URL(baseUrl).origin;`
- ` if (origin === PRODUCTION_ORIGIN && allowLiveGeneration !== true) {`
- ` throw new Error("Production generation is blocked");`
- ` }`
- `}`
The check is exact and local. A truthy string such as `"true"` is not enough. Unit tests confirm that an unapproved production call reaches the guard before the injected fetch function receives any request.
Keep the bearer key server-side. The API introduction requires `Authorization: Bearer` on API requests. For a browser-facing application, use the Next.js server boundary or the equivalent trusted backend in your framework. Local fixtures should use a fake key and must never copy a production credential into snapshots, logs, or test output.
Use a source video that has already passed local checks before the one live smoke. The video preflight workflow checks bytes, a video stream, duration, and live upload limits. If the smoke uses a signed URL, apply the source-URL lifetime gate so the remote input remains readable long enough for the provider fetch.
Exercise the full task lifecycle locally
Node.js can run this contract suite without a third-party mocking package. The stable Node.js test runner supplies test execution and assertions, while the built-in HTTP server can bind to `127.0.0.1` on an ephemeral port. The client receives that loopback base URL through dependency injection.
Test the POST boundary first. Assert the exact method and path, the bearer header, a multipart content type, one `video_url`, `mode=async`, and `variants_num=1`. Return the documented 202 fixture and require the client to preserve the task ID.
Then test the task reader independently:
- Return `processing`, then a valid `succeeded` fixture.
- Return `failed` with an error object and confirm the client stops.
- Return `succeeded` without audio and confirm the client rejects it.
- Return an audio URL over plain HTTP and confirm the client rejects it.
- Return repeated `processing` fixtures and confirm the configured read bound ends the test.
- Return an unknown status and confirm the client does not guess.
The test suite also accepts `completed` and rejects `canceled` when an adapter normalizes task names that way, matching the guidance in the task reference. Keep the provider’s native states and any adapter states explicit so a new string cannot become an infinite loop.
Bound every request. Node.js exposes `fetch` and `AbortSignal.timeout` as globals in its current global API. A timeout is an observation about the client request, not evidence that a generation POST was rejected by the server.
Preserve error classes instead of flattening them
The current Sonilo errors guide returns JSON error codes and messages with HTTP status. The local suite covers the branches that change the next application action:
| Response | Contract-test expectation | Application action |
|---|---|---|
| 401 | Preserve `auth_invalid` | Stop; repair the server credential |
| 402 | Preserve `payment_required` | Stop; require an account or budget decision |
| 422 | Preserve the input error | Stop; repair the source or request fields |
| 429 | Read `Retry-After` | Delay a bounded task read; do not create a second job |
| Timeout during POST | Classify as ambiguous | Reconcile; do not automatically replay the generation |
The tested parser accepts both delta-seconds and HTTP-date forms of `Retry-After`. It does not treat every error as retryable. The rate-limit admission guide covers shared RPM and in-flight generation budgets, while the circuit-breaker guide separates repeated dependency failures from invalid input, throttling, and ambiguous writes.
Keep error evidence safe. The structured logging contract preserves request and task correlation while excluding bearer values, signed input URLs, prompts, filenames, and temporary output capabilities.
Use the live account as a budget gate
Before a live smoke, read the account instead of assuming a free call exists. The List Services reference documents `available_services`, rate and concurrency limits, upload size, and a per-service trial object on self-serve accounts. Trial allowance is expressed as granted, used, and remaining.
The safe default for automated smoke testing is `requireTrial=true`. Continue only when Video-to-Music is enabled and its current remaining trial count is at least one. If no trial remains, stop before the POST. A team may deliberately permit a billable smoke, but that requires a separate explicit budget decision and the same one-call bound.
Do not use the marketing headline as an account entitlement. Read the authenticated service response at run time. The usage dashboard workflow shows how to reconcile account configuration with dated usage, and the cost estimator adds a conservative pre-submit ceiling when the smoke is allowed to be billable.
Keep `variants_num=1` for the smoke. Use a short, representative video that satisfies current service limits. The smoke should answer whether one current account can create and retrieve one valid output. It should not explore creative variation, load, throughput, pricing accuracy, or quality.
Run one controlled production smoke
Execute the live layer only after all local contract tests pass. The release job should require all of these conditions:
- The base URL is exactly the reviewed production API base.
- The live-generation flag is the exact boolean `true`.
- The API key comes from a server-side secret store.
- Account preflight confirms the service and the selected trial or billable budget.
- The input has passed media and URL-lifetime checks.
- The request fixes `variants_num=1`.
- The task reader has a bounded deadline and no second generation path.
- Success requires a nonempty HTTPS audio result.
Store the task ID as soon as the 202 body is validated. Poll the canonical task path using the bounded async polling workflow. If the runner stops during polling, the graceful shutdown workflow shows how to preserve accepted task IDs for later reconciliation.
After success, download and validate the bytes before the smoke passes. The official custom eval guidance requires a real nonempty output instead of a placeholder file. Move any result the application needs into durable storage using the temporary-output acceptance workflow.
Do not replay an ambiguous smoke POST
A client-side timeout can occur after the server accepted the job but before the client stored its task ID. RFC 9110 section 9.2.2 limits automatic retry of non-idempotent methods unless the client knows the request semantics are idempotent or knows the original request was not applied.
Freeze an ambiguous smoke instead of spending again. Use a durable application request record and reconcile available account or task evidence before a person authorizes another generated result. The duplicate-job guard provides the request-state model for that boundary.
GET task reads are different. They can be retried within the bounded polling policy when the status and error class permit it. A generation POST must never be hidden inside a generic retry wrapper.
Run the tested contract gate
`api-contract-gate-v1` is a dependency-free Node.js module plus a loopback test suite. Run it with `node --test api-contract-gate.test.mjs`. Nineteen deterministic tests passed on Node.js v22.23.2 on August 26, 2026:
- Production generation is blocked before fetch.
- The local mock receives the exact async form contract.
- An exact live approval allows the production destination with injected fetch.
- A malformed 202 response is rejected.
- HTTP 401 preserves `auth_invalid`.
- HTTP 402 preserves `payment_required`.
- HTTP 422 preserves its input classification.
- Processing followed by success returns one validated audio result.
- A normalized `completed` state is accepted.
- A failed terminal task is rejected.
- Success without audio is rejected.
- A non-HTTPS output is rejected.
- `Retry-After` is honored before the next read.
- The polling bound stops repeated processing.
- Both documented `Retry-After` forms are parsed.
- Account service and trial fields are validated.
- A zero-trial smoke stops before generation.
- One mocked smoke completes preflight, submission, and output validation.
- A request timeout keeps its own error class.
These results prove the client policy against controlled local HTTP and JSON fixtures. They do not establish live provider uptime, response time, media quality, billing accuracy, trial eligibility, account permissions, or the behavior of a customer deployment. Recheck the OpenAPI and endpoint references before relying on the fixture set after September 26, 2026.
When not to use Sonilo
Do not use Sonilo for a test that only needs generic HTTP client behavior. A local fake endpoint is sufficient and avoids coupling the test to a media product.
Do not run production smoke tests from every pull request, developer laptop, or untrusted fork. Keep the live job behind a protected release environment with a narrow secret, explicit permission, and a one-call budget.
Do not treat a successful smoke as evidence of creative quality, production availability, exact cost, or fit for every media type. Those require separate playback review, account evidence, load methodology, and business acceptance criteria.
Do not use a generated output as a substitute for unit and contract coverage. A provider success cannot prove that the client handles the failure paths it did not encounter.
Make contract drift fail before customer work
The release decision is clear: keep routine tests on loopback, read production configuration without generating media, and unlock one real smoke only after the client, account, input, and budget gates pass. When the OpenAPI or live response changes, update the fixture deliberately and rerun the full local matrix before customer work reaches the new contract.
Read the current API reference before adopting the example. If your platform needs a reviewed integration, capacity plan, or commercial terms, talk to Sonilo.


