Files
msd-core/tests/clock.test.cjs
Tom Boucher a875372f18 test(#3335): fold the workflow-content & phase-lifecycle fix-* cluster — Wave 3 (#3373)
* test(#3335): fold the workflow-content & phase-lifecycle fix-* cluster — Wave 3

Folds 13 legacy fix-*.test.cjs regression files (131 test() blocks) into
their module's main suite, per H3 (#3315) of the test-hygiene epic (#3053):

- 6 files with no prior target coverage: renamed (git mv) into new suites
  (spike-manifest-scoping, ship-note, add-todo, workflow-jq-dependency,
  resolve-execution-dynamic-routing, clock)
- 7 files merged into 5 pre-existing suites (worktree-base-ref x2,
  model-resolver, phase-locator x2, frontmatter, verification-status),
  deduplicated against existing coverage

Zero net test-coverage loss: every source assertion preserved or verified
as a genuine pre-existing duplicate. No production code changed.

Last fix-* wave (Wave 1 #3341, Wave 2 #3342 already merged); 4 issue-*
waves remain in #3315.

* test(#3335): fix orthogonal-review findings — Wave 3 fold

Standards-axis review + Memtrace graph pass found real defects in the
just-folded suites, all fixed here:

- phase-locator.test.cjs: pinned an unseeded fast-check property test
  (CONTRIBUTING.md determinism requirement), matching the sibling test's
  seed:7 convention.
- phase-locator.test.cjs: added assert.ok() presence guards after 9
  data.plans.find() calls that were dereferenced unguarded, inconsistent
  with 5 sibling tests in the same file that already guard correctly.
  Latent robustness gap — an omitted plan would throw an opaque TypeError
  instead of a clear assertion failure.
- Standardized the fold-wrapper convention (block-scoped __foldDescribe)
  across worktree-base-ref.test.cjs, verification-status.test.cjs, and
  phase-locator.test.cjs to match the pattern already established in
  frontmatter.test.cjs and model-resolver.test.cjs from earlier folds.
- worktree-base-ref.test.cjs: moved a mid-file require to the top-of-file
  require block.
- Eliminated duplicated env-isolation helpers: model-resolver.test.cjs and
  phase-locator.test.cjs each reimplemented GSD_WORKSTREAM/GSD_PROJECT
  save-restore independently; factored a shared isolateWorkstreamEnv()/
  restoreWorkstreamEnv() into tests/helpers.cjs and pointed both call
  sites at it.

No test() count changed in any file. No production code touched.

---------

Co-authored-by: sim <sim@local>
2026-08-11 22:23:55 -04:00

122 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* #2136 — operator-facing date-only fields must use the HOST-LOCAL calendar day,
* not the UTC calendar day.
*
* `clock.today()` derived the day by slicing a UTC ISO instant
* (`nowIso().split('T')[0]`), so in any negative-UTC-offset zone during UTC's
* early hours `last_activity` named a day the operator had not reached yet — a
* day AHEAD of `last_updated`'s local date, written by the same call.
*
* The fix adds `clock.localToday()` (host-local calendar day, honoring the same
* GSD_NOW_MS pin) and routes operator-facing date-only fields through it. These
* tests prove:
* (1) realClock.localToday() returns the local day under a pinned instant + TZ;
* (2) realClock.today() is unchanged (UTC) — internal/cosmetic stamps stay UTC;
* (3) the fake clock mirrors localToday();
* (4) the `last_activity` field is stamped from localToday, not today
* (a split clock where the two differ proves the wiring).
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const CLOCK_CJS = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'clock.cjs');
const STATE_TRANSITION_CJS = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'state-transition.cjs');
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
const { makeFakeClock } = require('./helpers/clock.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
// 2020-06-15T02:00:00.000Z. Under America/Chicago (UTC−5 in June) this instant
// is 2020-06-14 21:00 local — the local calendar day is 2020-06-14 while the UTC
// calendar day is 2020-06-15. This is the issue's exact repro instant.
const PINNED_MS = '1592186400000';
/**
* Run a one-liner in a FRESH subprocess so TZ is set at process start (Node
* caches timezone; runtime mutation of process.env.TZ is unreliable across
* versions). GSD_TEST_MODE + GSD_NOW_MS pin realClock via the documented seam.
*/
function clockInSubprocess(expr, env) {
const result = runNode(['-e', expr], { env, timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(result, `node -e ${expr}`);
return result.stdout.trim();
}
describe('#2136 realClock.localToday() — host-local calendar day', () => {
test('returns the LOCAL calendar day under a negative-UTC-offset zone', () => {
const out = clockInSubprocess(
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.localToday());`,
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'America/Chicago' },
);
assert.strictEqual(out, '2020-06-14',
'localToday() must be the local calendar day (2020-06-14 in Chicago), not the UTC day (2020-06-15)');
});
test('realClock.today() is unchanged (UTC calendar day) — internal stamps stay UTC', () => {
const out = clockInSubprocess(
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.today());`,
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'America/Chicago' },
);
assert.strictEqual(out, '2020-06-15',
'today() must remain the UTC calendar day; only operator-facing fields moved to localToday()');
});
test('localToday === today on a UTC host (no spurious divergence)', () => {
const out = clockInSubprocess(
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.localToday()+'|'+realClock.today());`,
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'UTC' },
);
const [local, utc] = out.split('|');
assert.strictEqual(local, '2020-06-15');
assert.strictEqual(utc, '2020-06-15');
});
});
describe('#2136 makeFakeClock mirrors localToday()', () => {
test('fake clock localToday() derives the local day from the pinned epoch', () => {
const clock = makeFakeClock(Number(PINNED_MS));
// The fake honors the host TZ of the test process; assert it returns a
// YYYY-MM-DD string derived from local Date methods (structure + determinism
// under the pinned epoch), mirroring realClock.localToday().
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(clock.localToday()),
'fake localToday() must return a YYYY-MM-DD string');
assert.strictEqual(typeof clock.localToday, 'function',
'makeFakeClock must expose localToday so it stays a drop-in Clock substitute');
});
});
describe('#2136 last_activity is stamped from localToday, not today', () => {
// A clock where the UTC day and the local day DIFFER. If the transition core
// still consulted today(), Last Activity would read the UTC value; the fix
// routes it through localToday().
const splitClock = Object.freeze({
today: () => '2020-06-15',
localToday: () => '2020-06-14',
nowIso: () => '2020-06-15T02:00:00.000Z',
});
test('sync transition writes the LOCAL day into Last Activity', () => {
const { transitionCore } = require(STATE_TRANSITION_CJS);
const input = [
'# Project State',
'',
'**Total Plans in Phase:** 2',
'**Last Activity:** 2020-06-10',
'**Progress:** [████░░░░░░] 40%',
'',
].join('\n');
const result = transitionCore(
input,
{ kind: 'sync', totalPlansInPhase: 5, percent: 60 },
{ clock: splitClock },
);
assert.strictEqual(stateExtractField(result.content, 'Last Activity'), '2020-06-14',
'Last Activity must use localToday() (2020-06-14), not today() (2020-06-15)');
});
});