Quiet web test lane warnings and align scheduler spec
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user