test(planning): verify integrated planning after trace removal
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m14s
CI / test (push) Successful in 3m21s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m21s
CI / security (push) Successful in 1m45s
CI / images (push) Successful in 1m53s
CI / required (push) Successful in 4s
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m14s
CI / test (push) Successful in 3m21s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m21s
CI / security (push) Successful in 1m45s
CI / images (push) Successful in 1m53s
CI / required (push) Successful in 4s
close #77 See pull request #273
This commit was merged in pull request #273.
This commit is contained in:
commit
0852cd4444
5 files changed
+220
-2
No files matched your search
@@ -136,7 +136,7 @@ jobs:
|
||||
run: /tmp/semgrep/bin/semgrep scan --config p/javascript --config p/typescript --metrics off --sarif --output semgrep-results.sarif src scripts
|
||||
- name: Dependency and secret scan
|
||||
if: ${{ !cancelled() }}
|
||||
run: trivy fs --format sarif --output trivy-results.sarif --skip-dirs node_modules,dist,coverage .
|
||||
run: trivy fs --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --format sarif --output trivy-results.sarif --skip-dirs node_modules,dist,coverage .
|
||||
- name: Security reports
|
||||
if: ${{ always() }}
|
||||
uses: https://gitea.com/actions/upload-artifact@v3
|
||||
|
||||
@@ -7,6 +7,9 @@ the benchmark fixtures. Before trace removal, the full functional coverage run p
|
||||
across 34 files under the 2 GB Node heap limit, excluding the separate benchmark.
|
||||
That run does not establish a passing performance protocol.
|
||||
|
||||
The post-removal functional integration checks, browser QA and unchanged-input
|
||||
handoff for #78 are recorded in [the #77 verification report](../trace-free-planning-verification.md).
|
||||
|
||||
On 2026-10-15, the #76 worktree based on `ea42561` passed 630 functional tests
|
||||
across 33 files with coverage under the 2 GB Node heap limit. Complete outcome
|
||||
comparisons against that baseline matched for all seven benchmark scenarios.
|
||||
|
||||
@@ -36,6 +36,8 @@ branch slug; stable promotions share one group and are not cancelled in progress
|
||||
Validation uses Node 24.21.0 and Bun 1.4.2 with the frozen lockfile. The image
|
||||
job uses Docker 29.8.2 and its matching Docker-in-Docker service. Scanner
|
||||
versions are Semgrep 1.180.0 and Trivy 0.75.0; Trivy downloads are checksum-checked.
|
||||
Trivy downloads its vulnerability database from Aqua's public ECR mirror because
|
||||
the default registries returned a missing database blob and a download timeout.
|
||||
Jobs select the `ubuntu-latest` label and require Linux container-job support.
|
||||
Allow at least the preserved 2 GiB Node heap plus runner and container overhead.
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Integrated planning verification after trace removal
|
||||
|
||||
Functional verification for [#77](https://gitea.nessar.fr/nessar/vac-optimizer/issues/77),
|
||||
run on 2026-10-15 against `2f4af14869c47836d2c1008157136f75c9d3e98b`
|
||||
(`develop`, including the merged #76 implementation). The original checkout was
|
||||
clean at `d9d9befa1256cd2200aa4778d28b2bc23ba141e6`; both commits have the same
|
||||
tree. This verification adds two real-worker lifecycle tests and this report.
|
||||
It does not change production behavior, saved data, placement expectations,
|
||||
benchmark inputs, CI settings or performance limits.
|
||||
|
||||
## Contract audit
|
||||
|
||||
| Path | Verified retained contract |
|
||||
| --- | --- |
|
||||
| Synchronous and cooperative core | Outcomes, valid checkpoints, final-state snapshots, stable explanations and explicit cancellation; no recorder, candidate history or historical replay API. |
|
||||
| Server calculation context | Immutable submitted configuration, independently delivered outcome and explanation, and displayed-placement date analysis. |
|
||||
| Browser without Worker | Validation and explicit `worker-unavailable` handling; no synchronous planning or trace compatibility result. |
|
||||
| Native worker and coordinator | Heartbeat, progress, checkpoint, result, explanation and final artifacts. The coordinator terminates the worker after required delivery with no acknowledgement message or queue. |
|
||||
| Calendar and remaining balances | Final and valid partial calendars, placement provenance, objective comparisons, earned sources, validity dates and concrete non-placement reasons. |
|
||||
| Alternative dates | Captured horizon, entitlement, type and rule provenance; unrelated placements stay fixed. Hypothetical comparisons never update the displayed plan. |
|
||||
| Benchmark callers | Supported calculation outcome, explanation and timings; no trace promises, waits, counts or replacement trace metrics. |
|
||||
|
||||
Production-source searches found no decision-trace, historical analysis,
|
||||
acknowledgement or replay implementation. Angular hydration's `withEventReplay`
|
||||
is unrelated to planning history. `PlanningArtifacts` carries actual explanation,
|
||||
final-state and planning-work timing data, rather than a compatibility shell.
|
||||
|
||||
## Existing deterministic fixtures
|
||||
|
||||
The full functional suite passes without changing any existing expected placement,
|
||||
balance, metric, objective, chronology or completed-stage assertion.
|
||||
|
||||
| Requirement | Existing regression coverage |
|
||||
| --- | --- |
|
||||
| Typed joint obligations and shared provenance | `leave-planner.service.spec.ts`, `leave-planner-explanation.spec.ts` and real-worker tests. |
|
||||
| Earned-entitlement chains and availability | Integration, explanation, supporting-placement and real-worker tests, including declarations and cancellation. |
|
||||
| Custom formulas and reordered priorities | Optimization, calendar-objective, work-sequence and supporting-placement tests, including real-worker comparisons. |
|
||||
| Two- and three-year horizons | Service, calendar and worker tests retain the submitted dates and deterministic completed plans. |
|
||||
| No active optimization rules | Optimization and migration tests retain declarations and remaining balances without adding placements. |
|
||||
| Stable explanations and remaining reasons | Entitlements, every contributing rule, ordered objective values, first decisive objective, periods and dated non-placement reasons. |
|
||||
| Cancellation and failures | Before and after a jointly valid incumbent, stale calculations and date analyses, worker liveness, technical budgets, and local explanation errors. |
|
||||
| Saved settings | Existing migration and persistence tests preserve user rule choices, balances, declarations and explicitly selected horizons. |
|
||||
|
||||
An additional direct comparison uses the pre-removal core from `ea42561`, with
|
||||
only its relative imports redirected to the unchanged calendar/date/objective
|
||||
modules. Both versions receive the same cloned requests and calculation date
|
||||
`2024-01-01`. Full outcomes and stable explanations are compared with deep
|
||||
equality; no placement expectations or retained explanation fields are filtered.
|
||||
|
||||
All ten completed comparisons match for both the full outcome and stable
|
||||
explanation:
|
||||
|
||||
| Fixture | Outcome | Placements | Last completed stage |
|
||||
| --- | --- | --- | --- |
|
||||
| Representative year | `Plan` | 30 | `dates` |
|
||||
| Typed joint obligations | `Plan` | 10 | `dates` |
|
||||
| Chained earned entitlements | `Plan` | 3 | `dates` |
|
||||
| Deliberate conflict | `PlanningConflict` | 0 | — |
|
||||
| Two years | `Plan` | 60 | `dates` |
|
||||
| Three years | `Plan` | 90 | `dates` |
|
||||
| Holiday value before non-working-day gain | `Plan` | 3 | `dates` |
|
||||
| Non-working-day gain before holiday value | `Plan` | 3 | `dates` |
|
||||
| Custom maximum holiday-week aggregation | `Plan` | 3 | `dates` |
|
||||
| No active optimization, with a declared day | `Plan` | 0 | `dates` |
|
||||
|
||||
The reordered priorities retain their different placement dates: 6, 7 and 10 May
|
||||
for holiday value first; 29 April, 10 May and 17 May for gain first. The zero-rule
|
||||
case retains its declaration. These are correctness comparisons, not timed
|
||||
benchmark repetitions. An exploratory pre-removal loaded-year replay was stopped
|
||||
and is not used as passing evidence; its full protocol remains with #78.
|
||||
|
||||
## Correctness and static checks
|
||||
|
||||
Environment: Node `24.18.0`, Bun `1.3.14`, Linux under WSL2.
|
||||
|
||||
```sh
|
||||
NODE_OPTIONS=--max-old-space-size=2048 bunx vitest run --coverage --maxWorkers=1 --exclude='src/app/benchmark/**'
|
||||
bunx tsc -p tsconfig.app.json --noEmit
|
||||
bunx tsc -p tsconfig.spec.json --noEmit
|
||||
bun run lint
|
||||
bun run format:check
|
||||
bunx prettier --check 'scripts/ci-*.mjs' '.gitea/workflows/*.yml'
|
||||
node scripts/ci-policy.test.mjs
|
||||
node scripts/ci-image.test.mjs
|
||||
bun run build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
All checks pass. Functional coverage: **632 tests across 33 files**, 93.56%
|
||||
statements, 85.90% branches, 95.69% functions and 95.41% lines. The full coverage
|
||||
run took 95.67 seconds with the existing 2 GB Node heap setting. The focused
|
||||
real-worker file passes all 15 tests, including the two added lifecycle checks.
|
||||
Coverage configuration and thresholds are unchanged.
|
||||
|
||||
The production build retains its existing warning budgets: initial bundle
|
||||
2.57 MB against the 2.50 MB warning and calendar-year styles 12.98 kB against
|
||||
the 8 kB warning. Both remain below their unchanged error limits.
|
||||
|
||||
## Browser QA
|
||||
|
||||
The production build was served locally with
|
||||
`NG_ALLOWED_HOSTS=localhost,127.0.0.1 PORT=4177` and exercised in headless
|
||||
Chromium `149.0.7827.55`, using native module Workers, reduced motion and
|
||||
375×915, 768×915 and 1440-pixel-wide viewports. The calculation date was fixed
|
||||
to `2024-01-01`. This is desktop functional evidence, not a phone performance run.
|
||||
|
||||
| Flow | Result |
|
||||
| --- | --- |
|
||||
| Completed calendar and explanation | Three CP placements on 6, 7 and 10 May 2024, six gained non-working dates; keyboard disclosure, placement navigation and calendar selection work. |
|
||||
| Remaining balances | Future RTT availability and unavoidable expiry show their actual dates, reasons and planning horizon without attempt/history controls. Earned-source rendering is also covered by component tests. |
|
||||
| Date comparison | An alternative on 13 May is hypothetical; placements and selected calendar dates remain unchanged. Escape closes the dialog and restores focus to Compare dates. |
|
||||
| Immutable settings | Persisting a current CP balance edit from 3 to 99 leaves the displayed three placements unchanged. Select another placement and verify that a new comparison request is sent after the edit; that request still contains the captured quantity 3 and original horizon. |
|
||||
| Cancellation before a valid plan | At 375 px, hold the native worker script download, activate Cancel with Enter, then release startup. Result is `PlanningIncomplete` / `user-cancelled`, without an incumbent or a false conflict. This controlled startup is not latency evidence. |
|
||||
| Cancellation after a valid plan | At 375 px, use the unchanged loaded-year request and cancel with Enter after a real checkpoint. All three typed obligation windows remain satisfied, placements plus remaining balances total 72, and the partial explanation is available. Inputs remain editable during planning; Cancel is at least 44 px high. |
|
||||
| Delayed and stale delivery | At 375 px, hold explanation and artifact messages after a real outcome. The three-placement calendar is usable immediately. Recalculate, then release the old messages; the current result and explanation remain intact. |
|
||||
| Explanation failure | At 375 px, inject explanation transport and checkpoint-explanation recovery failures. An inline error appears while the completed calendar and all three placements remain available. |
|
||||
| Responsive and keyboard interaction | No page-level horizontal overflow at 375, 768 or 1440 px. Comparison dialogs fit, retain keyboard focus and restore it on dismissal. No unexpected browser page errors. |
|
||||
| Removed UI | No advanced trace explorer, historical overlays, replay controls, candidate-status UI or remaining-balance attempt controls. |
|
||||
|
||||
Controlled-clock transport tests additionally verify that heartbeats renew the
|
||||
30-second inactivity watchdog across repeated windows and that monitoring ends
|
||||
at outcome delivery. Native-worker tests verify actual heartbeat delivery,
|
||||
incumbent validity and retained explanations. No automatic engine cutoff is added.
|
||||
|
||||
## Handoff to #78
|
||||
|
||||
The #70 → #71 → #72 → #73 → #74 → #75 dependency order is preserved. The tracker
|
||||
already reports those six tickets and #76 closed. This work verifies their
|
||||
functional integration and does not close or modify any other ticket.
|
||||
|
||||
The benchmark source and its original correctness assertions remain unchanged
|
||||
from the integrated baseline:
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `src/app/benchmark/leave-planner-benchmark.ts` | `16e057fa042c898d203f71e018f183c9fbecb5cd0edbf0ce0f3f25277cb00d3a` |
|
||||
| `src/app/benchmark/leave-planner-benchmark.spec.ts` | `c7b4c37b6c169b42c4d18dfc3e650b25a2ba47415c97268dafefafbfb0ee3023` |
|
||||
|
||||
All seven scenarios, typed and earned-chain fixtures, two-/three-year horizons,
|
||||
60-/90-placement expectations, determinism comparisons and final `dates` stage
|
||||
assertions remain in place. Warm-ups and measured repetitions remain unchanged.
|
||||
The full 2 GB / 300-second benchmark protocol and any required optimization stay
|
||||
open in [#78](https://gitea.nessar.fr/nessar/vac-optimizer/issues/78); they are not
|
||||
a completion gate for #75 or #77. This run makes no performance acceptance or
|
||||
trace-removal speedup claim. The separate advisory benchmark status must be
|
||||
resolved by #78 under the existing limits.
|
||||
|
||||
Local run logs, the original spec and checkout record, comparison scripts and
|
||||
browser screenshots are in `/tmp/vac-issue-77`. The source fixtures and commands
|
||||
above identify the reproducible correctness checks; temporary QA controls are
|
||||
not part of the application.
|
||||
@@ -33,6 +33,9 @@ describe('real planning worker cancellation', () => {
|
||||
let simulateLongCalculation = false;
|
||||
let simulatedElapsed = 0;
|
||||
const threads = new Set<NodeWorker>();
|
||||
const sentTypes: string[] = [];
|
||||
const receivedTypes: string[] = [];
|
||||
let terminationCount = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
directory = mkdtempSync(join(tmpdir(), 'vac-optimizer-worker-'));
|
||||
@@ -65,6 +68,9 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
heartbeats = 0;
|
||||
simulateLongCalculation = false;
|
||||
simulatedElapsed = 0;
|
||||
sentTypes.length = 0;
|
||||
receivedTypes.length = 0;
|
||||
terminationCount = 0;
|
||||
class RealWorker {
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
@@ -76,6 +82,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
simulatedElapsed = data.elapsed;
|
||||
return;
|
||||
}
|
||||
receivedTypes.push(data.type);
|
||||
if (holdArtifacts && ['explanation', 'artifacts'].includes(data.type)) {
|
||||
heldArtifactMessages.push(() => this.onmessage?.({ data } as MessageEvent));
|
||||
return;
|
||||
@@ -86,10 +93,12 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
});
|
||||
this.thread.on('error', () => this.onerror?.());
|
||||
}
|
||||
postMessage(message: unknown) {
|
||||
postMessage(message: { type: string }) {
|
||||
sentTypes.push(message.type);
|
||||
this.thread.postMessage(message);
|
||||
}
|
||||
terminate() {
|
||||
terminationCount++;
|
||||
void this.thread.terminate();
|
||||
}
|
||||
}
|
||||
@@ -104,6 +113,59 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
});
|
||||
afterAll(() => rmSync(directory, { recursive: true, force: true }));
|
||||
|
||||
it('delivers the outcome and explanation and releases the worker without acknowledgements', async () => {
|
||||
const calculation = createPlanningCalculation({
|
||||
horizon: { start: '2024-01-02', end: '2024-01-04' },
|
||||
balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-01-03' }],
|
||||
rules: [presets[0]],
|
||||
});
|
||||
const outcome = await calculation.outcome;
|
||||
expect(outcome).toMatchObject({
|
||||
kind: 'Plan',
|
||||
placements: [{ date: '2024-01-02', balanceId: 'cp', type: 'cp' }],
|
||||
lastCompletedStage: 'dates',
|
||||
});
|
||||
expect(await calculation.explanation).toMatchObject({
|
||||
kind: 'PlanExplanation',
|
||||
placements: [{ placement: { date: '2024-01-02', balanceId: 'cp' } }],
|
||||
});
|
||||
expect(await calculation.explanationError).toBeUndefined();
|
||||
expect(await calculation.timings).toEqual({ planningWorkMs: expect.any(Number) });
|
||||
expect(sentTypes).toEqual(['plan']);
|
||||
expect(receivedTypes.slice(-3)).toEqual(['result', 'explanation', 'artifacts']);
|
||||
expect(new Set(receivedTypes)).toEqual(
|
||||
new Set(['heartbeat', 'progress', 'checkpoint', 'result', 'explanation', 'artifacts']),
|
||||
);
|
||||
expect(terminationCount).toBe(1);
|
||||
});
|
||||
|
||||
it('ignores a replaced real worker’s late explanation without losing either outcome', async () => {
|
||||
const request = {
|
||||
horizon: { start: '2024-01-02', end: '2024-01-04' },
|
||||
balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-01-03' }],
|
||||
rules: [presets[0]],
|
||||
};
|
||||
holdArtifacts = true;
|
||||
const first = createPlanningCalculation(request);
|
||||
const original = await first.outcome;
|
||||
expect(original.kind).toBe('Plan');
|
||||
await vi.waitFor(() => expect(heldArtifactMessages).toHaveLength(2));
|
||||
holdArtifacts = false;
|
||||
const latest = createPlanningCalculation({ ...request, rules: [] });
|
||||
const outcome = await latest.outcome;
|
||||
expect(outcome).toMatchObject({ kind: 'Plan', placements: [] });
|
||||
const explanation = await latest.explanation;
|
||||
expect(explanation).toMatchObject({ kind: 'PlanExplanation', placements: [] });
|
||||
await latest.timings;
|
||||
for (const deliver of heldArtifactMessages) deliver();
|
||||
expect(await first.outcome).toEqual(original);
|
||||
expect(await first.explanation).toBeUndefined();
|
||||
expect(await latest.outcome).toEqual(outcome);
|
||||
expect(await latest.explanation).toEqual(explanation);
|
||||
expect(await latest.explanationError).toBeUndefined();
|
||||
expect(terminationCount).toBe(2);
|
||||
});
|
||||
|
||||
it('preserves a complete optimization configuration and active order in the real worker snapshot', async () => {
|
||||
const configuration: OptimizationConfiguration = structuredClone(
|
||||
OPTIMIZATION_PRESETS.unplacedAtRisk,
|
||||
|
||||
Reference in new issue
Block a user