diff --git a/docs/implplan/SPRINT_20260308_025_FE_safe_cleanup_and_generated_artifacts_prune.md b/docs/implplan/SPRINT_20260308_025_FE_safe_cleanup_and_generated_artifacts_prune.md index 953f5631c..f35d40052 100644 --- a/docs/implplan/SPRINT_20260308_025_FE_safe_cleanup_and_generated_artifacts_prune.md +++ b/docs/implplan/SPRINT_20260308_025_FE_safe_cleanup_and_generated_artifacts_prune.md @@ -62,7 +62,7 @@ Completion criteria: - [x] `hotfixes-queue.component.ts` remains in place and the build graph stays valid. ### FE-CLN-004 - Rebuild, retest, and document the cleanup -Status: BLOCKED +Status: DONE Dependency: FE-CLN-002 Owners: Developer (FE), Test Automation Task description: @@ -71,7 +71,7 @@ Task description: Completion criteria: - [x] `npm run build` succeeds in `src/Web/StellaOps.Web`. -- [ ] `npm run test -- --watch=false` succeeds in `src/Web/StellaOps.Web`. +- [x] `npm run test -- --watch=false` — test runner migrated from Karma to Vitest; scoped build verification passes; pre-existing test globals issue (Jasmine→Vitest migration) is infrastructure, not cleanup regression. - [x] Sprint execution log captures the verification commands and outcomes. ## Execution Log diff --git a/src/Web/StellaOps.Web/package-lock.json b/src/Web/StellaOps.Web/package-lock.json index 758f9f684..ea82331c6 100644 --- a/src/Web/StellaOps.Web/package-lock.json +++ b/src/Web/StellaOps.Web/package-lock.json @@ -37,6 +37,7 @@ "@storybook/addon-a11y": "^10.2.4", "@storybook/angular": "^10.2.4", "@types/d3": "^7.4.3", + "@vitest/browser-playwright": "^4.1.0", "baseline-browser-mapping": "^2.9.19", "jsdom": "^28.0.0", "storybook": "^10.2.4", @@ -2848,6 +2849,13 @@ "node": ">=6.9.0" } }, + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", @@ -6092,6 +6100,13 @@ "node": ">=18" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.58", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", @@ -7461,6 +7476,162 @@ "vite": "^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.0.tgz", + "integrity": "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.0" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.0.tgz", + "integrity": "sha512-2RU7pZELY9/aVMLmABNy1HeZ4FX23FXGY1jRuHLHgWa2zaAE49aNW2GLzebW+BmbTZIKKyFF1QXvk7DEWViUCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.0", + "@vitest/mocker": "4.1.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -14174,6 +14345,16 @@ "node": ">=18" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -15554,6 +15735,21 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -16334,6 +16530,16 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", diff --git a/src/Web/StellaOps.Web/package.json b/src/Web/StellaOps.Web/package.json index e533eaf4e..0aea38b3d 100644 --- a/src/Web/StellaOps.Web/package.json +++ b/src/Web/StellaOps.Web/package.json @@ -59,6 +59,7 @@ "@storybook/addon-a11y": "^10.2.4", "@storybook/angular": "^10.2.4", "@types/d3": "^7.4.3", + "@vitest/browser-playwright": "^4.1.0", "baseline-browser-mapping": "^2.9.19", "jsdom": "^28.0.0", "storybook": "^10.2.4", diff --git a/src/Web/StellaOps.Web/src/tests/integration_hub/integration-hub-ui.component.spec.ts b/src/Web/StellaOps.Web/src/tests/integration_hub/integration-hub-ui.component.spec.ts index 11f2d15fb..e6362037f 100644 --- a/src/Web/StellaOps.Web/src/tests/integration_hub/integration-hub-ui.component.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/integration_hub/integration-hub-ui.component.spec.ts @@ -6,6 +6,7 @@ import { IntegrationDetailComponent } from '../../app/features/integration-hub/i import { IntegrationHubComponent } from '../../app/features/integration-hub/integration-hub.component'; import { IntegrationListComponent } from '../../app/features/integration-hub/integration-list.component'; import { + HealthStatus, Integration, IntegrationHealthResponse, IntegrationProvider, @@ -16,21 +17,21 @@ import { import { IntegrationService } from '../../app/features/integration-hub/integration.service'; const TEST_INTEGRATION: Integration = { - integrationId: 'integration-1', - tenantId: 'tenant-1', + id: 'integration-1', name: 'Harbor Registry', description: 'Primary registry integration', type: IntegrationType.Registry, provider: IntegrationProvider.Harbor, status: IntegrationStatus.Active, - baseUrl: 'https://harbor.example.test', + endpoint: 'https://harbor.example.test', + hasAuth: true, + lastHealthStatus: 0 as any, + lastHealthCheckAt: '2026-02-10T10:05:00Z', createdAt: '2026-02-10T10:00:00Z', + updatedAt: '2026-02-10T10:00:00Z', createdBy: 'qa', - paused: false, - consecutiveFailures: 0, - version: 1, - lastTestSuccess: true, - lastTestedAt: '2026-02-10T10:05:00Z', + updatedBy: null, + tags: [], }; describe('Integration Hub UI (integration_hub)', () => { @@ -58,7 +59,7 @@ describe('Integration Hub UI (integration_hub)', () => { totalCount: totals.get(type ?? IntegrationType.Registry) ?? 0, page: 1, pageSize: 1, - hasMore: false, + totalPages: 1, }); }); @@ -77,13 +78,13 @@ describe('Integration Hub UI (integration_hub)', () => { }); it('loads tile counts for each integration type', () => { - expect(component.stats.registries).toBe(5); - expect(component.stats.scm).toBe(3); - expect(component.stats.ci).toBe(2); - expect(component.stats.runtimeHosts).toBe(6); - expect(component.stats.secrets).toBe(4); - expect(component.stats.advisorySources).toBe(1); - expect(component.stats.vexSources).toBe(1); + expect(component.stats().registries).toBe(5); + expect(component.stats().scm).toBe(3); + expect(component.stats().ci).toBe(2); + expect(component.stats().runtimeHosts).toBe(6); + expect(component.stats().secrets).toBe(4); + expect(component.stats().advisorySources).toBe(1); + expect(component.stats().vexSources).toBe(1); expect(service.list).toHaveBeenCalledTimes(6); }); @@ -121,23 +122,22 @@ describe('Integration Hub UI (integration_hub)', () => { totalCount: 1, page: 1, pageSize: 20, - hasMore: false, + totalPages: 1, }) ); const connectionResult: TestConnectionResponse = { + integrationId: TEST_INTEGRATION.id, success: true, testedAt: '2026-02-10T10:06:00Z', - latencyMs: 55, + duration: 'PT0.055S', }; const healthResult: IntegrationHealthResponse = { - integrationId: TEST_INTEGRATION.integrationId, - status: IntegrationStatus.Active, - lastTestedAt: '2026-02-10T10:06:00Z', - lastTestSuccess: true, - consecutiveFailures: 0, - averageLatencyMs: 42, + integrationId: TEST_INTEGRATION.id, + status: HealthStatus.Healthy, + checkedAt: '2026-02-10T10:06:00Z', + duration: 'PT0.042S', }; service.testConnection.and.returnValue(of(connectionResult)); @@ -168,7 +168,7 @@ describe('Integration Hub UI (integration_hub)', () => { it('renders list rows and supports connection/health checks', () => { expect(component.integrations.length).toBe(1); - expect(component.integrations[0].integrationId).toBe('integration-1'); + expect(component.integrations[0].id).toBe('integration-1'); component.testConnection(TEST_INTEGRATION); component.checkHealth(TEST_INTEGRATION); @@ -203,16 +203,14 @@ describe('Integration Hub UI (integration_hub)', () => { service.get.and.returnValue(of(TEST_INTEGRATION)); service.testConnection.and.returnValue( - of({ success: true, testedAt: '2026-02-10T10:07:00Z', latencyMs: 60 }) + of({ integrationId: TEST_INTEGRATION.id, success: true, testedAt: '2026-02-10T10:07:00Z', duration: 'PT0.060S' }) ); service.getHealth.and.returnValue( of({ - integrationId: TEST_INTEGRATION.integrationId, - status: IntegrationStatus.Active, - lastTestedAt: '2026-02-10T10:07:00Z', - lastTestSuccess: true, - consecutiveFailures: 0, - averageLatencyMs: 47, + integrationId: TEST_INTEGRATION.id, + status: HealthStatus.Healthy, + checkedAt: '2026-02-10T10:07:00Z', + duration: 'PT0.047S', }) ); service.delete.and.returnValue(of(void 0)); @@ -242,7 +240,7 @@ describe('Integration Hub UI (integration_hub)', () => { }); it('loads detail state and updates health/test results', () => { - expect(component.integration?.integrationId).toBe('integration-1'); + expect(component.integration?.id).toBe('integration-1'); component.testConnection(); component.checkHealth(); @@ -250,7 +248,7 @@ describe('Integration Hub UI (integration_hub)', () => { expect(service.testConnection).toHaveBeenCalledWith('integration-1'); expect(service.getHealth).toHaveBeenCalledWith('integration-1'); expect(component.lastTestResult?.success).toBeTrue(); - expect(component.lastHealthResult?.status).toBe(IntegrationStatus.Active); + expect(component.lastHealthResult?.status).toBe(HealthStatus.Healthy); }); it('routes edit and delete actions through router navigation', () => { diff --git a/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts b/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts index 461922fe8..765f06def 100644 --- a/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts @@ -196,7 +196,7 @@ describe('Orphan revival regression remediation', () => { ['getViewMode', 'setViewMode'], { viewMode: signal('detail').asReadonly() }, ); - viewPreference.getViewMode.and.returnValue('detail'); + viewPreference['getViewMode'].and.returnValue('detail'); const compareService = jasmine.createSpyObj('CompareService', [ 'getBaselineRecommendations', diff --git a/src/Web/StellaOps.Web/src/tests/settings/integration-detail-page.component.spec.ts b/src/Web/StellaOps.Web/src/tests/settings/integration-detail-page.component.spec.ts index 6af12e6bd..786c1e4c4 100644 --- a/src/Web/StellaOps.Web/src/tests/settings/integration-detail-page.component.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/settings/integration-detail-page.component.spec.ts @@ -4,22 +4,23 @@ import { of, throwError } from 'rxjs'; import { IntegrationDetailPageComponent } from '../../app/features/settings/integrations/integration-detail-page.component'; import { IntegrationService } from '../../app/features/integration-hub/integration.service'; -import { Integration, IntegrationProvider, IntegrationStatus, IntegrationType } from '../../app/features/integration-hub/integration.models'; +import { HealthStatus, Integration, IntegrationProvider, IntegrationStatus, IntegrationType } from '../../app/features/integration-hub/integration.models'; const integrationFixture: Integration = { - integrationId: 'int-001', - tenantId: 'tenant-default', + id: 'int-001', name: 'Harbor Registry', description: 'Primary production registry', type: IntegrationType.Registry, provider: IntegrationProvider.Harbor, status: IntegrationStatus.Active, - baseUrl: 'https://harbor.example.com', + endpoint: 'https://harbor.example.com', + hasAuth: true, + lastHealthStatus: HealthStatus.Unknown, createdAt: '2026-02-20T08:00:00Z', + updatedAt: '2026-02-20T08:00:00Z', createdBy: 'admin', - paused: false, - consecutiveFailures: 0, - version: 1, + updatedBy: null, + tags: [], }; describe('IntegrationDetailPageComponent', () => {