Compare commits

..
Author SHA1 Message Date
nessarandClaude Sonnet 5.5 43a3a477d6 test(calendar): pin the clock for the Paris today marker
CI / policy (push) Successful in 9s
CI / validate (push) Successful in 39s
CI / build (push) Successful in 31s
CI / benchmark (push) Successful in 1m41s
CI / security (push) Successful in 1m5s
CI / test (push) Successful in 2m22s
CI / images (push) Successful in 1m9s
CI / required (push) Successful in 2s
The test of the today marker read the wall clock, so it only exercised
the Paris/local date mismatch between 22:00 and 24:00 UTC and passed
with or without the fix the rest of the day. Pin the clock to 22:16 UTC,
already the next day in Paris, so a UTC runner fails without the fix at
any time.

Refs #301

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:56:23 +02:00
nessarandClaude Sonnet 5.5 3b75bdbe3e test(planning): hold the worker until the preparation cancel arrives
The worker reads its queue only between event-loop turns, so the
calendar preparation could finish before the cancel sent on its second
progress update arrived, and the test received a complete plan. Use the
real-worker harness hold already applied to the checkpoint cancellation
test, and cancel on the first preparation update so the hold is released.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:56:23 +02:00
nessarandClaude Sonnet 5.5 0d68dfeaf8 fix(calendar): mark today on the Europe/Paris date
The calendar highlighted today from the browser's local date while the
default planning horizon starts on the Europe/Paris date, so the two
disagreed for a few hours a day (and always, for users in another time
zone). The horizon's first day was then not marked as today. This also
failed the CI test run in UTC containers between 22:00 and 24:00 UTC.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:56:23 +02:00
6 changed files with 42 additions and 38 deletions

No files matched your search

