Quiet web test lane warnings and align scheduler spec

This commit is contained in:
master
2026-04-06 00:51:50 +03:00
parent f8e4bf65fb
commit de5bc63f89
4 changed files with 180 additions and 42 deletions

View File

@@ -0,0 +1,65 @@
# Sprint 20260405-006 - FE Default Web Test Lane Repair
## Topic & Scope
- Restore the default Angular/Vitest Web unit-test lane so `npm test -- --watch=false` passes again.
- Rewrite stale specs to current shipped Web surfaces and route contracts instead of recreating removed component APIs or feature trees.
- Working directory: `src/Web/StellaOps.Web/`.
- Expected evidence: green default Web test lane, green active-surface lane, updated sprint execution log.
## Dependencies & Concurrency
- Depends on `SPRINT_20260405_002_FE_test_lane_repair_for_active_surfaces.md` for the focused shipped-surface lane.
- Safe to run after the Graph/JobEngine persistence work because this sprint is limited to Web tests and test-only scaffolding.
## Documentation Prerequisites
- `docs/modules/web/architecture.md`
- `docs/implplan/SPRINT_20260405_002_FE_test_lane_repair_for_active_surfaces.md`
## Delivery Tracker
### FE-TEST-008 - Rewrite stale spec expectations to current component APIs
Status: DONE
Dependency: none
Owners: Developer / Implementer, Test Automation
Task description:
- Repair the current compile failures caused by specs asserting removed instance methods and fields such as `setTab`, `onSearch`, `tabs`, `headerTitle`, `shellState`, and `withContext`.
- Keep runtime behavior unchanged; the fix is to update tests to current component state and route contracts.
Completion criteria:
- [x] The default lane no longer fails on removed instance APIs in current shipped components.
- [x] Navigation assertions compile under the current Vitest assertion types.
- [x] The rewritten specs validate current DOM, signal state, or route behavior instead of dead component helpers.
### FE-TEST-009 - Repoint removed feature-tree specs to current shipped surfaces
Status: DONE
Dependency: FE-TEST-008
Owners: Developer / Implementer, Test Automation
Task description:
- Replace specs that still import removed Web feature trees (`agents`, `signals`, older platform-ops/setup pages, deleted environments list page) with tests against the current topology, doctor, platform-ops, and route-redirect owners.
- Preserve useful user-facing intent where a legacy route is still intentionally redirected.
Completion criteria:
- [x] No default-lane spec imports a deleted Web component or service path.
- [x] Legacy `signals` and `platform-ops` coverage is expressed through current redirects and live owning pages.
- [x] The default lane passes without excluding the repaired spec families.
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-04-05 | Sprint created for broad default-lane Web test repair after focused active-surface lane completion. | Developer |
| 2026-04-05 | Lowered the Web Vitest heap ceiling to 3072 MB, switched the runner to `forks`, and reduced deterministic batch size to 12 files so the default lane can run within the available process and RAM limits. | Developer |
| 2026-04-05 | Rewrote stale default-lane specs to current route contracts and shipped component behavior across releases, setup/platform, topology, security-risk, trust-admin, pack-registry, quiet-lane, and legacy redirect coverage. | Developer |
| 2026-04-05 | Verification complete: `npm test -- --watch=false` finished through the deterministic 32-batch runner with all batches green, and `npm run test:active-surfaces` passed 25/25 after the final repairs. | Test Automation |
| 2026-04-05 | Removed deprecated `allowSignalWrites` usage across the Web app and centralized jsdom/browser-noise cleanup in `src/test-setup.ts` for `ResizeObserver`, `alert`, Angular sanitizer output, and synthetic navigation warnings. | Developer |
| 2026-04-05 | Re-verified the quieter lane with `npm run test:active-surfaces` and `npm test -- --batch-from=31 --batch-to=31`; both passed with the previous warning noise removed from those runs. | Test Automation |
| 2026-04-05 | Completed a final full deterministic 32-batch rerun with all batches green, then removed the remaining `NG0956` warning via stable `@for` tracking in the policy editor and suppressed known expected failure-path console noise in the shared test harness; targeted noisy-spec reruns and `npm run test:active-surfaces` both passed cleanly afterward. | Developer |
## Decisions & Risks
- This sprint does not reintroduce deleted production APIs to satisfy tests.
- When a legacy route still exists, tests should cover the redirect contract; when the old feature no longer ships, tests should move to the current owning page.
- Existing unrelated repo changes outside `src/Web/StellaOps.Web/` remain out of scope and untouched.
- The default lane is intentionally verified through the deterministic batch runner that backs `npm test -- --watch=false`; after each late-batch fix, only the affected batch and remaining tail were rerun to avoid redundant full rebuilds under the repo's current memory pressure.
- Angular sanitizer chatter, deprecated `allowSignalWrites`, and jsdom-only `alert()` / synthetic navigation warnings are now handled in code or the shared test harness so passing runs stay readable.
- The remaining quiet-lane filtering in `src/test-setup.ts` is intentionally limited to known expected failure-path console output from specs that already assert user-visible error handling.
## Next Checkpoints
- 2026-04-05: Sprint complete; archive after adjacent Web test-lane work is no longer active.

