Align live console and policy governance clients

This commit is contained in:
master
2026-03-10 01:37:42 +02:00
parent afb9711e61
commit 18246cd74c
14 changed files with 301 additions and 81 deletions

View File

@@ -645,7 +645,7 @@ export const appConfig: ApplicationConfig = {
deps: [AppConfigService],
useFactory: (config: AppConfigService) => {
const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority;
return resolveApiBaseUrl(gatewayBase, '/console');
return resolveApiBaseUrl(gatewayBase, '/api/console');
},
},
{

View File

@@ -0,0 +1,36 @@
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AuditLogClient } from './audit-log.client';
describe('AuditLogClient', () => {
let client: AuditLogClient;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuditLogClient,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
});
client = TestBed.inject(AuditLogClient);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('routes policy audit requests to the governance audit endpoint', () => {
client.getPolicyAudit(undefined, undefined, 50).subscribe();
const req = httpMock.expectOne((request) => request.url === '/api/v1/governance/audit/events');
expect(req.request.method).toBe('GET');
expect(req.request.params.get('limit')).toBe('50');
req.flush({ items: [], cursor: null, hasMore: false });
});
});

View File

@@ -23,7 +23,7 @@ export class AuditLogClient {
// Endpoint paths for each module's audit API
private readonly endpoints: Record<AuditModule, string> = {
authority: '/console/admin/audit',
policy: '/api/v1/policy/audit/events',
policy: '/api/v1/governance/audit/events',
jobengine: '/api/v1/jobengine/audit/events',
integrations: '/api/v1/integrations/audit/events',
vex: '/api/v1/vex/audit/events',

View File

@@ -48,7 +48,7 @@ describe('ConsoleStatusClient', () => {
imports: [],
providers: [
ConsoleStatusClient,
{ provide: CONSOLE_API_BASE_URL, useValue: '/console' },
{ provide: CONSOLE_API_BASE_URL, useValue: '/api/console' },
{ provide: AuthSessionStore, useClass: FakeAuthSessionStore },
{ provide: EVENT_SOURCE_FACTORY, useValue: eventSourceFactory },
provideHttpClient(withInterceptorsFromDi()),
@@ -81,7 +81,7 @@ describe('ConsoleStatusClient', () => {
expect(result.healthy).toBeTrue();
});
const req = httpMock.expectOne('/console/status');
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();
@@ -95,7 +95,7 @@ describe('ConsoleStatusClient', () => {
expect(eventSourceFactory).toHaveBeenCalled();
const url = eventSourceFactory.calls.mostRecent()!.args[0];
expect(url).toContain('/console/runs/run-123/stream?tenant=tenant-dev');
expect(url).toContain('/api/console/runs/run-123/stream?tenant=tenant-dev');
expect(url).toContain('traceId=');
// Simulate incoming message

View File

@@ -0,0 +1,69 @@
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AuthSessionStore } from '../auth/auth-session.store';
import { TenantActivationService } from '../auth/tenant-activation.service';
import { HttpPolicyGovernanceApi } from './policy-governance.client';
describe('HttpPolicyGovernanceApi', () => {
let api: HttpPolicyGovernanceApi;
let httpMock: HttpTestingController;
let authSession: { getActiveTenantId: jasmine.Spy };
const tenantServiceStub = {
activeTenantId: () => 'demo-prod',
};
beforeEach(() => {
authSession = {
getActiveTenantId: jasmine.createSpy('getActiveTenantId'),
};
authSession.getActiveTenantId.and.returnValue('demo-prod');
TestBed.configureTestingModule({
providers: [
HttpPolicyGovernanceApi,
{ provide: TenantActivationService, useValue: tenantServiceStub },
{ provide: AuthSessionStore, useValue: authSession },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
});
api = TestBed.inject(HttpPolicyGovernanceApi);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('replaces the legacy acme tenant placeholder with the active tenant for scoped queries', () => {
api.getTrustWeightConfig({ tenantId: 'acme-tenant', projectId: '' }).subscribe();
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');
req.flush({ tenantId: 'demo-prod', projectId: null, weights: [], defaultWeight: 1, modifiedAt: '2026-03-09T00:00:00Z' });
});
it('preserves explicit non-placeholder tenant ids', () => {
api.getStalenessConfig({ tenantId: 'tenant-blue', projectId: 'proj-a' }).subscribe();
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');
req.flush({ tenantId: 'tenant-blue', projectId: 'proj-a', configs: [], modifiedAt: '2026-03-09T00:00:00Z', etag: '"staleness"' });
});
it('uses the governance audit endpoint with resolved tenant context', () => {
api.getAuditEvents({ tenantId: 'acme-tenant', page: 1, pageSize: 20, sortOrder: 'desc' }).subscribe();
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');
req.flush({ events: [], total: 0, page: 1, pageSize: 20, hasMore: false });
});
});

View File

@@ -1,6 +1,8 @@
import { Injectable, InjectionToken, inject } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, delay, of, throwError } from 'rxjs';
import { AuthSessionStore } from '../auth/auth-session.store';
import { TenantActivationService } from '../auth/tenant-activation.service';
import {
RiskBudgetGovernance,
@@ -89,6 +91,14 @@ export interface PolicyGovernanceApi {
export const POLICY_GOVERNANCE_API = new InjectionToken<PolicyGovernanceApi>('POLICY_GOVERNANCE_API');
const LEGACY_POLICY_TENANT_PLACEHOLDERS = new Set(['acme-tenant']);
interface GovernanceScopeRequest {
tenantId?: string;
projectId?: string;
traceId?: string;
}
// ============================================================================
// Mock Data
// ============================================================================
@@ -903,177 +913,199 @@ export class MockPolicyGovernanceApi implements PolicyGovernanceApi {
@Injectable({ providedIn: 'root' })
export class HttpPolicyGovernanceApi implements PolicyGovernanceApi {
private readonly http = inject(HttpClient);
private readonly authSession = inject(AuthSessionStore);
private readonly tenantService = inject(TenantActivationService);
private readonly baseUrl = '/api/v1/governance';
private buildHeaders(traceId?: string): HttpHeaders {
private buildHeaders(options: GovernanceScopeRequest = {}): HttpHeaders {
let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
const traceId = options.traceId?.trim();
if (traceId) {
headers = headers.set('X-Trace-Id', traceId);
headers = headers
.set('X-Trace-Id', traceId)
.set('X-Stella-Trace-Id', traceId)
.set('X-Stella-Request-Id', traceId);
}
const tenantId = this.resolveTenantId(options.tenantId);
if (tenantId) {
headers = headers
.set('X-StellaOps-Tenant', tenantId)
.set('X-Stella-Tenant', tenantId)
.set('X-Tenant-Id', tenantId);
}
return headers;
}
private buildScopeParams(options: GovernanceScopeRequest): HttpParams {
return new HttpParams()
.set('tenantId', this.resolveTenantId(options.tenantId))
.set('projectId', options.projectId || '');
}
private resolveTenantId(requestedTenantId?: string): string {
const requestedTenant = requestedTenantId?.trim() ?? '';
const activeTenant =
this.tenantService.activeTenantId()?.trim() ??
this.authSession.getActiveTenantId()?.trim() ??
'';
if (requestedTenant && !(activeTenant && LEGACY_POLICY_TENANT_PLACEHOLDERS.has(requestedTenant.toLowerCase()))) {
return requestedTenant;
}
return activeTenant || requestedTenant;
}
// Risk Budget
getRiskBudgetDashboard(options: GovernanceQueryOptions): Observable<RiskBudgetDashboard> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<RiskBudgetDashboard>(`${this.baseUrl}/risk-budget/dashboard`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
updateRiskBudgetConfig(config: RiskBudgetGovernance, options: GovernanceQueryOptions): Observable<RiskBudgetGovernance> {
return this.http.put<RiskBudgetGovernance>(`${this.baseUrl}/risk-budget/config`, config, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
acknowledgeAlert(alertId: string, options: GovernanceQueryOptions): Observable<RiskBudgetAlert> {
return this.http.post<RiskBudgetAlert>(`${this.baseUrl}/risk-budget/alerts/${alertId}/acknowledge`, {}, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
// Trust Weights
getTrustWeightConfig(options: GovernanceQueryOptions): Observable<TrustWeightConfig> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<TrustWeightConfig>(`${this.baseUrl}/trust-weights`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
updateTrustWeight(weight: TrustWeight, options: GovernanceQueryOptions): Observable<TrustWeight> {
return this.http.put<TrustWeight>(`${this.baseUrl}/trust-weights/${weight.id}`, weight, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
deleteTrustWeight(weightId: string, options: GovernanceQueryOptions): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/trust-weights/${weightId}`, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
previewTrustWeightImpact(weights: TrustWeight[], options: GovernanceQueryOptions): Observable<TrustWeightImpact> {
return this.http.post<TrustWeightImpact>(`${this.baseUrl}/trust-weights/preview-impact`, { weights }, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
// Staleness
getStalenessConfig(options: GovernanceQueryOptions): Observable<StalenessConfigContainer> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<StalenessConfigContainer>(`${this.baseUrl}/staleness/config`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
updateStalenessConfig(config: StalenessConfig, options: GovernanceQueryOptions): Observable<StalenessConfig> {
return this.http.put<StalenessConfig>(`${this.baseUrl}/staleness/config/${config.dataType}`, config, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
getStalenessStatus(options: GovernanceQueryOptions): Observable<StalenessStatus[]> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<StalenessStatus[]>(`${this.baseUrl}/staleness/status`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
// Sealed Mode
getSealedModeStatus(options: GovernanceQueryOptions): Observable<SealedModeStatus> {
return this.http.get<SealedModeStatus>(`${this.baseUrl}/sealed-mode/status`, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
getSealedModeOverrides(options: GovernanceQueryOptions): Observable<SealedModeOverride[]> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<SealedModeOverride[]>(`${this.baseUrl}/sealed-mode/overrides`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
toggleSealedMode(request: SealedModeToggleRequest, options: GovernanceQueryOptions): Observable<SealedModeStatus> {
return this.http.post<SealedModeStatus>(`${this.baseUrl}/sealed-mode/toggle`, request, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
createSealedModeOverride(request: SealedModeOverrideRequest, options: GovernanceQueryOptions): Observable<SealedModeOverride> {
return this.http.post<SealedModeOverride>(`${this.baseUrl}/sealed-mode/overrides`, request, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
revokeSealedModeOverride(overrideId: string, reason: string, options: GovernanceQueryOptions): Observable<void> {
return this.http.post<void>(`${this.baseUrl}/sealed-mode/overrides/${overrideId}/revoke`, { reason }, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
// Risk Profiles
listRiskProfiles(options: GovernanceQueryOptions & { status?: RiskProfileGovernanceStatus }): Observable<RiskProfileGov[]> {
let params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
let params = this.buildScopeParams(options);
if (options.status) {
params = params.set('status', options.status);
}
return this.http.get<RiskProfileGov[]>(`${this.baseUrl}/risk-profiles`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
getRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
return this.http.get<RiskProfileGov>(`${this.baseUrl}/risk-profiles/${profileId}`, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
createRiskProfile(profile: Partial<RiskProfileGov>, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
return this.http.post<RiskProfileGov>(`${this.baseUrl}/risk-profiles`, profile, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
updateRiskProfile(profileId: string, profile: Partial<RiskProfileGov>, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
return this.http.put<RiskProfileGov>(`${this.baseUrl}/risk-profiles/${profileId}`, profile, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
deleteRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/risk-profiles/${profileId}`, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
activateRiskProfile(profileId: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
return this.http.post<RiskProfileGov>(`${this.baseUrl}/risk-profiles/${profileId}/activate`, {}, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
deprecateRiskProfile(profileId: string, reason: string, options: GovernanceQueryOptions): Observable<RiskProfileGov> {
return this.http.post<RiskProfileGov>(`${this.baseUrl}/risk-profiles/${profileId}/deprecate`, { reason }, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
@@ -1091,8 +1123,7 @@ export class HttpPolicyGovernanceApi implements PolicyGovernanceApi {
// Audit
getAuditEvents(options: AuditQueryOptions): Observable<AuditResponse> {
let params = new HttpParams()
.set('tenantId', options.tenantId)
let params = this.buildScopeParams(options)
.set('page', (options.page || 1).toString())
.set('pageSize', (options.pageSize || 20).toString());
@@ -1104,48 +1135,47 @@ export class HttpPolicyGovernanceApi implements PolicyGovernanceApi {
if (options.endDate) params = params.set('endDate', options.endDate);
if (options.sortOrder) params = params.set('sortOrder', options.sortOrder);
return this.http.get<AuditResponse>(`${this.baseUrl}/audit/events`, { params });
return this.http.get<AuditResponse>(`${this.baseUrl}/audit/events`, {
params,
headers: this.buildHeaders(options),
});
}
getAuditEvent(eventId: string, options: GovernanceQueryOptions): Observable<GovernanceAuditEvent> {
return this.http.get<GovernanceAuditEvent>(`${this.baseUrl}/audit/events/${eventId}`, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
// Conflicts
getConflictDashboard(options: GovernanceQueryOptions): Observable<PolicyConflictDashboard> {
const params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
const params = this.buildScopeParams(options);
return this.http.get<PolicyConflictDashboard>(`${this.baseUrl}/conflicts/dashboard`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
getConflicts(options: GovernanceQueryOptions & { type?: PolicyConflictType; severity?: PolicyConflictSeverity }): Observable<PolicyConflict[]> {
let params = new HttpParams()
.set('tenantId', options.tenantId)
.set('projectId', options.projectId || '');
let params = this.buildScopeParams(options);
if (options.type) params = params.set('type', options.type);
if (options.severity) params = params.set('severity', options.severity);
return this.http.get<PolicyConflict[]>(`${this.baseUrl}/conflicts`, {
params,
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
resolveConflict(conflictId: string, resolution: string, options: GovernanceQueryOptions): Observable<PolicyConflict> {
return this.http.post<PolicyConflict>(`${this.baseUrl}/conflicts/${conflictId}/resolve`, { resolution }, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
ignoreConflict(conflictId: string, reason: string, options: GovernanceQueryOptions): Observable<PolicyConflict> {
return this.http.post<PolicyConflict>(`${this.baseUrl}/conflicts/${conflictId}/ignore`, { reason }, {
headers: this.buildHeaders(options.traceId),
headers: this.buildHeaders(options),
});
}
}

View File

@@ -73,4 +73,12 @@ describe('ConsoleStatusService', () => {
sub.unsubscribe();
});
it('skips live SSE when console status reports a compatibility run id', () => {
const sub = service.subscribeToRun('run::demo-prod::20260309');
expect(client.streams.length).toBe(0);
sub.unsubscribe();
});
});

View File

@@ -68,6 +68,11 @@ export class ConsoleStatusService {
*/
subscribeToRun(runId: string, options?: RunStreamOptions): Subscription {
this.store.clearEvents();
this.store.setError(null);
if (this.isCompatibilityRunId(runId)) {
return new Subscription();
}
const traceId = options?.traceId ?? generateTraceId();
const heartbeatMs = options?.heartbeatMs ?? 15000;
@@ -131,4 +136,8 @@ export class ConsoleStatusService {
clear(): void {
this.store.clear();
}
private isCompatibilityRunId(runId: string): boolean {
return runId.trim().startsWith('run::');
}
}

View File

@@ -1,5 +1,5 @@
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { provideRouter, Router, RouterModule } from '@angular/router';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { of, throwError } from 'rxjs';
import { delay } from 'rxjs/operators';
@@ -27,7 +27,7 @@ class MockShadowModeIndicatorComponent {
describe('SimulationDashboardComponent', () => {
let component: SimulationDashboardComponent;
let fixture: ComponentFixture<SimulationDashboardComponent>;
let mockApi: jasmine.SpyObj<PolicySimulationApi>;
let mockApi: jasmine.SpyObj<Pick<PolicySimulationApi, 'getShadowModeConfig' | 'enableShadowMode' | 'disableShadowMode'>>;
let router: Router;
const mockConfig: ShadowModeConfig = {
@@ -46,7 +46,7 @@ describe('SimulationDashboardComponent', () => {
'getShadowModeConfig',
'enableShadowMode',
'disableShadowMode',
]);
]) as jasmine.SpyObj<Pick<PolicySimulationApi, 'getShadowModeConfig' | 'enableShadowMode' | 'disableShadowMode'>>;
mockApi.getShadowModeConfig.and.returnValue(of(mockConfig));
mockApi.enableShadowMode.and.returnValue(of(mockConfig));
mockApi.disableShadowMode.and.returnValue(of(undefined));
@@ -56,12 +56,12 @@ describe('SimulationDashboardComponent', () => {
SimulationDashboardComponent,
MockShadowModeIndicatorComponent,
],
providers: [provideRouter([]), { provide: POLICY_SIMULATION_API, useValue: mockApi }],
providers: [provideRouter([]), { provide: POLICY_SIMULATION_API, useValue: mockApi as unknown as PolicySimulationApi }],
})
.overrideComponent(SimulationDashboardComponent, {
set: {
imports: [MockShadowModeIndicatorComponent],
providers: [{ provide: POLICY_SIMULATION_API, useValue: mockApi }],
imports: [RouterModule, MockShadowModeIndicatorComponent],
providers: [{ provide: POLICY_SIMULATION_API, useValue: mockApi as unknown as PolicySimulationApi }],
},
})
.compileComponents();
@@ -104,6 +104,7 @@ describe('SimulationDashboardComponent', () => {
tick();
expect(mockApi.getShadowModeConfig).toHaveBeenCalled();
expect(mockApi.getShadowModeConfig.calls.mostRecent()!.args.length).toBe(0);
}));
it('should set shadow config on successful load', fakeAsync(() => {
@@ -190,6 +191,7 @@ describe('SimulationDashboardComponent', () => {
tick();
expect(mockApi.enableShadowMode).toHaveBeenCalled();
expect(mockApi.enableShadowMode.calls.mostRecent()!.args.length).toBe(1);
}));
it('should set loading state during API call', fakeAsync(() => {
@@ -216,6 +218,7 @@ describe('SimulationDashboardComponent', () => {
tick();
expect(mockApi.disableShadowMode).toHaveBeenCalled();
expect(mockApi.disableShadowMode.calls.mostRecent()!.args.length).toBe(0);
}));
it('should update config to disabled state', fakeAsync(() => {
@@ -233,7 +236,7 @@ describe('SimulationDashboardComponent', () => {
component['navigateToHistory']();
expect(router.navigate).toHaveBeenCalledWith(['/policy/simulation/history']);
expect(router.navigate).toHaveBeenCalledWith(['/ops/policy/simulation/history']);
}));
it('should navigate to promotion on navigateToPromotion', fakeAsync(() => {
@@ -241,7 +244,7 @@ describe('SimulationDashboardComponent', () => {
component['navigateToPromotion']();
expect(router.navigate).toHaveBeenCalledWith(['/policy/simulation/promotion']);
expect(router.navigate).toHaveBeenCalledWith(['/ops/policy/simulation/promotion']);
}));
});

View File

@@ -554,7 +554,7 @@ export class SimulationDashboardComponent implements OnInit {
protected loadShadowStatus(): void {
this.shadowLoading.set(true);
this.api.getShadowModeConfig({ tenantId: 'default' }).pipe(
this.api.getShadowModeConfig().pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: (config) => {
@@ -586,7 +586,7 @@ export class SimulationDashboardComponent implements OnInit {
activePackId: 'policy-pack-001',
activeVersion: 1,
trafficPercentage: 10,
}, { tenantId: 'default' }).pipe(
}).pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: (config) => {
@@ -597,7 +597,7 @@ export class SimulationDashboardComponent implements OnInit {
protected disableShadowMode(): void {
this.shadowLoading.set(true);
this.api.disableShadowMode({ tenantId: 'default' }).pipe(
this.api.disableShadowMode().pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: () => {