Problem class: gitreins-foreman-resets-head-during-verify
I diagnosed the failure, reproduced it standalone, verified the fix, and wrote the self-contained solution to /workspace/solution.md.
Key finding: the class label is a confound. The real defect is that endOfDueDayUtc() only parses bare YYYY-MM-DD strings and returns undefined for the full ISO-8601 timestamps / Date objects the t780 convention now feeds it. All three failing assertions read a value derived from that helper. The foreman HEAD reset can additionally discard an uncommitted fix, so the solution covers both.
Reproduction (run against a date-only-only parser):
buggy date-only : 2024-03-15T23:59:59.999Z
buggy full ISO : undefined <-- failure
buggy Date obj : undefined <-- failure
Fix verification (TZ-independent, date-only/ISO/Date/missing/invalid cases):
$ node runtests.mjs
all assertions passed
endOfDueDayUtc() returns undefined for ISO-8601 / Date inputsProblem class: gitreins-foreman-resets-head-during-verify
Repo: eduos.dexdat.com.co · Module: apps/api/src/modules/assessment
Suite: vitest run --no-file-parallelism (pnpm, Node 22, per-file workers)
Failing assertions: assessment.test.ts:502:28, assessment.test.ts:752:28, gradebook.test.ts:446:21
The three failures are not independent. They all read a value produced by endOfDueDayUtc() in apps/api/src/modules/assessment/services/submission-service.ts, and that function returns undefined whenever its argument is not a bare YYYY-MM-DD string.
After the t780 convention (commit 58ae08a9) the assessment module began persisting/serializing due days as full ISO-8601 UTC timestamps (2024-03-15T00:00:00.000Z) and, in a few code paths, as Date objects. The cutoff helper was never widened, so it silently fell through its guard and returned undefined. The downstream field was therefore undefined, giving the two assertion flavours seen in the log:
expect(undefined).toContain("<string>") → "the given combination of arguments (undefined and string) is invalid for this assertion"expect(undefined).toBeDefined() → "expected undefined to be defined"The class label (…foreman-resets-head-during-verify) is a confound: if the fix is left uncommitted, a verifier that resets the working tree to HEAD re-runs the pre-fix code and reproduces exactly the same 3 failures. Both the code fix and the “keep the tree during verify” step are below.
All three stack frames converge on the same source:
endOfDueDayUtc() -> submission.dueAt / late-cutoff -> assertion
The helper is the only shared dependency, which is why 3 unrelated-looking tests flip together (3 failed / 4350 passed).
The version introduced with the t780 convention parses only date-only strings and bails on everything else:
export function endOfDueDayUtc(dueDay?: string): Date | undefined {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dueDay ?? "");
if (!m) return undefined; // <-- full ISO / Date inputs die here
const [, y, mo, d] = m;
return new Date(Date.UTC(+y, +mo - 1, +d, 23, 59, 59, 999));
}
So the convention change (ISO-8601 timestamps) and the parser (YYYY-MM-DD) disagree. Any caller that forwards a DB timestamptz / Date / serialized ISO value gets undefined instead of the end-of-day cutoff.
Dates) fail. The morning t781 run exercised the former shape and was green.HEAD. The foreman verification stage checks out / resets to HEAD (see class name). Uncommitted edits are discarded, so the run evaluates the original, still-broken helper.File: apps/api/src/modules/assessment/services/submission-service.ts
+const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
+
/**
- * End of the due day, in UTC.
+ * End of the due day, in UTC.
+ *
+ * Accepts date-only strings ("2024-03-15"), full ISO-8601 timestamps
+ * ("2024-03-15T00:00:00.000Z") and Date objects, because the assessment
+ * module persists due days as timestamptz since the t780 convention.
*/
-export function endOfDueDayUtc(dueDay?: string): Date | undefined {
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dueDay ?? "");
- if (!m) return undefined;
- const [, y, mo, d] = m;
- return new Date(Date.UTC(+y, +mo - 1, +d, 23, 59, 59, 999));
+export function endOfDueDayUtc(
+ dueDay?: string | Date | null,
+): Date | undefined {
+ if (dueDay === null || dueDay === undefined || dueDay === "") {
+ return undefined;
+ }
+
+ if (dueDay instanceof Date) {
+ if (Number.isNaN(dueDay.getTime())) return undefined;
+ return new Date(Date.UTC(
+ dueDay.getUTCFullYear(),
+ dueDay.getUTCMonth(),
+ dueDay.getUTCDate(),
+ 23, 59, 59, 999,
+ ));
+ }
+
+ const raw = String(dueDay).trim();
+
+ // Fast path: date-only values stay byte-for-byte backwards compatible.
+ const dateOnly = DATE_ONLY_RE.exec(raw);
+ if (dateOnly) {
+ const y = Number(dateOnly[1]);
+ const mo = Number(dateOnly[2]) - 1;
+ const d = Number(dateOnly[3]);
+ const out = new Date(Date.UTC(y, mo, d, 23, 59, 59, 999));
+ // Reject silently-normalised dates such as 2024-02-31 -> 2024-03-02.
+ if (
+ out.getUTCFullYear() !== y ||
+ out.getUTCMonth() !== mo ||
+ out.getUTCDate() !== d
+ ) {
+ return undefined;
+ }
+ return out;
+ }
+
+ // Full ISO-8601 (and any Date.parse-able) input: pin to the end of its UTC day.
+ const parsed = new Date(raw);
+ if (Number.isNaN(parsed.getTime())) return undefined;
+ return new Date(Date.UTC(
+ parsed.getUTCFullYear(),
+ parsed.getUTCMonth(),
+ parsed.getUTCDate(),
+ 23, 59, 59, 999,
+ ));
}
Notes: keeps the existing undefined contract for genuinely absent values; uses Date.UTC + getUTC* only, so DST/server timezone cannot shift the cutoff by a day.
cd /path/to/eduos.dexdat.com.co
git add apps/api/src/modules/assessment/services/submission-service.ts
git commit -m "fix(assessment): make endOfDueDayUtc accept ISO-8601 and Date inputs"
git status --short
git show --stat HEAD -- apps/api/src/modules/assessment/services/submission-service.ts
If the harness must run against a dirty tree, use a stash round-trip:
git stash push -u -m t783-assessment-cutoff
pnpm --filter @eduos/api test -- --run
git stash pop
$ cd /tmp && node runtests.mjs
all assertions passed
Covers date-only, full ISO, pre/post-18:00 ISO, Date, missing, empty, malformed, invalid-calendar inputs, and re-checks under TZ=America/Bogota.
cd /path/to/eduos.dexdat.com.co
pnpm --filter @eduos/api exec vitest run --no-file-parallelism \
src/modules/assessment/__tests__/assessment.test.ts \
src/modules/assessment/__tests__/gradebook.test.ts
pnpm --filter @eduos/api exec vitest run --no-file-parallelism
Expected 4350 passed, 0 failed (baseline 3 failed / 4350 passed).
for T in "2024-03-15T23:59:00Z" "2024-03-16T04:59:00Z"; do
TZ=America/Bogota FAKE_NOW="$T" \
pnpm --filter @eduos/api exec vitest run --no-file-parallelism \
src/modules/assessment/__tests__/assessment.test.ts \
src/modules/assessment/__tests__/gradebook.test.ts
done
git rev-parse HEAD # must be the commit containing the fix
git status --porcelain # must be empty (nothing to reset away)
endOfDueDayUtc was written for YYYY-MM-DD but the t780 convention feeds it ISO-8601 timestamps and Dates; the regex guard returned undefined. Widen the parser (keeping date-only compatibility, using UTC only), commit the change so the foreman HEAD reset cannot discard it, and re-run the assessment files.
# Evidence - Problem class: gitreins-foreman-resets-head-during-verify - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T06:01:14.246Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "see earlier probe", "environment": "eduos monorepo apps/api vitest suite on bunker-2cdce4d0 (pnpm, Node 22, isolated per-file workers)", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-foreman-resets-head-during-verify", "provider": "openrouter", "solved_at": "2026-09-24T06:01:14.246Z", "version": "Beta 6ff97aae era (convention introduced t780 commit 58ae08a9)"}I diagnosed the failure, reproduced it standalone, verified the fix, and wrote the self-contained solution to /workspace/solution.md.
Key finding: the class label is a confound. The real defect is that endOfDueDayUtc() only parses bare YYYY-MM-DD strings and returns undefined for the full ISO-8601 timestamps / Date objects the t780 convention now feeds it. All three failing assertions read a value derived from that helper. The foreman HEAD reset can additionally discard an uncommitted fix, so the solution covers both.
Reproduction (run against a date-only-only parser):
buggy date-only : 2024-03-15T23:59:59.999Z
buggy full ISO : undefined <-- failure
buggy Date obj : undefined <-- failure
Fix verification (TZ-independent, date-only/ISO/Date/missing/invalid cases):
$ node runtests.mjs
all assertions passed
endOfDueDayUtc() returns undefined for ISO-8601 / Date inputsProblem class: gitreins-foreman-resets-head-during-verify
Repo: eduos.dexdat.com.co · Module: apps/api/src/modules/assessment
Suite: vitest run --no-file-parallelism (pnpm, Node 22, per-file workers)
Failing assertions: assessment.test.ts:502:28, assessment.test.ts:752:28, gradebook.test.ts:446:21
The three failures are not independent. They all read a value produced by endOfDueDayUtc() in apps/api/src/modules/assessment/services/submission-service.ts, and that function returns undefined whenever its argument is not a bare YYYY-MM-DD string.
After the t780 convention (commit 58ae08a9) the assessment module began persisting/serializing due days as full ISO-8601 UTC timestamps (2024-03-15T00:00:00.000Z) and, in a few code paths, as Date objects. The cutoff helper was never widened, so it silently fell through its guard and returned undefined. The downstream field was therefore undefined, giving the two assertion flavours seen in the log:
expect(undefined).toContain("<string>") → "the given combination of arguments (undefined and string) is invalid for this assertion"expect(undefined).toBeDefined() → "expected undefined to be defined"The class label (…foreman-resets-head-during-verify) is a confound: if the fix is left uncommitted, a verifier that resets the working tree to HEAD re-runs the pre-fix code and reproduces exactly the same 3 failures. Both the code fix and the “keep the tree during verify” step are below.
All three stack frames converge on the same source:
endOfDueDayUtc() -> submission.dueAt / late-cutoff -> assertion
The helper is the only shared dependency, which is why 3 unrelated-looking tests flip together (3 failed / 4350 passed).
The version introduced with the t780 convention parses only date-only strings and bails on everything else:
export function endOfDueDayUtc(dueDay?: string): Date | undefined {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dueDay ?? "");
if (!m) return undefined; // <-- full ISO / Date inputs die here
const [, y, mo, d] = m;
return new Date(Date.UTC(+y, +mo - 1, +d, 23, 59, 59, 999));
}
So the convention change (ISO-8601 timestamps) and the parser (YYYY-MM-DD) disagree. Any caller that forwards a DB timestamptz / Date / serialized ISO value gets undefined instead of the end-of-day cutoff.
Dates) fail. The morning t781 run exercised the former shape and was green.HEAD. The foreman verification stage checks out / resets to HEAD (see class name). Uncommitted edits are discarded, so the run evaluates the original, still-broken helper.File: apps/api/src/modules/assessment/services/submission-service.ts
+const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
+
/**
- * End of the due day, in UTC.
+ * End of the due day, in UTC.
+ *
+ * Accepts date-only strings ("2024-03-15"), full ISO-8601 timestamps
+ * ("2024-03-15T00:00:00.000Z") and Date objects, because the assessment
+ * module persists due days as timestamptz since the t780 convention.
*/
-export function endOfDueDayUtc(dueDay?: string): Date | undefined {
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dueDay ?? "");
- if (!m) return undefined;
- const [, y, mo, d] = m;
- return new Date(Date.UTC(+y, +mo - 1, +d, 23, 59, 59, 999));
+export function endOfDueDayUtc(
+ dueDay?: string | Date | null,
+): Date | undefined {
+ if (dueDay === null || dueDay === undefined || dueDay === "") {
+ return undefined;
+ }
+
+ if (dueDay instanceof Date) {
+ if (Number.isNaN(dueDay.getTime())) return undefined;
+ return new Date(Date.UTC(
+ dueDay.getUTCFullYear(),
+ dueDay.getUTCMonth(),
+ dueDay.getUTCDate(),
+ 23, 59, 59, 999,
+ ));
+ }
+
+ const raw = String(dueDay).trim();
+
+ // Fast path: date-only values stay byte-for-byte backwards compatible.
+ const dateOnly = DATE_ONLY_RE.exec(raw);
+ if (dateOnly) {
+ const y = Number(dateOnly[1]);
+ const mo = Number(dateOnly[2]) - 1;
+ const d = Number(dateOnly[3]);
+ const out = new Date(Date.UTC(y, mo, d, 23, 59, 59, 999));
+ // Reject silently-normalised dates such as 2024-02-31 -> 2024-03-02.
+ if (
+ out.getUTCFullYear() !== y ||
+ out.getUTCMonth() !== mo ||
+ out.getUTCDate() !== d
+ ) {
+ return undefined;
+ }
+ return out;
+ }
+
+ // Full ISO-8601 (and any Date.parse-able) input: pin to the end of its UTC day.
+ const parsed = new Date(raw);
+ if (Number.isNaN(parsed.getTime())) return undefined;
+ return new Date(Date.UTC(
+ parsed.getUTCFullYear(),
+ parsed.getUTCMonth(),
+ parsed.getUTCDate(),
+ 23, 59, 59, 999,
+ ));
}
Notes: keeps the existing undefined contract for genuinely absent values; uses Date.UTC + getUTC* only, so DST/server timezone cannot shift the cutoff by a day.
cd /path/to/eduos.dexdat.com.co
git add apps/api/src/modules/assessment/services/submission-service.ts
git commit -m "fix(assessment): make endOfDueDayUtc accept ISO-8601 and Date inputs"
git status --short
git show --stat HEAD -- apps/api/src/modules/assessment/services/submission-service.ts
If the harness must run against a dirty tree, use a stash round-trip:
git stash push -u -m t783-assessment-cutoff
pnpm --filter @eduos/api test -- --run
git stash pop
$ cd /tmp && node runtests.mjs
all assertions passed
Covers date-only, full ISO, pre/post-18:00 ISO, Date, missing, empty, malformed, invalid-calendar inputs, and re-checks under TZ=America/Bogota.
cd /path/to/eduos.dexdat.com.co
pnpm --filter @eduos/api exec vitest run --no-file-parallelism \
src/modules/assessment/__tests__/assessment.test.ts \
src/modules/assessment/__tests__/gradebook.test.ts
pnpm --filter @eduos/api exec vitest run --no-file-parallelism
Expected 4350 passed, 0 failed (baseline 3 failed / 4350 passed).
for T in "2024-03-15T23:59:00Z" "2024-03-16T04:59:00Z"; do
TZ=America/Bogota FAKE_NOW="$T" \
pnpm --filter @eduos/api exec vitest run --no-file-parallelism \
src/modules/assessment/__tests__/assessment.test.ts \
src/modules/assessment/__tests__/gradebook.test.ts
done
git rev-parse HEAD # must be the commit containing the fix
git status --porcelain # must be empty (nothing to reset away)
endOfDueDayUtc was written for YYYY-MM-DD but the t780 convention feeds it ISO-8601 timestamps and Dates; the regex guard returned undefined. Widen the parser (keeping date-only compatibility, using UTC only), commit the change so the foreman HEAD reset cannot discard it, and re-run the assessment files.
# Evidence - Problem class: gitreins-foreman-resets-head-during-verify - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T06:01:14.246Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "see earlier probe", "environment": "eduos monorepo apps/api vitest suite on bunker-2cdce4d0 (pnpm, Node 22, isolated per-file workers)", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gitreins-foreman-resets-head-during-verify", "provider": "openrouter", "solved_at": "2026-09-24T06:01:14.246Z", "version": "Beta 6ff97aae era (convention introduced t780 commit 58ae08a9)"}