-2
View File
@@ -2,8 +2,6 @@ name: CI
on: on:
push: push:
branches: [develop, main]
tags: ['v*']
pull_request: pull_request:
types: [opened, reopened, synchronize, edited] types: [opened, reopened, synchronize, edited]
workflow_dispatch: workflow_dispatch:
+3 -3
View File
@@ -249,13 +249,13 @@ The server answers `GET` and `HEAD` only (other methods get 405). It serves file
[`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) defines Gitea Actions validation, functional coverage, production builds, security scans, and Docker image publication. [Issue #268](https://gitea.nessar.fr/nessar/vac-optimizer/issues/268) tracks activation. Actions is enabled with owner approval; publisher credentials and live workflow validation are still pending. The [activation guide](docs/gitea-actions.md) records runner, credential, permission, and merge-gate requirements. [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) defines Gitea Actions validation, functional coverage, production builds, security scans, and Docker image publication. [Issue #268](https://gitea.nessar.fr/nessar/vac-optimizer/issues/268) tracks activation. Actions is enabled with owner approval; publisher credentials and live workflow validation are still pending. The [activation guide](docs/gitea-actions.md) records runner, credential, permission, and merge-gate requirements.
The workflow runs on pushes to `develop` and `main`, `v*` tag pushes, PR opening/reopening/synchronization/edits, and manual dispatch. Other branches run CI only through their PR, so a branch without a PR gets no automatic CI or image. Manual runs validate without publishing images. Required jobs cover policy regression tests, GitFlow and Conventional Commits, ESLint, Prettier, `bun check` type checks, Bun coverage, the production build, Semgrep SAST, and Trivy dependency/secret scans. Coverage, build output, and SARIF reports are retained for seven days. The workflow runs on branch/tag pushes, PR opening/reopening/synchronization/edits, and manual dispatch. Manual runs validate without publishing images. Required jobs cover policy regression tests, GitFlow and Conventional Commits, ESLint, Prettier, `bun check` type checks, Bun coverage, the production build, Semgrep SAST, and Trivy dependency/secret scans. Coverage, build output, and SARIF reports are retained for seven days.
Jobs that run project commands install Bun 1.4.3 with `scripts/install-bun.sh` (a checksum-verified download, no npm) and run their steps with a `PATH` that holds Bun but no Node. The job containers keep Node only for Gitea's checkout and artifact actions, which are JavaScript actions the runner executes itself; see the [activation guide](docs/gitea-actions.md#runners-and-permissions). Jobs that run project commands install Bun 1.4.3 with `scripts/install-bun.sh` (a checksum-verified download, no npm) and run their steps with a `PATH` that holds Bun but no Node. The job containers keep Node only for Gitea's checkout and artifact actions, which are JavaScript actions the runner executes itself; see the [activation guide](docs/gitea-actions.md#runners-and-permissions).
Functional tests run with `bun test --isolate`; coverage is line-level only (Bun reports no branch or function data). The separate required benchmark job runs without coverage in a container limited to 3 GiB (JavaScriptCore has no heap-size flag, so this replaces the former 2 GiB Node heap cap) and keeps its 240-second hang guard, one warm-up and two measured repetitions of all seven scenarios. [#78](https://gitea.nessar.fr/nessar/vac-optimizer/issues/78) restored this gate after a complete local pass in 298.6 seconds; see [the measurements and hardware limits](docs/benchmark/issue-78.md). After [#302](docs/benchmark/issue-302.md) the local protocol takes about 41 seconds. Benchmark success is required for image publication and the aggregate `required` job. The lowered hang guard and tightened multipliers still need confirmation on the CI runners. Functional tests run with `bun test --isolate`; coverage is line-level only (Bun reports no branch or function data). The separate required benchmark job runs without coverage in a container limited to 3 GiB (JavaScriptCore has no heap-size flag, so this replaces the former 2 GiB Node heap cap) and keeps its 240-second hang guard, one warm-up and two measured repetitions of all seven scenarios. [#78](https://gitea.nessar.fr/nessar/vac-optimizer/issues/78) restored this gate after a complete local pass in 298.6 seconds; see [the measurements and hardware limits](docs/benchmark/issue-78.md). After [#302](docs/benchmark/issue-302.md) the local protocol takes about 41 seconds. Benchmark success is required for image publication and the aggregate `required` job. The lowered hang guard and tightened multipliers still need confirmation on the CI runners.
The `required` job aggregates successful validation and expected image publication, accepting the image job's intentional skip for fork PRs and manual runs. Image jobs require successful validation and are limited to pushes to `develop`, `main` and release tags, and same-repository PRs. Registry credentials are confined to the publishing step. The `required` job aggregates successful validation and expected image publication, accepting the image job's intentional skip for fork PRs and manual runs. Image jobs require successful validation and are limited to pushes and same-repository PRs. Registry credentials are confined to the publishing step.
Both protected branches still require `gitlab-to-gitea/ci-conversion-required` until the owner approves the first merge message and a successful Gitea PR run, then replaces the migration gate with the actual pull-request context for `required`. Local validation does not satisfy this status. Historical CI reports and artifacts remain in administrator rollback backups; the original configuration is retained in Git history. Both protected branches still require `gitlab-to-gitea/ci-conversion-required` until the owner approves the first merge message and a successful Gitea PR run, then replaces the migration gate with the actual pull-request context for `required`. Local validation does not satisfy this status. Historical CI reports and artifacts remain in administrator rollback backups; the original configuration is retained in Git history.
@@ -271,7 +271,7 @@ The image publisher implements this tag contract in `gitea.nessar.fr/nessar/vac-
| Trigger | Published image tags | | Trigger | Published image tags |
| --- | --- | | --- | --- |
| Successful push to `develop` or `main`, or same-repository PR | `branch-<normalized branch slug>` and `sha-<full commit SHA>` | | Successful push to any branch | `branch-<normalized branch slug>` and `sha-<full commit SHA>` |
| Explicit release tag `vX.Y.Z` | `vX.Y.Z` and `latest`, using the existing SHA image | | Explicit release tag `vX.Y.Z` | `vX.Y.Z` and `latest`, using the existing SHA image |
For example, `feature/calendar` uses `branch-feature-calendar`; integration uses `branch-develop`. Preserve the previous slug normalization: lowercase the branch name, replace characters other than `a-z` and `0-9` with `-`, truncate to 63 bytes, and remove leading/trailing `-`. Choose branch names with distinct slugs. Only valid release tags can update `latest`. Automatic image publication and tag promotion begin after the owner provisions publisher credentials and validates publication permissions. For example, `feature/calendar` uses `branch-feature-calendar`; integration uses `branch-develop`. Preserve the previous slug normalization: lowercase the branch name, replace characters other than `a-z` and `0-9` with `-`, truncate to 63 bytes, and remove leading/trailing `-`. Choose branch names with distinct slugs. Only valid release tags can update `latest`. Automatic image publication and tag promotion begin after the owner provisions publisher credentials and validates publication permissions.
+20 -24
View File
@@ -11,13 +11,10 @@ message.
## Jobs and triggers ## Jobs and triggers
Pushes to `develop` and `main` and `v*` tag pushes run CI. Other branches Every branch and tag push runs CI. Pull requests run on opening, reopening,
(feature, renovate, release, hotfix) run it only through their pull request; a synchronization, and title/body edits. Manual dispatch runs validation without
branch without a PR gets no automatic CI or image, so open a draft PR to get publishing images. There is no schedule or deployment job in the preserved
one. Pull requests run on opening, reopening, synchronization, and title/body configuration; the Compose files remain manual deployment options.
edits. Manual dispatch runs validation without publishing images. There is no
schedule or deployment job in the preserved configuration; the Compose files
remain manual deployment options.
| Job | Behavior | | Job | Behavior |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -30,19 +27,18 @@ remain manual deployment options.
| `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag, running the publisher with Bun. Runs only for pushes and same-repository PRs. | | `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag, running the publisher with Bun. Runs only for pushes and same-repository PRs. |
| `required` | Fails on failed, cancelled, or skipped required validation, including the benchmark. Requires image publication when expected; accepts its intentional skip for fork PRs and manual dispatch. It is shell only and needs no Bun. | | `required` | Fails on failed, cancelled, or skipped required validation, including the benchmark. Requires image publication when expected; accepts its intentional skip for fork PRs and manual dispatch. It is shell only and needs no Bun. |
The benchmark gates image publication (`images.needs` includes `benchmark`) and The benchmark runs on both push and pull_request events: the push run gates image
the `required` aggregate. The relative gate absorbs a slower runner and load publication (`images.needs` includes `benchmark`) and the `required` aggregate.
that lasts through the whole job, because calibration runs in the same job. It The relative gate absorbs a slower runner and load that lasts through the whole
calibrates before and after the protocol and judges against the slower value, job, because calibration runs in the same job. It calibrates before and after the
so it also absorbs load that starts during the protocol, such as another run protocol and judges against the slower value, so it also absorbs load that starts
sharing the runner. It does not absorb load that ends before the second during the protocol, such as an overlapping push and pull_request run of the same
calibration; the 240-second hang guard still bounds those runs. commit. It does not absorb load that ends before the second calibration; the
240-second hang guard still bounds those runs.
Because branch pushes outside `develop` and `main` do not trigger CI, a commit Push and PR runs are separate so PR title and rebase checks run on the PR event.
on a PR branch runs only on PR events, which carry the PR title and rebase New runs cancel older runs for the same event/ref. Image jobs serialize per
checks. New runs cancel older runs for the same event/ref. Image jobs serialize branch slug; stable promotions share one group and are not cancelled in progress.
per branch slug; stable promotions share one group and are not cancelled in
progress.
## Runners and permissions ## Runners and permissions
@@ -116,10 +112,10 @@ only one place fails CI instead of drifting.
## Images and releases ## Images and releases
Images use the [Gitea container registry](https://docs.gitea.com/usage/packages/container/) Images use the [Gitea container registry](https://docs.gitea.com/usage/packages/container/)
at `gitea.nessar.fr/nessar/vac-optimizer`. Pushes to `develop` and `main` and at `gitea.nessar.fr/nessar/vac-optimizer`. Branch pushes and same-repository PRs
same-repository PRs publish `branch-<slug>` and `sha-<full commit SHA>`. Slugs publish `branch-<slug>` and `sha-<full commit SHA>`. Slugs retain the previous
retain the previous lowercase, non-alphanumeric-to-hyphen, 63-byte lowercase, non-alphanumeric-to-hyphen, 63-byte normalization; choose distinct
normalization; choose distinct branch slugs. branch slugs.
Only stable `vX.Y.Z` tags matching the tagged commit's `package.json` version Only stable `vX.Y.Z` tags matching the tagged commit's `package.json` version
and reachable from `main` may promote images. Promotion first pulls the existing and reachable from `main` may promote images. Promotion first pulls the existing
@@ -152,7 +148,7 @@ secrets. Complete the remaining activation checks in this order:
`git show 0d900fd72d1a405d257f5a566c55cc0cb3d2bc61:.gitlab-ci.yml`. `git show 0d900fd72d1a405d257f5a566c55cc0cb3d2bc61:.gitlab-ci.yml`.
2. Publish the reviewed feature branch and open its PR to register the workflow. 2. Publish the reviewed feature branch and open its PR to register the workflow.
Once registered, manual dispatch verifies validation and artifacts without Once registered, manual dispatch verifies validation and artifacts without
publishing images. PR validation can also run now; its image and publishing images. Push and PR validation can also run now; their image and
aggregate checks remain blocked until publisher secrets are provisioned. aggregate checks remain blocked until publisher secrets are provisioned.
3. Verify runner isolation and Docker service support, then provision the two 3. Verify runner isolation and Docker service support, then provision the two
publisher secrets. Review the package publisher's permissions and registry publisher secrets. Review the package publisher's permissions and registry
+12
View File
@@ -8,6 +8,7 @@ import {
SelectedDates, SelectedDates,
} from './calendar-service'; } from './calendar-service';
import { DateRange } from '@angular/material/datepicker'; import { DateRange } from '@angular/material/datepicker';
import { vi } from '../../testing/vitest-compat';
describe('CalendarService', () => { describe('CalendarService', () => {
let service: CalendarService; let service: CalendarService;
@@ -197,6 +198,8 @@ describe('CalendarService', () => {
service.project({ placements: [], horizon }); service.project({ placements: [], horizon });
const day = (key: string) => new Date(`${key}T00:00:00`); const day = (key: string) => new Date(`${key}T00:00:00`);
afterEach(() => vi.useRealTimers());
it('projects weekends, holidays and today for the default planning horizon', () => { it('projects weekends, holidays and today for the default planning horizon', () => {
const { selections } = project({ start: '2026-01-01', end: '2027-01-01' }); const { selections } = project({ start: '2026-01-01', end: '2027-01-01' });
expect(selections.dayTypes(day('2026-05-09'))).toContain(DayType.WEEKEND); expect(selections.dayTypes(day('2026-05-09'))).toContain(DayType.WEEKEND);
@@ -207,6 +210,15 @@ describe('CalendarService', () => {
); );
}); });
it('marks today on the Europe/Paris date when the runtime date is still the day before', () => {
vi.setSystemTime(new Date('2026-10-17T22:16:00Z'));
const horizon = service.getDefaultPlanningHorizon();
expect(horizon.start).toBe('2026-10-18');
const { selections } = project(horizon);
expect(selections.getClasses(day('2026-10-18'))).toContain(DayType.TODAY);
expect(selections.getClasses(day('2026-10-17'))).not.toContain(DayType.TODAY);
});
it('lists the covered months, including partial boundary months and year changes', () => { it('lists the covered months, including partial boundary months and year changes', () => {
expect(project({ start: '2024-11-20', end: '2025-02-02' }).months).toEqual([ expect(project({ start: '2024-11-20', end: '2025-02-02' }).months).toEqual([
{ monthIndex: 10, year: 2024 }, { monthIndex: 10, year: 2024 },
+4 -6
View File
@@ -85,11 +85,9 @@ export class CalendarService {
const holidays = new Set( const holidays = new Set(
this.getHolidays(first.getFullYear(), last.getFullYear()).map((h) => h.getTime()), this.getHolidays(first.getFullYear(), last.getFullYear()).map((h) => h.getTime()),
); );
const { const [todayYear, todayMonth, todayDate] = planningDateInParis(new Date())
date: todayDate, .split('-')
month: todayMonth, .map(Number);
year: todayYear,
} = this.dateCache.getTodayComponents();
const months: CalendarProjection['months'] = []; const months: CalendarProjection['months'] = [];
const ranges: SelectedDate[] = []; const ranges: SelectedDate[] = [];
const cursor = new Date(first); const cursor = new Date(first);
@@ -124,7 +122,7 @@ export class CalendarService {
); );
if ( if (
day.getDate() === todayDate && day.getDate() === todayDate &&
day.getMonth() === todayMonth && day.getMonth() + 1 === todayMonth &&
day.getFullYear() === todayYear day.getFullYear() === todayYear
) )
ranges.push(new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day))); ranges.push(new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day)));
@@ -587,8 +587,8 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
); );
it('stops during calendar preparation without a plan or an obligation conflict', async () => { it('stops during calendar preparation without a plan or an obligation conflict', async () => {
holdForCancelAtStage = 'preparation';
const stages: string[] = []; const stages: string[] = [];
let preparationUpdates = 0;
const calculation = createPlanningCalculation({ const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2027-01-01' }, horizon: { start: '2024-01-01', end: '2027-01-01' },
balances: [], balances: [],
@@ -596,7 +596,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
declaredDays: [{ date: '2024-01-02', type: 'other' }], declaredDays: [{ date: '2024-01-02', type: 'other' }],
progress: (stage) => { progress: (stage) => {
stages.push(stage); stages.push(stage);
if (stage === 'preparation' && ++preparationUpdates === 2) calculation.cancel(); if (stage === 'preparation') calculation.cancel();
}, },
}); });
const outcome = await calculation.outcome; const outcome = await calculation.outcome;
@@ -606,7 +606,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
}); });
if (outcome.kind !== 'PlanningIncomplete') return; if (outcome.kind !== 'PlanningIncomplete') return;
expect(outcome.incumbent).toBeUndefined(); expect(outcome.incumbent).toBeUndefined();
expect(preparationUpdates).toBeGreaterThanOrEqual(2); expect(stages).toContain('preparation');
expect(stages).not.toContain('obligations'); expect(stages).not.toContain('obligations');
expect(await calculation.explanationError).toBeUndefined(); expect(await calculation.explanationError).toBeUndefined();
expect(heartbeats).toBeGreaterThan(0); expect(heartbeats).toBeGreaterThan(0);