Skip to content

Commit 9ca303e

Browse files
claude[bot]os-zhuangclaude
authored
fix(pm): give git-history's self-test windows a complete instant, not a bare date (#10811)
`git rev-list --since=2026-06-20` is an approxidate: git fills the missing time of day from the CURRENT wall clock, not from midnight. The self-test's fixture stamps its 40 commits at 12:00:00Z, so the `--since` edge swept across c19 (2026-06-20T12:00:00Z) once a day — 21 commits before 12:00 UTC, 20 after. The self-test was therefore green every morning and red every afternoon, failing `Lint & Repo Gates` for every PR in the repo and getting queued PRs evicted on rebuild. Every window edge in the self-test is now a complete UTC instant (`2026-06-20T00:00:00Z`), which git parses exactly and never approxidates. Placing each edge at 00:00:00Z also leaves 12 h — half the fixture's daily cadence, the widest gap available — between it and the nearest commit stamp. A new case recomputes both properties from the window constants, so the defect class cannot return silently. The tool's own `--since` handling is unchanged; only the question the test asks is. Counts stay exact (21 / 3 / 5) — no assertion was loosened. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <jack@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent f4e5d91 commit 9ca303e

1 file changed

Lines changed: 48 additions & 13 deletions

File tree

scripts/pm/git-history.mjs

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,41 @@ function selfTest() {
454454
t('splitRemoteRef refuses an unknown remote', splitRemoteRef('upstream/main', ['origin']) === null);
455455

456456
// ── real repos ────────────────────────────────────────────────────────────
457+
// Every window edge below is a COMPLETE UTC instant, never a bare
458+
// `YYYY-MM-DD`. `git rev-list --since=2026-06-20` is an *approxidate*: git
459+
// fills the missing time of day from the CURRENT WALL CLOCK, not from
460+
// midnight. With this fixture stamped 12:00:00Z, that edge swept across c19
461+
// once a day — 21 commits before 12:00 UTC, 20 after — so this self-test was
462+
// green every morning and red every afternoon (measured 2026-08-21: three
463+
// off-by-one failures, ~10 h red on `main`, `Lint & Repo Gates` failing for
464+
// every PR and the merge queue evicting them on rebuild). A complete instant
465+
// is parsed exactly and never consults `now`; putting each edge at 00:00:00Z
466+
// additionally leaves 12 h — half the fixture's daily cadence, the widest gap
467+
// available — between it and the nearest commit stamp. `collect-release-notes.sh
468+
// --self-test`, which runs over an identical fixture in the same `lint.yml`
469+
// step, has always spelled its window this way.
470+
const FIXTURE_EPOCH = '2026-06-01T12:00:00Z';
471+
const FIXTURE_COMMITS = 40;
472+
const WINDOW_SINCE = '2026-06-20T00:00:00Z';
473+
const WINDOW_UNTIL = '2026-07-11T00:00:00Z';
474+
const NARROW_SINCE = '2026-07-08T00:00:00Z';
475+
476+
// Both halves of that property, recomputed from the constants rather than
477+
// asserted about them, so moving a window or re-cadencing the fixture re-runs
478+
// the check instead of dating it. A bare date fails the shape test; a stamp
479+
// or edge nudged toward its neighbour fails the gap test.
480+
const stampsMs = Array.from({ length: FIXTURE_COMMITS }, (_, i) => Date.parse(FIXTURE_EPOCH) + i * day);
481+
const edges = [WINDOW_SINCE, WINDOW_UNTIL, NARROW_SINCE];
482+
const edgesAreCompleteInstants = edges.every((e) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(e));
483+
const closestEdgeMs = Math.min(
484+
...edges.map((e) => Math.min(...stampsMs.map((stamp) => Math.abs(Date.parse(e) - stamp)))),
485+
);
486+
t('every window edge is a COMPLETE instant and clears every fixture stamp by hours — a bare '
487+
+ 'YYYY-MM-DD is approxidated to the CURRENT time of day, which is what made this self-test '
488+
+ 'pass before 12:00 UTC and fail after it',
489+
edgesAreCompleteInstants && closestEdgeMs >= 6 * 60 * 60 * 1000,
490+
`complete=${edgesAreCompleteInstants} closest edge-to-stamp gap ${closestEdgeMs / (60 * 60 * 1000)}h`);
491+
457492
const root = mkdtempSync(join(tmpdir(), 'git-history-selftest-'));
458493
const g = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
459494
try {
@@ -463,8 +498,8 @@ function selfTest() {
463498
g(['config', 'user.email', 'selftest@objectstack.ai'], up);
464499
g(['config', 'user.name', 'selftest'], up);
465500
// 40 commits, one per day, oldest first: 2026-06-01 .. 2026-07-10.
466-
for (let i = 0; i < 40; i += 1) {
467-
const d = new Date(Date.parse('2026-06-01T12:00:00Z') + i * day).toISOString();
501+
for (let i = 0; i < FIXTURE_COMMITS; i += 1) {
502+
const d = new Date(Date.parse(FIXTURE_EPOCH) + i * day).toISOString();
468503
writeFileSync(join(up, 'f.txt'), `commit ${i}\n`);
469504
g(['add', 'f.txt'], up);
470505
execFileSync('git', ['commit', '--quiet', '-m', `c${i}`], {
@@ -488,7 +523,7 @@ function selfTest() {
488523
return { stdout: String(r.stdout || ''), stderr: String(r.stderr || ''), code: r.status };
489524
};
490525

491-
const fullAnswer = runCliAllowFail(['count', '--since=2026-06-20', '--until=2026-07-11'], full);
526+
const fullAnswer = runCliAllowFail(['count', `--since=${WINDOW_SINCE}`, `--until=${WINDOW_UNTIL}`], full);
492527
t('a complete clone answers, exit 0', fullAnswer.code === 0, JSON.stringify(fullAnswer));
493528
t('and the answer is the real one (21 commits: the daily fixture commits i=19..39)',
494529
fullAnswer.stdout.trim() === '21', `got ${JSON.stringify(fullAnswer.stdout)}`);
@@ -497,11 +532,11 @@ function selfTest() {
497532
const shallow = join(root, 'shallow');
498533
g(['clone', '--quiet', '--depth=5', `file://${up}`, shallow], root);
499534
t('the shallow fixture really is shallow', isShallow(shallow) === true);
500-
const raw = g(['rev-list', '--count', '--first-parent', '--since=2026-06-20', '--until=2026-07-11', 'origin/main'], shallow).trim();
535+
const raw = g(['rev-list', '--count', '--first-parent', `--since=${WINDOW_SINCE}`, `--until=${WINDOW_UNTIL}`, 'origin/main'], shallow).trim();
501536
t('BASELINE — raw git answers the same question with a wrong number and no warning '
502537
+ '(this is the defect, reproduced)', raw === '5' && raw !== '20', `raw git said ${raw}`);
503538

504-
const refused = runCliAllowFail(['count', '--since=2026-06-20', '--until=2026-07-11', '--no-fetch'], shallow);
539+
const refused = runCliAllowFail(['count', `--since=${WINDOW_SINCE}`, `--until=${WINDOW_UNTIL}`, '--no-fetch'], shallow);
505540
t('the helper REFUSES that same question rather than answering it', refused.code === 2, `exit ${refused.code}`);
506541
t('and stdout stays EMPTY, so a captured number is empty rather than plausible '
507542
+ '(zero is a broken scan, not a clean repo — #4690)', refused.stdout.trim() === '',
@@ -511,7 +546,7 @@ function selfTest() {
511546
refused.stderr);
512547

513548
// Deepening from a real (local) remote makes the same question answerable.
514-
const deepened = runCliAllowFail(['count', '--since=2026-06-20', '--until=2026-07-11'], shallow);
549+
const deepened = runCliAllowFail(['count', `--since=${WINDOW_SINCE}`, `--until=${WINDOW_UNTIL}`], shallow);
515550
t('with fetching allowed it deepens and then answers, exit 0', deepened.code === 0, JSON.stringify(deepened));
516551
t('and the answer now MATCHES the complete clone', deepened.stdout.trim() === '21',
517552
`got ${JSON.stringify(deepened.stdout)}`);
@@ -524,7 +559,7 @@ function selfTest() {
524559
// A shallow clone deep enough for the asked window answers with NO fetch.
525560
const shallowDeep = join(root, 'shallow-deep');
526561
g(['clone', '--quiet', '--depth=5', `file://${up}`, shallowDeep], root);
527-
const narrow = runCliAllowFail(['count', '--since=2026-07-08', '--until=2026-07-11', '--no-fetch'], shallowDeep);
562+
const narrow = runCliAllowFail(['count', `--since=${NARROW_SINCE}`, `--until=${WINDOW_UNTIL}`, '--no-fetch'], shallowDeep);
528563
t('a still-shallow clone whose floor predates the window answers WITHOUT fetching '
529564
+ '(a bare is-shallow guard would have refused this correct answer)',
530565
narrow.code === 0 && narrow.stdout.trim() === '3', JSON.stringify(narrow));
@@ -533,14 +568,14 @@ function selfTest() {
533568
+ 'not the shallow flag', isShallow(shallowDeep) === true);
534569

535570
// ── historyHorizon: the read-only reading the #9902 adopters call ───────
536-
const hFull = historyHorizon({ cwd: full, ref: 'origin/main', sinceMs: Date.parse('2026-06-20') });
571+
const hFull = historyHorizon({ cwd: full, ref: 'origin/main', sinceMs: Date.parse(WINDOW_SINCE) });
537572
t('historyHorizon clears a complete clone and reports no floor',
538573
hFull.covered === true && hFull.shallow === false && hFull.floor === null && hFull.remedy === null,
539574
JSON.stringify(hFull));
540575
t('and it carries the ref tip, so an allowed answer can still be printed with its horizon',
541576
/^\d{4}-\d{2}-\d{2}$/.test(hFull.tip), JSON.stringify(hFull));
542577

543-
const hShort = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse('2026-06-20') });
578+
const hShort = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse(WINDOW_SINCE) });
544579
t('historyHorizon REFUSES the window raw git answered with 5 instead of 21',
545580
hShort.covered === false, JSON.stringify(hShort));
546581
t('and it names the floor rather than only saying "shallow"',
@@ -552,22 +587,22 @@ function selfTest() {
552587
+ 'newer than the floor already present',
553588
remedyDate !== undefined && Date.parse(remedyDate) <= Date.parse('2026-07-06'), String(hShort.remedy));
554589

555-
const hNarrow = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse('2026-07-08') });
590+
const hNarrow = historyHorizon({ cwd: shallowDeep, ref: 'origin/main', sinceMs: Date.parse(NARROW_SINCE) });
556591
t('a shallow clone whose floor predates the window is CLEARED by historyHorizon too — '
557592
+ 'the adopters must not refuse answers that are provably right',
558593
hNarrow.covered === true && hNarrow.shallow === true, JSON.stringify(hNarrow));
559594
t('and a cleared shallow clone still reports its floor, so the number travels with its horizon',
560595
hNarrow.floor === '2026-07-06', JSON.stringify(hNarrow));
561596

562597
t('an unresolvable ref is refused rather than read as covered',
563-
historyHorizon({ cwd: shallowDeep, ref: 'origin/nope', sinceMs: Date.parse('2026-07-08') }).covered === false);
598+
historyHorizon({ cwd: shallowDeep, ref: 'origin/nope', sinceMs: Date.parse(NARROW_SINCE) }).covered === false);
564599

565600
// No remote to deepen from: refuse, never answer.
566601
const orphan = join(root, 'orphan');
567602
g(['clone', '--quiet', '--depth=5', `file://${up}`, orphan], root);
568603
g(['remote', 'remove', 'origin'], orphan);
569604
g(['update-ref', 'refs/heads/probe', g(['rev-parse', 'HEAD'], orphan).trim()], orphan);
570-
const noRemote = runCliAllowFail(['count', '--since=2026-06-20', '--ref=probe'], orphan);
605+
const noRemote = runCliAllowFail(['count', `--since=${WINDOW_SINCE}`, '--ref=probe'], orphan);
571606
t('with no remote to deepen from it refuses instead of answering from what is there',
572607
noRemote.code === 2, JSON.stringify(noRemote));
573608
t('and it says so by name', /no remote|does not resolve/.test(noRemote.stderr || ''), noRemote.stderr);
@@ -578,7 +613,7 @@ function selfTest() {
578613
badSince.code === 1, JSON.stringify(badSince));
579614

580615
// ensure answers nothing at all
581-
const ens = runCliAllowFail(['ensure', '--since=2026-07-08'], shallowDeep);
616+
const ens = runCliAllowFail(['ensure', `--since=${NARROW_SINCE}`], shallowDeep);
582617
t('ensure proves coverage and prints no number', ens.code === 0 && ens.stdout.trim() === '',
583618
JSON.stringify(ens));
584619
} finally {

0 commit comments

Comments
 (0)