import { expect, test, type Page, type Route } from '@playwright/test'; import type { StubAuthSession } from '../../src/app/testing/auth-fixtures'; const operatorSession: StubAuthSession = { subjectId: 'release-promotions-e2e-user', tenant: 'tenant-default', scopes: [ 'admin', 'ui.read', 'release:read', 'release:write', 'release:publish', 'orch:read', 'orch:operate', 'policy:read', 'policy:review', ], }; const mockConfig = { authority: { issuer: '/authority', clientId: 'stella-ops-ui', authorizeEndpoint: '/authority/connect/authorize', tokenEndpoint: '/authority/connect/token', logoutEndpoint: '/authority/connect/logout', redirectUri: 'https://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'https://127.0.0.1:4400/', scope: 'openid profile email ui.read', audience: '/gateway', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: '/authority', scanner: '/scanner', policy: '/policy', concelier: '/concelier', attestor: '/attestor', gateway: '/gateway', }, quickstartMode: true, setup: 'complete', }; const approvalSummary = { approvalId: 'apr-001', releaseId: 'rel-001', releaseName: 'API Gateway', releaseVersion: '2.1.0', sourceEnvironment: 'stage', targetEnvironment: 'production', requestedBy: 'alice', requestedAt: '2026-03-08T08:30:00Z', urgency: 'normal', justification: 'Promote the verified API Gateway release to production.', status: 'pending', currentApprovals: 0, requiredApprovals: 2, blockers: [], }; const approvalDetail = { ...approvalSummary, gateResults: [ { gateId: 'gate-policy', gateName: 'Policy', type: 'policy', status: 'passed', message: 'Policy checks passed.', evaluatedAt: '2026-03-08T08:35:00Z', }, ], actions: [], approvers: [], releaseComponents: [{ name: 'api-gateway', version: '2.1.0', digest: 'sha256:api-gateway' }], }; const createdApproval = { id: 'apr-new', releaseId: 'rel-001', releaseName: 'API Gateway', releaseVersion: '2.1.0', sourceEnvironment: 'stage', targetEnvironment: 'production', requestedBy: 'release-promotions-e2e-user', requestedAt: '2026-03-08T09:45:00Z', urgency: 'normal', justification: 'Promote the API Gateway release to production after decisioning review.', status: 'pending', currentApprovals: 0, requiredApprovals: 2, gatesPassed: true, scheduledTime: null, expiresAt: '2026-03-10T09:45:00Z', }; async function fulfillJson(route: Route, body: unknown, status = 200): Promise { await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body), }); } async function setupHarness(page: Page): Promise { await page.addInitScript((session) => { (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session; }, operatorSession); await page.route('**/api/**', (route) => fulfillJson(route, {})); await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig)); await page.route('**/platform/i18n/*.json', (route) => fulfillJson(route, {})); await page.route('**/config.json', (route) => fulfillJson(route, mockConfig)); await page.route('**/.well-known/openid-configuration', (route) => fulfillJson(route, { issuer: 'https://127.0.0.1:4400/authority', authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize', token_endpoint: 'https://127.0.0.1:4400/authority/connect/token', jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json', response_types_supported: ['code'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'], }), ); await page.route('**/authority/.well-known/jwks.json', (route) => fulfillJson(route, { keys: [] })); await page.route('**/console/branding**', (route) => fulfillJson(route, { tenantId: operatorSession.tenant, appName: 'Stella Ops', logoUrl: null, cssVariables: {}, }), ); await page.route('**/console/profile**', (route) => fulfillJson(route, { subjectId: operatorSession.subjectId, username: 'release-promotions-e2e', displayName: 'Release Promotions E2E', tenant: operatorSession.tenant, roles: ['release-operator'], scopes: operatorSession.scopes, }), ); await page.route('**/console/token/introspect**', (route) => fulfillJson(route, { active: true, tenant: operatorSession.tenant, subject: operatorSession.subjectId, scopes: operatorSession.scopes, }), ); await page.route('**/authority/console/tenants**', (route) => fulfillJson(route, { tenants: [ { tenantId: operatorSession.tenant, displayName: 'Default Tenant', isDefault: true, isActive: true, }, ], }), ); await page.route('**/api/v2/context/regions**', (route) => fulfillJson(route, [{ regionId: 'eu-west', displayName: 'EU West', sortOrder: 1, enabled: true }]), ); await page.route('**/api/v2/context/environments**', (route) => fulfillJson(route, [ { environmentId: 'production', regionId: 'eu-west', environmentType: 'prod', displayName: 'Production', sortOrder: 1, enabled: true, }, ]), ); await page.route('**/api/v2/context/preferences**', (route) => fulfillJson(route, { tenantId: operatorSession.tenant, actorId: operatorSession.subjectId, regions: ['eu-west'], environments: ['production'], timeWindow: '24h', stage: 'all', updatedAt: '2026-03-08T07:00:00Z', updatedBy: operatorSession.subjectId, }), ); await page.route(/\/api\/v2\/releases\/approvals(?:\?.*)?$/, (route) => fulfillJson(route, [approvalSummary])); await page.route(/\/api\/v1\/approvals\/apr-001$/, (route) => fulfillJson(route, approvalDetail)); await page.route(/\/api\/v1\/approvals\/apr-new$/, (route) => fulfillJson(route, { ...approvalDetail, ...createdApproval, gateResults: approvalDetail.gateResults, }), ); await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/available-environments$/, (route) => fulfillJson(route, [ { id: 'env-stage', name: 'Stage', tier: 'staging' }, { id: 'env-production', name: 'Production', tier: 'production' }, ]), ); await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/promotion-preview(?:\?.*)?$/, (route) => fulfillJson(route, { releaseId: 'rel-001', releaseName: 'API Gateway', sourceEnvironment: 'stage', targetEnvironment: 'production', gateResults: [ { gateId: 'gate-policy', gateName: 'Policy', type: 'policy', status: 'passed', message: 'Policy checks passed.', evaluatedAt: '2026-03-08T09:40:00Z', }, ], allGatesPassed: true, requiredApprovers: 2, estimatedDeployTime: 180, warnings: [], }), ); await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/promote$/, async (route) => fulfillJson(route, createdApproval, 201), ); } test.beforeEach(async ({ page }) => { await setupHarness(page); }); test('release overview surfaces the canonical promotions page', async ({ page }) => { await page.goto('/releases/overview', { waitUntil: 'networkidle' }); await page.locator('.overview').getByRole('link', { name: 'Promotions' }).click(); await expect(page).toHaveURL(/\/releases\/promotions(?:\?.*)?$/); await expect(page.getByRole('heading', { name: 'Promotions' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Create Promotion' })).toBeVisible(); }); test('legacy promotions create alias lands on the canonical wizard and submits a promotion request', async ({ page }) => { await page.goto( '/release-control/promotions/create?releaseId=rel-001&returnTo=%2Freleases%2Fruns%2Frun-001%2Fgate-decision', { waitUntil: 'networkidle' }, ); await expect(page).toHaveURL(/\/releases\/promotions\/create\?releaseId=rel-001/); await expect(page.getByLabel('Release context handoff')).toContainText('rel-001'); await expect(page.getByRole('heading', { name: 'Select Region and Environment Path' })).toBeVisible(); await page.locator('#target-env').selectOption('env-production'); await expect(page.getByRole('heading', { name: 'Gate Preview' })).toBeVisible(); await expect(page.getByText('All gates passed')).toBeVisible(); await page.getByRole('button', { name: 'Next ->' }).click(); await expect(page.getByRole('heading', { name: 'Approval Context' })).toBeVisible(); await page.getByLabel('Justification').fill( 'Promote the API Gateway release to production after decisioning review.', ); await page.getByRole('button', { name: 'Next ->' }).click(); await expect(page.getByRole('heading', { name: 'Launch Promotion' })).toBeVisible(); await page.getByRole('button', { name: 'Submit Promotion Request' }).click(); await expect(page).toHaveURL(/\/releases\/promotions\/apr-new(?:\?.*)?$/); await expect(page.getByRole('heading', { name: 'API Gateway' })).toBeVisible(); await expect(page.getByText('pending', { exact: true })).toBeVisible(); });