OpenAPI query param discovery and header cleanup completion

Backend: ExtractParameters() now discovers query params from [AsParameters]
records and [FromQuery] attributes via handler method reflection. Gateway
OpenApiDocumentGenerator emits parameters arrays in the aggregated spec.
QueryParameterInfo added to EndpointSchemaInfo for HELLO payload transport.

Frontend: Remaining spec files and straggler services updated to canonical
X-Stella-Ops-* header names. Sprint 026 archived (tasks 01-06 DONE,
07-09 TODO for backend service rename pass).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-10 17:13:58 +02:00
parent 8578065675
commit 8a1fb9bd9b
28 changed files with 349 additions and 94 deletions

View File

@@ -53,10 +53,10 @@ describe('AdvisoryApiHttpClient', () => {
const req = httpMock.expectOne((r) => r.url === '/api/advisories' && r.params.get('search') === 'demo');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('If-None-Match')).toBe('"etag-1"');
req.flush({ items: [], count: 0, continuationToken: null, etag: '"etag-2"', traceId: 'trace-1' });
});

View File

@@ -150,7 +150,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent()!.args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-Stella-Trace-Id')).toBe('custom-trace-123');
expect(headers.get('X-Stella-Ops-Trace-Id')).toBe('custom-trace-123');
});
});
@@ -347,7 +347,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent()!.args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-StellaOps-Tenant')).toBe('tenant-xyz');
expect(headers.get('X-Stella-Ops-Tenant')).toBe('tenant-xyz');
});
it('should include trace ID header', () => {
@@ -357,7 +357,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent()!.args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-Stella-Trace-Id')).toBeTruthy();
expect(headers.get('X-Stella-Ops-Trace-Id')).toBeTruthy();
});
it('should include request ID header', () => {
@@ -367,7 +367,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent()!.args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-Stella-Request-Id')).toBeTruthy();
expect(headers.get('X-Stella-Ops-Request-Id')).toBeTruthy();
});
it('should include Accept header', () => {
@@ -388,7 +388,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent()!.args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-StellaOps-Tenant')).toBe('');
expect(headers.get('X-Stella-Ops-Tenant')).toBe('');
});
});

View File

