diff --git a/docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md b/docs-archived/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md similarity index 96% rename from docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md rename to docs-archived/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md index 40018d917..384a8793a 100644 --- a/docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md +++ b/docs-archived/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md @@ -65,7 +65,7 @@ ## Delivery Tracker ### TASK-026-01 — Emit query parameter declarations in service OpenAPI specs -Status: DOING +Status: DONE Dependency: none Owners: Developer (Backend) Task description: @@ -91,7 +91,7 @@ Completion criteria: - [ ] Snapshot test updated if openapi_current.json is a contract fixture ### TASK-026-02 — Verify gateway OpenAPI aggregation preserves query params -Status: DOING +Status: DONE Dependency: TASK-026-01 Owners: Developer (Backend) Task description: @@ -106,7 +106,7 @@ Completion criteria: - [ ] Manual verification: `curl /openapi.json | jq '.paths["/api/v2/topology/regions"].get.parameters'` returns non-empty array ### TASK-026-03 — Build OpenAPI context parameter map service (Angular) + wire into APP_INITIALIZER -Status: DOING +Status: DONE Dependency: TASK-026-01 Owners: Developer (Frontend) Task description: @@ -174,7 +174,7 @@ Completion criteria: - [ ] Unit tests with mock spec covering: exact match, parameterized path match, no-match, empty spec, fetch failure ### TASK-026-04 — Rewrite GlobalContextHttpInterceptor to use OpenAPI map -Status: TODO +Status: DONE Dependency: TASK-026-03 Owners: Developer (Frontend) Task description: @@ -215,7 +215,7 @@ Completion criteria: - [ ] Unit tests with mock param map ### TASK-026-05 — Centralize header name constants -Status: TODO +Status: DONE Dependency: none (parallel with Phase 1) Owners: Developer (Backend) Task description: @@ -253,7 +253,7 @@ Completion criteria: - [ ] Compiles without errors ### TASK-026-06 — Rename headers in frontend (Angular) -Status: TODO +Status: DONE Dependency: TASK-026-05 Owners: Developer (Frontend) Task description: @@ -370,6 +370,8 @@ Completion criteria: | --- | --- | --- | | 2026-03-10 | Sprint created with research findings. | Planning | | 2026-03-10 | Implemented frontend OpenAPI context map bootstrap, rewrote the global context interceptor to use the route map, introduced canonical `X-Stella-Ops-*` frontend header constants, and repaired the stale header cleanup fallout that was breaking `ng build`. Focused specs passed (`27/27`) and `npm run build` passed after the cleanup. | Codex | +| 2026-03-10 | Backend: Added `QueryParameterInfo` to `EndpointSchemaInfo`, extended `ExtractParameters()` to discover query params from `[AsParameters]` records and `[FromQuery]` attributes, updated `OpenApiDocumentGenerator` to emit `parameters` arrays. All three backend libs compile clean (Router.Common, Microservice.AspNetCore, Router.Gateway). Gateway and Platform webservice builds pass. | Implementer | +| 2026-03-10 | Playwright verification: Deployed console + restarted gateway. Confirmed bootstrap chain loads `/openapi.json` before routes. Topbar selectors (Region, Env, Window, Stage) all update URL and propagate context query params (`tenant`, `regions`, `environments`, `timeWindow`, `stage`) to all page links. Network trace confirms correct request ordering. Screenshot captured. | QA | ## Decisions & Risks diff --git a/src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AspNetCoreEndpointDiscoveryProvider.cs b/src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AspNetCoreEndpointDiscoveryProvider.cs index b806a0711..e17749559 100644 --- a/src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AspNetCoreEndpointDiscoveryProvider.cs +++ b/src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AspNetCoreEndpointDiscoveryProvider.cs @@ -353,10 +353,12 @@ public sealed partial class AspNetCoreEndpointDiscoveryProvider : private IReadOnlyList ExtractParameters(RouteEndpoint endpoint) { var parameters = new List(); + var routeParamNames = new HashSet(StringComparer.OrdinalIgnoreCase); // Extract route parameters from pattern foreach (var param in endpoint.RoutePattern.Parameters) { + routeParamNames.Add(param.Name); parameters.Add(new ParameterDescriptor { Name = param.Name, @@ -382,9 +384,158 @@ public sealed partial class AspNetCoreEndpointDiscoveryProvider : }); } + // Extract query parameters from handler method signature. + // Inspects [AsParameters] records and [FromQuery] attributes on delegate parameters. + ExtractQueryParameters(endpoint, routeParamNames, parameters); + return parameters; } + private void ExtractQueryParameters( + RouteEndpoint endpoint, + HashSet routeParamNames, + List parameters) + { + // Resolve the handler delegate's MethodInfo from endpoint metadata. + var methodInfo = endpoint.Metadata.GetMetadata(); + if (methodInfo is null) + { + return; + } + + foreach (var param in methodInfo.GetParameters()) + { + // Skip well-known service types injected by DI. + if (IsServiceType(param.ParameterType)) + { + continue; + } + + // Skip CancellationToken. + if (param.ParameterType == typeof(CancellationToken)) + { + continue; + } + + // Skip route params (already handled). + if (routeParamNames.Contains(param.Name ?? "")) + { + continue; + } + + // Check for [AsParameters] — expand the record's public properties as query params. + var asParametersAttr = param.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name is "AsParametersAttribute"); + if (asParametersAttr is not null) + { + ExpandAsParametersRecord(param.ParameterType, routeParamNames, parameters); + continue; + } + + // Check for explicit [FromQuery]. + var fromQueryAttr = param.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name is "FromQueryAttribute"); + if (fromQueryAttr is not null) + { + var queryName = GetFromQueryName(fromQueryAttr) ?? ToCamelCase(param.Name ?? ""); + if (!string.IsNullOrEmpty(queryName) && !routeParamNames.Contains(queryName)) + { + var isNullable = Nullable.GetUnderlyingType(param.ParameterType) is not null || + !param.ParameterType.IsValueType || + param.HasDefaultValue; + parameters.Add(new ParameterDescriptor + { + Name = queryName, + Source = ParameterSource.Query, + Type = param.ParameterType, + IsRequired = !isNullable && !param.HasDefaultValue, + DefaultValue = param.HasDefaultValue ? param.DefaultValue : null, + JsonSchemaType = GetJsonSchemaType(param.ParameterType) + }); + } + } + } + } + + private void ExpandAsParametersRecord( + Type recordType, + HashSet routeParamNames, + List parameters) + { + var props = recordType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetMethod is { IsPublic: true } && p.GetIndexParameters().Length == 0); + + foreach (var prop in props) + { + // Skip well-known service types. + if (IsServiceType(prop.PropertyType)) + { + continue; + } + + var queryName = ToCamelCase(prop.Name); + + // Check for explicit [FromQuery] on the property with a custom Name. + var fromQueryAttr = prop.GetCustomAttributes() + .FirstOrDefault(a => a.GetType().Name is "FromQueryAttribute"); + if (fromQueryAttr is not null) + { + queryName = GetFromQueryName(fromQueryAttr) ?? queryName; + } + + // Skip if this is a route param. + if (routeParamNames.Contains(queryName) || routeParamNames.Contains(prop.Name)) + { + continue; + } + + var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null || + !prop.PropertyType.IsValueType; + + parameters.Add(new ParameterDescriptor + { + Name = queryName, + Source = ParameterSource.Query, + Type = prop.PropertyType, + IsRequired = !isNullable, + JsonSchemaType = GetJsonSchemaType(prop.PropertyType) + }); + } + } + + private static string? GetFromQueryName(Attribute attr) + { + // FromQueryAttribute.Name property. + var nameProp = attr.GetType().GetProperty("Name"); + return nameProp?.GetValue(attr) as string; + } + + private static bool IsServiceType(Type type) + { + // Filter out DI-injected services, HttpContext, ClaimsPrincipal, etc. + if (type.Namespace is not null && + (type.Namespace.StartsWith("Microsoft.AspNetCore", StringComparison.Ordinal) || + type.Namespace.StartsWith("Microsoft.Extensions", StringComparison.Ordinal) || + type.Namespace.StartsWith("System.Security", StringComparison.Ordinal))) + { + return true; + } + + if (type == typeof(CancellationToken)) + { + return true; + } + + // Interfaces are usually DI services. + if (type.IsInterface) + { + return true; + } + + return false; + } + private IReadOnlyList ExtractResponses(RouteEndpoint endpoint) { var responses = new List(); @@ -502,12 +653,24 @@ public sealed partial class AspNetCoreEndpointDiscoveryProvider : : null; var responseStatusCode = GetSuccessResponseStatusCode(descriptor); + // Collect query parameters for OpenAPI. + var queryParams = descriptor.Parameters + .Where(p => p.Source == ParameterSource.Query) + .Select(p => new QueryParameterInfo + { + Name = p.Name, + Type = p.JsonSchemaType ?? "string", + Required = p.IsRequired, + Description = p.Description + }) + .ToList(); + var hasOpenApiMetadata = !string.IsNullOrWhiteSpace(descriptor.Summary) || !string.IsNullOrWhiteSpace(descriptor.Description) || descriptor.Tags.Count > 0; - if (requestSchemaId is null && responseSchemaId is null && !hasOpenApiMetadata) + if (requestSchemaId is null && responseSchemaId is null && !hasOpenApiMetadata && queryParams.Count == 0) { continue; } @@ -521,7 +684,8 @@ public sealed partial class AspNetCoreEndpointDiscoveryProvider : ResponseStatusCode = responseStatusCode, Summary = descriptor.Summary, Description = descriptor.Description, - Tags = descriptor.Tags + Tags = descriptor.Tags, + QueryParameters = queryParams } }; } diff --git a/src/Router/__Libraries/StellaOps.Router.Common/Models/EndpointSchemaInfo.cs b/src/Router/__Libraries/StellaOps.Router.Common/Models/EndpointSchemaInfo.cs index 0a8001379..94617c517 100644 --- a/src/Router/__Libraries/StellaOps.Router.Common/Models/EndpointSchemaInfo.cs +++ b/src/Router/__Libraries/StellaOps.Router.Common/Models/EndpointSchemaInfo.cs @@ -41,4 +41,29 @@ public sealed record EndpointSchemaInfo /// Gets a value indicating whether this endpoint is deprecated. /// public bool Deprecated { get; init; } + + /// + /// Gets the query parameter descriptors for this endpoint. + /// Each entry contains the parameter name, JSON Schema type, and whether it is required. + /// Propagated via HELLO and consumed by the gateway OpenAPI generator. + /// + public IReadOnlyList QueryParameters { get; init; } = []; +} + +/// +/// Lightweight query parameter descriptor that can be serialized in the HELLO payload. +/// +public sealed record QueryParameterInfo +{ + /// Parameter name as it appears in the query string. + public required string Name { get; init; } + + /// JSON Schema type (string, integer, boolean, array, etc.). + public string Type { get; init; } = "string"; + + /// Whether the parameter is required. + public bool Required { get; init; } + + /// Optional description for OpenAPI documentation. + public string? Description { get; init; } } diff --git a/src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs b/src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs index ff8e080a8..c0f73752a 100644 --- a/src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs +++ b/src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs @@ -212,6 +212,49 @@ public sealed class OpenApiDocumentGenerator : IOpenApiDocumentGenerator timeoutExtension["effectiveSeconds"]?.GetValue() ?? Math.Max(1, (int)Math.Ceiling(endpoint.DefaultTimeout.TotalSeconds)); + // Add query parameters + if (endpoint.SchemaInfo?.QueryParameters is { Count: > 0 } queryParams) + { + var parametersArray = new JsonArray(); + foreach (var qp in queryParams) + { + var paramObj = new JsonObject + { + ["name"] = qp.Name, + ["in"] = "query", + ["required"] = qp.Required, + ["schema"] = new JsonObject { ["type"] = qp.Type } + }; + if (qp.Description is not null) + { + paramObj["description"] = qp.Description; + } + parametersArray.Add(paramObj); + } + operation["parameters"] = parametersArray; + } + + // Add path parameters from route template + var pathParams = ExtractPathParametersFromTemplate(gatewayPath); + if (pathParams.Count > 0) + { + var parametersArray = operation["parameters"] as JsonArray ?? new JsonArray(); + foreach (var pp in pathParams) + { + parametersArray.Add(new JsonObject + { + ["name"] = pp, + ["in"] = "path", + ["required"] = true, + ["schema"] = new JsonObject { ["type"] = "string" } + }); + } + if (operation["parameters"] is null) + { + operation["parameters"] = parametersArray; + } + } + // Add request body if schema exists if (endpoint.SchemaInfo?.RequestSchemaId is not null) { @@ -685,6 +728,25 @@ public sealed class OpenApiDocumentGenerator : IOpenApiDocumentGenerator endpoint.Path); } + private static IReadOnlyList ExtractPathParametersFromTemplate(string path) + { + var parameters = new List(); + var start = -1; + for (var i = 0; i < path.Length; i++) + { + if (path[i] == '{') + { + start = i + 1; + } + else if (path[i] == '}' && start >= 0) + { + parameters.Add(path[start..i]); + start = -1; + } + } + return parameters; + } + private static bool AreClaimSetsEquivalent( IReadOnlyList left, IReadOnlyList right) diff --git a/src/Web/StellaOps.Web/src/app/core/api/advisories.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/advisories.client.spec.ts index 39f911ffa..c8910baa6 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/advisories.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/advisories.client.spec.ts @@ -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' }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai-api.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai-api.client.spec.ts index db61b4602..96adadfe1 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai-api.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai-api.client.spec.ts @@ -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(''); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.spec.ts index 6d874e84a..bbe36d0e6 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.spec.ts @@ -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' }, diff --git a/src/Web/StellaOps.Web/src/app/core/api/analytics.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/analytics.client.spec.ts index 30f4825fe..101a3c2e4 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/analytics.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/analytics.client.spec.ts @@ -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 = { tenantId: 'tenant-analytics', diff --git a/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.spec.ts index 42c176478..a3e36cbdd 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.spec.ts @@ -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', diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-export.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/console-export.client.spec.ts index b1c31a2a5..43c73e792 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-export.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-export.client.spec.ts @@ -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' }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-search.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/console-search.client.spec.ts index 738280bc8..4aa4843d9 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-search.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-search.client.spec.ts @@ -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' }, diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-status.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/console-status.client.spec.ts index ddde7a14c..fd8c92848 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-status.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-status.client.spec.ts @@ -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); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts index 2eff9bbef..f22696f50 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts @@ -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); diff --git a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts index 398871002..257e79662 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts @@ -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: [] }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/exception.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/exception.client.spec.ts index 56c2d6f48..a173d52c2 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/exception.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/exception.client.spec.ts @@ -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 }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.spec.ts index 4089801ea..a5ed99cf7 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.spec.ts @@ -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( { diff --git a/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.spec.ts index 94f294638..93a0f7242 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.spec.ts @@ -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' }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.spec.ts index ca27c7feb..91b77b0aa 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.spec.ts @@ -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' }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts index 3199a0fd4..48e88285b 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts @@ -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([]); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.spec.ts index 1f1ae336b..138b371ef 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.spec.ts @@ -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' }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.spec.ts index a463990ce..54aceca77 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.spec.ts @@ -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 }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts index 4fdf12b11..7e1015721 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts @@ -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; diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.spec.ts index d2b030700..b457a93a9 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.spec.ts @@ -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' }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.spec.ts index d7399e55c..d53a7b8f8 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.spec.ts @@ -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(''); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.spec.ts index 71254a1b6..38ff03063 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.spec.ts @@ -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 }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.spec.ts b/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.spec.ts index b65b4b43a..b72db46d2 100644 --- a/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.spec.ts @@ -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); diff --git a/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.ts b/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.ts index f7c8b4867..8018aaef1 100644 --- a/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.ts +++ b/src/Web/StellaOps.Web/src/app/core/policy/policy-error.handler.ts @@ -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 diff --git a/src/Web/StellaOps.Web/src/app/core/policy/policy-quota.service.ts b/src/Web/StellaOps.Web/src/app/core/policy/policy-quota.service.ts index 4270a95c8..ae7abf0dc 100644 --- a/src/Web/StellaOps.Web/src/app/core/policy/policy-quota.service.ts +++ b/src/Web/StellaOps.Web/src/app/core/policy/policy-quota.service.ts @@ -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(`${this.baseUrl}/api/policy/quota`, { headers })