View File

@@ -101,7 +101,7 @@ interface ChecklistItem {
<p>Errors and warnings are sorted deterministically by line and column.</p>
</header>
<ul class="diagnostics__list">
@for (diag of diagnostics; track diag) {
@for (diag of diagnostics; track trackDiagnostic($index, diag)) {
<li class="diagnostics__item">
<span class="diagnostics__severity" [attr.data-severity]="diag.severity">
{{ diag.severity | titlecase }}
@@ -121,9 +121,9 @@ interface ChecklistItem {
<header class="sidebar-card__header">
<h3>Compliance checklist</h3>
<p>Must stay green before submit/review.</p>
</header>
<ul class="checklist">
@for (item of checklist; track item) {
</header>
<ul class="checklist">
@for (item of checklist; track trackChecklistItem($index, item)) {
<li class="checklist__item">
<span class="checklist__status" [attr.data-status]="item.status"></span>
<div class="checklist__body">
@@ -792,6 +792,14 @@ export class PolicyEditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.checklist = items;
}
protected trackDiagnostic(_index: number, diag: PolicyDiagnostic): string {
return `${diag.code}:${diag.line}:${diag.column}:${diag.message}`;
}
protected trackChecklistItem(_index: number, item: ChecklistItem): string {
return item.id;
}
/**
* Derive a PolicyPackDocument from the loaded PolicyPack so the shared
* visual gate editor can render. Returns undefined if the pack lacks

View File

@@ -23,9 +23,38 @@ import {
import { beforeEach, vi, type Mock } from 'vitest';
const angularTestEnvironmentKey = '__stellaAngularTestEnvironmentInitialized__';
const angularTestConsolePatchKey = '__stellaAngularTestConsolePatched__';
const angularTestResourceRoot = join(process.cwd(), 'src');
const angularTestResourcePathCache = new Map<string, string>();
const angularTestResourceContentCache = new Map<string, string>();
const suppressedTestWarnFragments = [
'WARNING: sanitizing HTML stripped some content',
];
const suppressedTestErrorFragments = [
"Not implemented: navigation to another Document",
"Not implemented: Window's alert() method",
'Failed to load budget dashboard:',
'Failed to load config:',
'Failed to load console context',
'console run stream error',
'Mermaid render error:',
'GraphViz render error:',
'Evidence bundle export failed:',
];
class TestResizeObserver {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
function consoleMessageIncludes(args: unknown[], fragment: string): boolean {
return args.some((arg) => String(arg).includes(fragment));
}
function consoleMessageIncludesAny(args: unknown[], fragments: readonly string[]): boolean {
return fragments.some((fragment) => consoleMessageIncludes(args, fragment));
}
async function findAngularTestResourcePath(
searchRoot: string,
@@ -95,7 +124,55 @@ if (!(globalThis as Record<string, unknown>)[angularTestEnvironmentKey]) {
(globalThis as Record<string, unknown>)[angularTestEnvironmentKey] = true;
}
if (!(globalThis as Record<string, unknown>)[angularTestConsolePatchKey]) {
const originalWarn = console.warn.bind(console);
const originalError = console.error.bind(console);
console.warn = (...args: unknown[]) => {
if (consoleMessageIncludesAny(args, suppressedTestWarnFragments)) {
return;
}
originalWarn(...args);
};
console.error = (...args: unknown[]) => {
if (consoleMessageIncludesAny(args, suppressedTestErrorFragments)) {
return;
}
originalError(...args);
};
(globalThis as Record<string, unknown>)[angularTestConsolePatchKey] = true;
}
beforeEach(async () => {
if (!globalThis.crypto?.subtle) {
const nodeCryptoModule =
(globalThis as any).process?.getBuiltinModule?.('node:crypto') ??
(globalThis as any).process?.getBuiltinModule?.('crypto') ??
(globalThis as any).require?.('crypto') ??
(globalThis as any).module?.require?.('crypto');
if (nodeCryptoModule?.webcrypto) {
Object.defineProperty(globalThis, 'crypto', {
value: nodeCryptoModule.webcrypto,
configurable: true,
});
}
}
Object.defineProperty(globalThis, 'ResizeObserver', {
value: TestResizeObserver,
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'alert', {
value: () => undefined,
configurable: true,
writable: true,
});
await resolveComponentResources(resolveAngularTestResource);
getTestBed().resetTestingModule();
});

View File

@@ -24,7 +24,6 @@ const createRun = (overrides: Partial<SchedulerRun> = {}): SchedulerRun => ({
progress: 100,
itemsProcessed: 200,
itemsTotal: 200,
retryCount: 0,
...overrides,
});
@@ -50,25 +49,16 @@ const createSchedulerApiMock = (): SchedulerApi => {
{
id: 'sch-seed',
name: 'Seed Schedule',
description: 'seed',
cronExpression: '0 6 * * *',
timezone: 'UTC',
enabled: true,
taskType: 'scan',
taskConfig: {},
mode: 'analysis-only',
selection: { scope: 'all-images' },
limits: { parallelism: 1 },
lastRunAt: undefined,
nextRunAt: undefined,
createdAt: '2026-02-11T10:00:00Z',
updatedAt: '2026-02-11T10:00:00Z',
createdBy: 'test',
tags: [],
retryPolicy: {
maxRetries: 3,
backoffMultiplier: 2,
initialDelayMs: 1000,
maxDelayMs: 60000,
},
concurrencyLimit: 1,
},
];
return {
@@ -78,25 +68,16 @@ const createSchedulerApiMock = (): SchedulerApi => {
const created: Schedule = {
id: `sch-${schedules.length + 1}`,
name: dto.name,
description: dto.description,
cronExpression: dto.cronExpression,
timezone: dto.timezone,
enabled: dto.enabled,
taskType: dto.taskType,
taskConfig: dto.taskConfig ?? {},
mode: dto.mode,
selection: { ...dto.selection },
limits: { ...(dto.limits ?? {}) },
lastRunAt: undefined,
nextRunAt: undefined,
createdAt: '2026-02-11T10:00:00Z',
updatedAt: '2026-02-11T10:00:00Z',
createdBy: 'test',
tags: dto.tags ?? [],
retryPolicy: dto.retryPolicy ?? {
maxRetries: 3,
backoffMultiplier: 2,
initialDelayMs: 1000,
maxDelayMs: 60000,
},
concurrencyLimit: dto.concurrencyLimit ?? 1,
};
schedules = [...schedules, created];
return of(created);
@@ -119,13 +100,14 @@ const createSchedulerApiMock = (): SchedulerApi => {
pauseSchedule: (_id: string) => of(void 0),
resumeSchedule: (_id: string) => of(void 0),
triggerSchedule: (_id: string) => of(void 0),
listRuns: () => of({ runs: [], nextCursor: undefined }),
cancelRun: (_runId: string) => of(createRun({ status: 'cancelled' })),
retryRun: (_runId: string) => of(createRun({ id: 'run-retry-001', status: 'queued', retryOf: 'run-001' })),
previewImpact: () => of({
scheduleId: 'preview',
proposedChange: 'update',
affectedRuns: 0,
nextRunTime: '2026-02-11T11:00:00Z',
estimatedLoad: 10,
conflicts: [],
total: 0,
usageOnly: true,
generatedAt: '2026-02-11T11:00:00Z',
sample: [],
warnings: [],
}),
} satisfies SchedulerApi;
@@ -156,6 +138,7 @@ describe('scheduler-orchestrator-ops-ui behavior', () => {
});
it('orders runs deterministically when triggered timestamps tie', () => {
spyOn(Date, 'now').and.returnValue(new Date('2026-02-11T12:00:00Z').getTime());
component.runs.set([
createRun({ id: 'run-b', scheduleName: 'Beta', triggeredAt: '2026-02-11T12:00:00Z' }),
createRun({ id: 'run-a', scheduleName: 'Alpha', triggeredAt: '2026-02-11T12:00:00Z' }),
@@ -256,7 +239,6 @@ describe('scheduler-orchestrator-ops-ui behavior', () => {
component.showCreateModal();
component.scheduleForm.name = 'Invalid Cron Job';
component.scheduleForm.cronExpression = 'bad-cron';
component.scheduleForm.taskType = 'scan';
component.saveSchedule();
fixture.detectChanges();
@@ -267,17 +249,23 @@ describe('scheduler-orchestrator-ops-ui behavior', () => {
expect(host.querySelector('.form-error')).toBeTruthy();
});
it('normalizes tags deterministically when creating schedules', () => {
it('persists selector scope and limits from the simplified schedule form', () => {
component.showCreateModal();
component.scheduleForm.name = 'Tag Normalization Job';
component.scheduleForm.name = 'Namespace Scope Job';
component.scheduleForm.cronExpression = '0 6 * * *';
component.scheduleForm.taskType = 'scan';
component.scheduleForm.tagsInput = 'prod, alpha, prod, beta';
component.scheduleForm.mode = 'analysis-only';
component.scheduleForm.selectionScope = 'by-namespace';
component.scheduleForm.namespacesInput = 'prod, alpha';
component.scheduleForm.parallelism = 4;
component.saveSchedule();
const created = component.schedules().find((schedule) => schedule.name === 'Tag Normalization Job');
expect(created?.tags).toEqual(['alpha', 'beta', 'prod']);
const created = component.schedules().find((schedule) => schedule.name === 'Namespace Scope Job');
expect(created?.selection).toEqual({
scope: 'by-namespace',
namespaces: ['prod', 'alpha'],
});
expect(created?.limits.parallelism).toBe(4);
});
it('uses canonical back link to operations scheduler runs', () => {