@@ -44,8 +44,8 @@ describe('AdvisoryAiApiHttpClient', () => {
const req = httpMock.expectOne('/api/v1/advisory-ai/explain');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.body).toEqual(request);
req.flush({
@@ -67,8 +67,8 @@ describe('AdvisoryAiApiHttpClient', () => {
const req = httpMock.expectOne('/api/v1/advisory-ai/remediate');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-2');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-2');
expect(req.request.body).toEqual(request);
req.flush({
@@ -93,8 +93,8 @@ describe('AdvisoryAiApiHttpClient', () => {
const req = httpMock.expectOne('/api/v1/advisory-ai/justify');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-3');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-3');
req.flush({
justificationId: 'justify-1',
@@ -114,7 +114,7 @@ describe('AdvisoryAiApiHttpClient', () => {
const req = httpMock.expectOne('/api/v1/advisory-ai/rate-limits');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-default');
req.flush([
{ feature: 'explain', limit: 10, remaining: 8, resetsAt: '2025-01-15T00:00:00Z' },

View File

@@ -41,9 +41,9 @@ describe('AnalyticsHttpClient', () => {
r.params.get('environment') === 'prod'
);
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-analytics');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-123');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-123');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-analytics');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-123');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-123');
const response: PlatformListResponse<unknown> = {
tenantId: 'tenant-analytics',

View File

@@ -105,7 +105,7 @@ describe('AuditBundlesHttpClient', () => {
);
expect(request.request.headers.get('Authorization')).toBe('DPoP access-token');
expect(request.request.headers.get('DPoP')).toBe('proof-token');
expect(request.request.headers.get('X-Stella-Tenant')).toBe('demo-prod');
expect(request.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
request.flush({
bundleId: 'bndl-0001',
status: 'queued',

View File

@@ -54,8 +54,8 @@ describe('ConsoleExportClient', () => {
const req = httpMock.expectOne('/console/exports');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-default');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('Idempotency-Key')).toBe('abc');
req.flush({ exportId: 'exp-1', status: 'queued' });
});
@@ -65,8 +65,8 @@ describe('ConsoleExportClient', () => {
const req = httpMock.expectOne('/console/exports/exp-1');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-xyz');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-2');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-xyz');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-2');
req.flush({ exportId: 'exp-1', status: 'running' });
});
});

View File

@@ -47,9 +47,9 @@ describe('ConsoleSearchHttpClient', () => {
const req = httpMock.expectOne((r) => r.url === '/api/search' && r.params.get('query') === 'jwt');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
req.flush({
items: [],
ranking: { sortKeys: [], payloadHash: 'sha256:test' },
@@ -65,8 +65,8 @@ describe('ConsoleSearchHttpClient', () => {
client.search({ traceId: 'trace-2' }).subscribe();
const req = httpMock.expectOne('/api/search');
expect(req.request.headers.has('X-StellaOps-Tenant')).toBeFalse();
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-2');
expect(req.request.headers.has('X-Stella-Ops-Tenant')).toBeFalse();
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-2');
req.flush({
items: [],
ranking: { sortKeys: [], payloadHash: 'sha256:test2' },

View File

@@ -83,9 +83,9 @@ describe('ConsoleStatusClient', () => {
const req = httpMock.expectOne('/api/console/status');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-dev');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBeTruthy();
expect(req.request.headers.get('X-Stella-Request-Id')).toBeTruthy();
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-dev');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBeTruthy();
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBeTruthy();
req.flush(sample);
});

View File

@@ -77,8 +77,8 @@ describe('CvssClient', () => {
const req = httpMock.expectOne('/api/cvss/receipts/rcpt-1');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-Stella-Tenant')).toBe('tenant-123');
expect(req.request.headers.has('X-Stella-Trace-Id')).toBeTrue();
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-123');
expect(req.request.headers.has('X-Stella-Ops-Trace-Id')).toBeTrue();
req.flush(dto);
expect(receipt?.score.overall).toBe(9.1);

View File

@@ -42,8 +42,8 @@ describe('EvidencePackHttpClient', () => {
const request = httpMock.expectOne((pending) => pending.url === '/v1/evidence-packs');
expect(request.request.method).toBe('GET');
expect(request.request.params.get('runId')).toBe('run-42');
expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod');
expect(request.request.headers.get('X-Stella-Trace-Id')).toBe('trace-ep-42');
expect(request.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
expect(request.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-ep-42');
request.flush({ count: 0, packs: [] });
});
});

View File

@@ -45,10 +45,10 @@ describe('ExceptionApiHttpClient', () => {
const req = httpMock.expectOne((r) => r.url === '/api/exceptions' && r.params.get('status') === 'approved');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
req.flush({ items: [], count: 0, continuationToken: null });
});

View File

@@ -58,7 +58,7 @@ describe('FirstSignalHttpClient', () => {
const req = httpMock.expectOne('/api/console/runs/run%3A%3Atenant-dev%3A%3A20260309/first-signal');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-dev');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-dev');
expect(req.request.headers.get('If-None-Match')).toBe('"compat-prev"');
req.flush(
{

View File

@@ -64,10 +64,10 @@ describe('JobEngineControlHttpClient', () => {
expect(req.request.params.get('paused')).toBe('false');
expect(req.request.params.get('limit')).toBe('25');
expect(req.request.params.get('continuationToken')).toBe('cursor-1');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('If-None-Match')).toBe('"etag-1"');
req.flush({ items: [], count: 0, continuationToken: null, etag: '"etag-2"', traceId: 'trace-1' });
});
@@ -82,8 +82,8 @@ describe('JobEngineControlHttpClient', () => {
const req = httpMock.expectOne('/api/jobengine/quotas');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Require-Operator')).toBe('1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Require-Operator')).toBe('1');
req.flush({
quotaId: 'q-1',
tenantId: 'tenant-x',
@@ -109,8 +109,8 @@ describe('JobEngineControlHttpClient', () => {
const req = httpMock.expectOne('/api/jobengine/deadletter/entry-1/replay');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Require-Operator')).toBe('1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Require-Operator')).toBe('1');
req.flush({ success: true, newJobId: 'job-1', errorMessage: null, updatedEntry: null, traceId: 'trace-3' });
});
@@ -144,8 +144,8 @@ describe('JobEngineControlHttpClient', () => {
client.listQuotas({ traceId: 'trace-6' }).subscribe();
const req = httpMock.expectOne('/api/jobengine/quotas');
expect(req.request.headers.has('X-StellaOps-Tenant')).toBeFalse();
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-6');
expect(req.request.headers.has('X-Stella-Ops-Tenant')).toBeFalse();
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-6');
req.flush({ items: [], count: 0, continuationToken: null, etag: '"etag-3"', traceId: 'trace-6' });
});
});

View File

@@ -54,10 +54,10 @@ describe('OrchestratorHttpClient', () => {
const req = httpMock.expectOne((r) => r.url === '/api/jobengine/sources' && r.params.get('sourceType') === 'concelier');
expect(req.request.method).toBe('GET');
expect(req.request.params.get('enabled')).toBe('true');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('If-None-Match')).toBe('"etag-1"');
req.flush({ items: [], count: 0, continuationToken: null, etag: '"etag-2"', traceId: 'trace-1' });
});

View File

@@ -52,7 +52,7 @@ describe('NotifyApiHttpClient', () => {
client.listChannels().subscribe();
const request = httpMock.expectOne('/api/v1/notify/channels');
expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod');
expect(request.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
request.flush([]);
});
@@ -62,7 +62,7 @@ describe('NotifyApiHttpClient', () => {
client.listRules().subscribe();
const request = httpMock.expectOne('/api/v1/notify/rules');
expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-explicit');
expect(request.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-explicit');
request.flush([]);
});
});

View File

@@ -48,10 +48,10 @@ describe('PolicyExceptionsHttpClient', () => {
const req = httpMock.expectOne('/api/policy/effective');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
req.flush({ policyVersion: 'sha256:test', items: [], continuationToken: null, traceId: 'trace-1' });
});

View File

@@ -43,8 +43,8 @@ describe('HttpPolicyGovernanceApi', () => {
const req = httpMock.expectOne((request) => request.url === '/api/v1/governance/trust-weights');
expect(req.request.params.get('tenantId')).toBe('demo-prod');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod');
expect(req.request.headers.get('X-Stella-Tenant')).toBe('demo-prod');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
req.flush({ tenantId: 'demo-prod', projectId: null, weights: [], defaultWeight: 1, modifiedAt: '2026-03-09T00:00:00Z' });
});
@@ -54,7 +54,7 @@ describe('HttpPolicyGovernanceApi', () => {
const req = httpMock.expectOne((request) => request.url === '/api/v1/governance/staleness/config');
expect(req.request.params.get('tenantId')).toBe('tenant-blue');
expect(req.request.params.get('projectId')).toBe('proj-a');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-blue');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-blue');
req.flush({ tenantId: 'tenant-blue', projectId: 'proj-a', configs: [], modifiedAt: '2026-03-09T00:00:00Z', etag: '"staleness"' });
});
@@ -63,7 +63,7 @@ describe('HttpPolicyGovernanceApi', () => {
const req = httpMock.expectOne((request) => request.url === '/api/v1/governance/audit/events');
expect(req.request.params.get('tenantId')).toBe('demo-prod');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
req.flush({ events: [], total: 0, page: 1, pageSize: 20, hasMore: false });
});
});

View File

@@ -70,7 +70,7 @@ describe('PolicySimulationHttpClient', () => {
const promise = firstValueFrom(httpClient.getShadowModeConfig());
const req = httpMock.expectOne(`${baseUrl}/policy/shadow/config`);
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-001');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-001');
req.flush(mockConfig);
const result = await promise;
@@ -732,7 +732,7 @@ describe('PolicySimulationHttpClient', () => {
const promise = firstValueFrom(httpClient.getShadowModeConfig({ tenantId: customTenant }));
const req = httpMock.expectOne(`${baseUrl}/policy/shadow/config`);
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe(customTenant);
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe(customTenant);
req.flush({ enabled: false, status: 'disabled' });
await promise;
@@ -749,7 +749,7 @@ describe('PolicySimulationHttpClient', () => {
}),
);
const req = httpMock.expectOne((request) => request.url === `${baseUrl}/policy/simulations/history`);
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('demo-prod');
req.flush({ items: [], total: 0, hasMore: false });
await promise;

View File

@@ -53,10 +53,10 @@ describe('VexEvidenceHttpClient', () => {
const req = httpMock.expectOne((r) => r.url === '/api/vex/statements' && r.params.get('vulnId') === 'CVE-2024-12345');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-x');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-1');
expect(req.request.headers.get('X-Stella-Ops-Trace-Id')).toBe('trace-1');
expect(req.request.headers.get('X-Stella-Ops-Request-Id')).toBe('trace-1');
expect(req.request.headers.get('If-None-Match')).toBe('"etag-1"');
req.flush({ items: [], count: 0, continuationToken: null, etag: '"etag-2"', traceId: 'trace-1' });
});

View File

@@ -193,7 +193,7 @@ describe('VexHubApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent().args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-Stella-Trace-Id')).toBe('custom-trace-123');
expect(headers.get('X-Stella-Ops-Trace-Id')).toBe('custom-trace-123');
});
});
@@ -474,7 +474,7 @@ describe('VexHubApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent().args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-StellaOps-Tenant')).toBe('tenant-abc');
expect(headers.get('X-Stella-Ops-Tenant')).toBe('tenant-abc');
});
it('should include trace ID header', () => {
@@ -484,7 +484,7 @@ describe('VexHubApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent().args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-Stella-Trace-Id')).toBeTruthy();
expect(headers.get('X-Stella-Ops-Trace-Id')).toBeTruthy();
});
it('should include Accept header', () => {
@@ -505,7 +505,7 @@ describe('VexHubApiHttpClient', () => {
const callArgs = httpClientSpy.get.calls.mostRecent().args;
const headers = callArgs[1]!.headers as HttpHeaders;
expect(headers.get('X-StellaOps-Tenant')).toBe('');
expect(headers.get('X-Stella-Ops-Tenant')).toBe('');
});
});

View File

@@ -52,8 +52,8 @@ describe('VulnerabilityHttpClient', () => {
});
const req = httpMock.expectOne('https://api.example.local/vuln?page=1&pageSize=5');
expect(req.request.headers.get('X-Stella-Tenant')).toBe('tenant-dev');
expect(req.request.headers.has('X-Stella-Trace-Id')).toBeTrue();
expect(req.request.headers.get('X-Stella-Ops-Tenant')).toBe('tenant-dev');
expect(req.request.headers.has('X-Stella-Ops-Trace-Id')).toBeTrue();
req.flush(stub);
});
@@ -61,7 +61,7 @@ describe('VulnerabilityHttpClient', () => {
client.listVulnerabilities({ page: 1, projectId: 'proj-ops' }).subscribe();
const req = httpMock.expectOne('https://api.example.local/vuln?page=1');
expect(req.request.headers.get('X-Stella-Project')).toBe('proj-ops');
expect(req.request.headers.get('X-Stella-Ops-Project')).toBe('proj-ops');
req.flush({ items: [], total: 0, page: 1, pageSize: 20 });
});
});

View File

@@ -269,22 +269,22 @@ describe('parsePolicyError', () => {
});
describe('trace ID extraction', () => {
it('should extract X-Stella-Trace-Id header', () => {
it('should extract X-Stella-Ops-Trace-Id header', () => {
const response = createErrorResponse(
500,
{},
{ 'X-Stella-Trace-Id': 'stella-trace-123' }
{ 'X-Stella-Ops-Trace-Id': 'stella-trace-123' }
);
const error = parsePolicyError(response);
expect(error.traceId).toBe('stella-trace-123');
});
it('should fall back to X-Request-Id header', () => {
it('should fall back to X-Stella-Ops-Request-Id header', () => {
const response = createErrorResponse(
500,
{},
{ 'X-Request-Id': 'request-456' }
{ 'X-Stella-Ops-Request-Id': 'request-456' }
);
const error = parsePolicyError(response);

View File

@@ -1,4 +1,5 @@
import { HttpErrorResponse } from '@angular/common/http';
import { StellaOpsHeaders } from '../http/stella-ops-headers';
import {
PolicyError,
PolicyErrorCode,
@@ -177,8 +178,8 @@ export function parsePolicyError(response: HttpErrorResponse): PolicyApiError {
// Extract trace ID from headers
const traceId =
response.headers?.get('X-Stella-Trace-Id') ??
response.headers?.get('X-Request-Id') ??
response.headers?.get(StellaOpsHeaders.TraceId) ??
response.headers?.get(StellaOpsHeaders.RequestId) ??
(body?.traceId as string | undefined);
// Get error code

View File

@@ -4,6 +4,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject, timer, of, catchError, map, tap } from 'rxjs';
import { AppConfigService } from '../config/app-config.service';
import { StellaOpsHeaders } from '../http/stella-ops-headers';
import { ConsoleSessionStore } from '../console/console-session.store';
import { QuotaInfo, RateLimitInfo } from '../api/policy-engine.models';
@@ -188,7 +189,7 @@ export class PolicyQuotaService {
* Load quota info from server.
*/
refreshQuotaInfo(): void {
const headers = new HttpHeaders().set('X-Tenant-Id', this.tenantId);
const headers = new HttpHeaders().set(StellaOpsHeaders.Tenant, this.tenantId);
this.http
.get<QuotaInfo>(`${this.baseUrl}/api/policy/quota`, { headers })