mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(review HIGH 3): restore statusUrl on RunScenarioResponse + document 202→200 wire break (#3207)
Commit 7 silently shifted /api/scenario/v1/run-scenario's response
contract in two ways that the commit message covered only partially:
1. HTTP 202 Accepted → HTTP 200 OK
2. Dropped `statusUrl` string from the response body
The `statusUrl` drop was mentioned as "unused by SupplyChainPanel" but
not framed as a contract change. The 202 → 200 shift was not mentioned
at all. This is a same-version (v1 → v1) migration, so external callers
that key off either signal — `response.status === 202` or
`response.body.statusUrl` — silently branch incorrectly.
Evaluated options:
(a) sebuf per-RPC status-code config — not available. sebuf's
HttpConfig only models `path` and `method`; no status annotation.
(b) Bump to scenario/v2 — judged heavier than the break itself for
a single status-code shift. No in-repo caller uses 202 or
statusUrl; the docs-level impact is containable.
(c) Accept the break, document explicitly, partially restore.
Took option (c):
- Restored `statusUrl` in the proto (new field `string status_url = 3`
on RunScenarioResponse). Server computes
`/api/scenario/v1/get-scenario-status?jobId=<encoded job_id>` and
populates it on every successful enqueue. External callers that
followed this URL keep working unchanged.
- 202 → 200 is not recoverable inside the sebuf generator, so it is
called out explicitly in two places:
- docs/api-scenarios.mdx now includes a prominent `<Warning>` block
documenting the v1→v1 contract shift + the suggested migration
(branch on response body shape, not HTTP status).
- RunScenarioResponse proto comment explains why 200 is the new
success status on enqueue.
OpenAPI bundle regenerated to reflect the restored statusUrl field.
- Regression test added in tests/scenario-handler.test.mjs pinning
`statusUrl` to the exact URL-encoded shape — locks the invariant so
a future proto rename or handler refactor can't silently drop it
again.
From koala73 review (#3242 second-pass, HIGH new #3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,10 +60,19 @@ Enqueues a job. Returns the assigned `jobId` the caller must poll.
|
||||
```json
|
||||
{
|
||||
"jobId": "scenario:1713456789012:a1b2c3d4",
|
||||
"status": "pending"
|
||||
"status": "pending",
|
||||
"statusUrl": "/api/scenario/v1/get-scenario-status?jobId=scenario%3A1713456789012%3Aa1b2c3d4"
|
||||
}
|
||||
```
|
||||
|
||||
- `statusUrl` — server-computed convenience URL. Callers that don't want to hardcode the status path can follow this directly (it URL-encodes the `jobId`).
|
||||
|
||||
<Warning>
|
||||
**Wire-contract change (v1 → v1)** — the pre-sebuf-migration endpoint returned `202 Accepted` on successful enqueue; the migrated endpoint returns `200 OK`. No per-RPC status-code configuration is available in sebuf's HTTP annotations today, and introducing a `/v2` for a single status-code shift was judged heavier than the break itself.
|
||||
|
||||
If your integration branches on `response.status === 202`, switch to branching on response body shape (`response.body.status === "pending"` indicates enqueue success). `statusUrl` is preserved exactly as before and is a safe signal to key off.
|
||||
</Warning>
|
||||
|
||||
**Errors**:
|
||||
|
||||
| Status | `message` | Cause |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -165,9 +165,24 @@ components:
|
||||
status:
|
||||
type: string
|
||||
description: Always "pending" at enqueue time.
|
||||
statusUrl:
|
||||
type: string
|
||||
description: |-
|
||||
Convenience URL the caller can use to poll this job's status.
|
||||
Server-computed as `/api/scenario/v1/get-scenario-status?jobId=<job_id>`.
|
||||
Restored after the v1 → v1 sebuf migration because external callers
|
||||
may key off this field.
|
||||
description: |-
|
||||
RunScenarioResponse carries the enqueued job id. Clients poll
|
||||
GetScenarioStatus with this id until status != "pending".
|
||||
|
||||
NOTE: the legacy (pre-sebuf) endpoint returned HTTP 202 Accepted on
|
||||
enqueue; the sebuf-generated server emits 200 OK for all successful
|
||||
responses (no per-RPC status-code configuration is available in the
|
||||
current sebuf HTTP annotations). The 202 → 200 shift on a same-version
|
||||
(v1 → v1) migration is called out in docs/api-scenarios.mdx and the
|
||||
OpenAPI bundle; external consumers keying off `response.status === 202`
|
||||
need to branch on response body shape instead.
|
||||
GetScenarioStatusRequest:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -22,9 +22,22 @@ message RunScenarioRequest {
|
||||
|
||||
// RunScenarioResponse carries the enqueued job id. Clients poll
|
||||
// GetScenarioStatus with this id until status != "pending".
|
||||
//
|
||||
// NOTE: the legacy (pre-sebuf) endpoint returned HTTP 202 Accepted on
|
||||
// enqueue; the sebuf-generated server emits 200 OK for all successful
|
||||
// responses (no per-RPC status-code configuration is available in the
|
||||
// current sebuf HTTP annotations). The 202 → 200 shift on a same-version
|
||||
// (v1 → v1) migration is called out in docs/api-scenarios.mdx and the
|
||||
// OpenAPI bundle; external consumers keying off `response.status === 202`
|
||||
// need to branch on response body shape instead.
|
||||
message RunScenarioResponse {
|
||||
// Generated job id of the form `scenario:{epoch_ms}:{8-char-suffix}`.
|
||||
string job_id = 1;
|
||||
// Always "pending" at enqueue time.
|
||||
string status = 2;
|
||||
// Convenience URL the caller can use to poll this job's status.
|
||||
// Server-computed as `/api/scenario/v1/get-scenario-status?jobId=<job_id>`.
|
||||
// Restored after the v1 → v1 sebuf migration because external callers
|
||||
// may key off this field.
|
||||
string status_url = 3;
|
||||
}
|
||||
|
||||
@@ -67,5 +67,13 @@ export async function runScenario(
|
||||
throw new ApiError(502, 'Failed to enqueue scenario job', '');
|
||||
}
|
||||
|
||||
return { jobId, status: 'pending' };
|
||||
// statusUrl is a server-computed convenience URL preserved from the legacy
|
||||
// /api/scenario/v1/run contract so external callers can keep polling via the
|
||||
// response body rather than hardcoding the status path. See the proto comment
|
||||
// on RunScenarioResponse for why this matters on a v1 → v1 migration.
|
||||
return {
|
||||
jobId,
|
||||
status: 'pending',
|
||||
statusUrl: `/api/scenario/v1/get-scenario-status?jobId=${encodeURIComponent(jobId)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface RunScenarioRequest {
|
||||
export interface RunScenarioResponse {
|
||||
jobId: string;
|
||||
status: string;
|
||||
statusUrl: string;
|
||||
}
|
||||
|
||||
export interface GetScenarioStatusRequest {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface RunScenarioRequest {
|
||||
export interface RunScenarioResponse {
|
||||
jobId: string;
|
||||
status: string;
|
||||
statusUrl: string;
|
||||
}
|
||||
|
||||
export interface GetScenarioStatusRequest {
|
||||
|
||||
@@ -105,6 +105,14 @@ describe('ScenarioService handlers', () => {
|
||||
const res = await runScenario(proCtx(), { scenarioId: 'taiwan-strait-full-closure', iso2: '' });
|
||||
assert.match(res.jobId, /^scenario:\d{13}:[a-z0-9]{8}$/);
|
||||
assert.equal(res.status, 'pending');
|
||||
// statusUrl preserved from the legacy v1 contract — server-computed,
|
||||
// URL-encoded jobId, safe for callers to follow directly. Locked in
|
||||
// because sebuf's 200-only convention breaks the 202/202-body pairing
|
||||
// from the pre-migration contract, and statusUrl is the safe alternative.
|
||||
assert.equal(
|
||||
res.statusUrl,
|
||||
`/api/scenario/v1/get-scenario-status?jobId=${encodeURIComponent(res.jobId)}`,
|
||||
);
|
||||
const pushCall = calls.find((c) => String(c.body).includes('RPUSH'));
|
||||
assert.ok(pushCall, 'RPUSH pipeline must be dispatched');
|
||||
const pushed = JSON.parse(pushCall.body);
|
||||
|
||||
Reference in New Issue
Block a user