Agent skills
← Back to the TIMEBOMB documentation
TIMEBOMB demo evidence
Agent Skill Synthetic demonstration
Captured output from TIMEBOMB runs on a synthetic fixture: the request, the audit report, failing tests, the fix, the passing rerun, and follow-up runs in other agents.
Synthetic fixture. This is a small demonstration module with two intentionally seeded defects. Both defects were specified in the request before the audit, so the audit did not discover them independently. It is not a customer application. The main run used Claude Code; later sections cover Codex CLI and Claude Code skill-folder runs (Node.js v22.23.2, Linux, one machine). None of this establishes compatibility with other agents or versions, or coverage of any production system. File names like evidence/… refer to the private evidence folder and are not published; the logs shown here are rendered from those files.
Demo request
The request given to the agent. It states the policy and names both seeded defects up front.
Use TIMEBOMB on my small Node.js trial service. Requirements: a trial lasts one calendar month in UTC, keeps its start time of day, clamps to the last day of the next month, and grants access only strictly before expiration. I've seeded two defects: unclamped month overflow, and access allowed at the exact expiration instant. Test Jan 30 and 31 into February in leap and non-leap years, plus 1 ms before, at, and after expiration, with a normal control. Don't change the system clock. Show failing tests, fix them, and rerun.Audit report
Synthetic fixture. Both defects were seeded deliberately and named in the request before the audit. These results show the workflow working. They are not discoveries about a real application.
Scope
- Target:
timebomb-subscription-demo/src/subscription.js(trialExpiresAt,createSubscriptionService) - Runtime: Node.js v22.23.2, npm 10.9.8, built-in
node:test - Mode: audit, then fixes (authorized by the request)
- Revision: no VCS. Seeded file sha256
4138aea8…6068c, fixed file sha256267f87f9…6142f
Execution summary
Executed
10 scenarios
10 selected, 10 executed. One test per scenario.
Before fix
6 CONFIRMED
6 scenarios, traced to 2 seeded defects.
Before fix
4 PASS
Controls and boundaries that already held: TB-C1, TB-A0, TB-A1, TB-A3.
After fix
10/10 PASS
10 of 10 scenarios pass against the fixed source.
Other classes
0 SUPPORTED / POLICY GAP / BLOCKED
POLICY GAP: 0 (the request states every policy).
Each scenario is one test, so the scenario count equals the test count here. Passing these scenarios does not prove that all temporal behavior is correct.
Temporal inventory
| Feature | Owner | Clock | Stored state | Semantics | Boundary | Test control | Source |
|---|---|---|---|---|---|---|---|
| Trial expiration | trialExpiresAt | injected now() at startTrial | startedAtMs, expiresAtMs (ms) | UTC calendar rule | month end | explicit start timestamp | src/subscription.js |
| Access decision | hasAccess | injected now() | expiresAtMs | instant | expiration instant | injected clock, 1 ms steps | src/subscription.js |
Only one clock exists: a now() function passed in by the test. There is no browser, database, scheduler, or provider clock. Precision is milliseconds, so "immediately before/after" means ±1 ms.
Scenario ledger
| ID | Feature | Boundary | Clock control | Before fix | After fix |
|---|---|---|---|---|---|
| TB-E1 | Expiration | Jan 30 → Feb, leap year (2028) | explicit start | CONFIRMED | PASS |
| TB-E2 | Expiration | Jan 31 → Feb, leap year (2028) | explicit start | CONFIRMED | PASS |
| TB-E3 | Expiration | Jan 30 → Feb, non-leap year (2027) | explicit start | CONFIRMED | PASS |
| TB-E4 | Expiration | Jan 31 → Feb, non-leap year (2027) | explicit start | CONFIRMED | PASS |
| TB-C1 | Expiration | control: Mar 15 → Apr 15 | explicit start | PASS | PASS |
| TB-A0 | Access | control: mid-trial | injected clock | PASS | PASS |
| TB-A1 | Access | expiration − 1 ms | injected clock | PASS | PASS |
| TB-A2 | Access | exactly at expiration | injected clock | CONFIRMED | PASS |
| TB-A3 | Access | expiration + 1 ms | injected clock | PASS | PASS |
| TB-I1 | Expiration → access | Jan 31 start, check Mar 1 | injected clock | CONFIRMED | PASS |
Evidence: evidence/01-before-fix.txt, evidence/02-after-fix.txt, evidence/03-reproduce-script.txt. Command: npm test (node --test --test-reporter=spec).
Findings
TB-F1: Expiration overflows into March for Jan 30/31 starts (seeded defect 1)
- Classification: CONFIRMED before fix, PASS after fix
- Severity (in this fixture): high. A trial starting Jan 30 or 31 runs 1 to 3 days longer than the requirement allows, and TB-I1 shows the extra time is granted as real access.
- Expected source: the stated requirement (one UTC calendar month, clamped)
- Cause:
setUTCMonth(getUTCMonth() + 1)with no clamping
| Scenario | Start | Expected | Actual before fix |
|---|---|---|---|
| TB-E1 | 2028-01-30T14:45:10.250Z | 2028-02-29T14:45:10.250Z | 2028-03-01T14:45:10.250Z |
| TB-E2 | 2028-01-31T14:45:10.250Z | 2028-02-29T14:45:10.250Z | 2028-03-02T14:45:10.250Z |
| TB-E3 | 2027-01-30T23:59:59.999Z | 2027-02-28T23:59:59.999Z | 2027-03-02T23:59:59.999Z |
| TB-E4 | 2027-01-31T00:00:00.000Z | 2027-02-28T00:00:00.000Z | 2027-03-03T00:00:00.000Z |
| TB-I1 | 2027-01-31, access checked 2027-03-01 | false | true |
Fix: build the next month with Date.UTC, clamp the day to that month's last day, and keep the original hours, minutes, seconds, and milliseconds.
TB-F2: Access allowed at the exact expiration instant (seeded defect 2)
- Classification: CONFIRMED before fix, PASS after fix
- Severity (in this fixture): low. Access lasts 1 ms too long, but the authorization rule itself is wrong.
- Expected source: the stated requirement (invalid at or after expiration)
- Cause:
now() <= expiresAtMs - Observed: TB-A2 at 2027-04-15T08:30:00.000Z returned
true, expectedfalse - Fix:
now() < expiresAtMs
Changes
src/subscription.js: two fixes, shown inevidence/fix.difftest/subscription.test.js: 10 scenarios with literal expected values, run against the real service
Before and after
Both runs use npm test (node --test --test-reporter=spec). The counts below are read from the logs by the build script.
| Run | Tests | Pass | Fail | Exit status |
|---|---|---|---|---|
| Before fix | 10 | 4 | 6 | 1 |
| After fix | 10 | 10 | 0 | 0 |
Logs are rendered from the captured evidence files by a build script. Stack frames (lines beginning with at) omitted. Node prints each error object’s opening { at the end of the last frame line, so that brace is shown on its own line. Paths shortened (private directory prefixes removed). Nothing else in the output was edited.
$ cd timebomb-subscription-demo && npm test # seeded (pre-fix) src/subscription.js
# node v22.23.2, npm 10.9.8, captured 2026-10-20T19:16:09Z
# sha256 src/subscription.js: 4138aea88e41d06d1d0901960998e9df5357e8ea839cd7e89eb7585b7046068c
> timebomb-subscription-demo@1.0.0 test
> node --test --test-reporter=spec
▶ trial expiration: one UTC calendar month, clamped to month end
✖ TB-E1 Jan 30 -> Feb, leap year 2028 (1.719777ms)
✖ TB-E2 Jan 31 -> Feb, leap year 2028 (0.233057ms)
✖ TB-E3 Jan 30 -> Feb, non-leap year 2027 (0.117488ms)
✖ TB-E4 Jan 31 -> Feb, non-leap year 2027 (0.13013ms)
✔ TB-C1 control: Mar 15 -> Apr 15, no clamping needed (0.089302ms)
✖ trial expiration: one UTC calendar month, clamped to month end (2.889086ms)
▶ access boundary: valid strictly before expiration
✔ TB-A0 control: mid-trial access (0.181539ms)
✔ TB-A1 1 ms before expiration (0.112017ms)
✖ TB-A2 exactly at expiration (0.197131ms)
✔ TB-A3 1 ms after expiration (0.148608ms)
✖ access boundary: valid strictly before expiration (0.857734ms)
▶ interaction: month-end expiration feeds authorization
✖ TB-I1 Jan 31 2027 start has no access on Mar 1 2027 (0.206896ms)
✖ interaction: month-end expiration feeds authorization (0.26981ms)
ℹ tests 10
ℹ suites 3
ℹ pass 4
ℹ fail 6
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 46.868303$ cd timebomb-subscription-demo && npm test # fixed src/subscription.js
# node v22.23.2, npm 10.9.8, captured 2026-10-20T19:16:25Z
# sha256 src/subscription.js: 267f87f9d7414840483be023871019a37e07b6e64d4c90851d6d2b5aedf6142f
> timebomb-subscription-demo@1.0.0 test
> node --test --test-reporter=spec
▶ trial expiration: one UTC calendar month, clamped to month end
✔ TB-E1 Jan 30 -> Feb, leap year 2028 (0.63885ms)
✔ TB-E2 Jan 31 -> Feb, leap year 2028 (0.093458ms)
✔ TB-E3 Jan 30 -> Feb, non-leap year 2027 (0.111542ms)
✔ TB-E4 Jan 31 -> Feb, non-leap year 2027 (0.063704ms)
✔ TB-C1 control: Mar 15 -> Apr 15, no clamping needed (0.107754ms)
✔ trial expiration: one UTC calendar month, clamped to month end (2.077433ms)
▶ access boundary: valid strictly before expiration
✔ TB-A0 control: mid-trial access (0.154095ms)
✔ TB-A1 1 ms before expiration (0.128425ms)
✔ TB-A2 exactly at expiration (0.150333ms)
✔ TB-A3 1 ms after expiration (0.138029ms)
✔ access boundary: valid strictly before expiration (0.783828ms)
▶ interaction: month-end expiration feeds authorization
✔ TB-I1 Jan 31 2027 start has no access on Mar 1 2027 (0.12568ms)
✔ interaction: month-end expiration feeds authorization (0.180489ms)
ℹ tests 10
ℹ suites 3
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 46.278108
# exit status: 0Before-fix log, continued: failure details for the 6 failing tests, then the exit status
✖ failing tests:
test at test/subscription.test.js:37:5
✖ TB-E1 Jan 30 -> Feb, leap year 2028 (1.719777ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ '2028-03-01T14:45:10.250Z'
- '2028-02-29T14:45:10.250Z'
^
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: '2028-03-01T14:45:10.250Z',
expected: '2028-02-29T14:45:10.250Z',
operator: 'strictEqual',
diff: 'simple'
}
test at test/subscription.test.js:37:5
✖ TB-E2 Jan 31 -> Feb, leap year 2028 (0.233057ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ '2028-03-02T14:45:10.250Z'
- '2028-02-29T14:45:10.250Z'
^
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: '2028-03-02T14:45:10.250Z',
expected: '2028-02-29T14:45:10.250Z',
operator: 'strictEqual',
diff: 'simple'
}
test at test/subscription.test.js:37:5
✖ TB-E3 Jan 30 -> Feb, non-leap year 2027 (0.117488ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ '2027-03-02T23:59:59.999Z'
- '2027-02-28T23:59:59.999Z'
^
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: '2027-03-02T23:59:59.999Z',
expected: '2027-02-28T23:59:59.999Z',
operator: 'strictEqual',
diff: 'simple'
}
test at test/subscription.test.js:37:5
✖ TB-E4 Jan 31 -> Feb, non-leap year 2027 (0.13013ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ '2027-03-03T00:00:00.000Z'
- '2027-02-28T00:00:00.000Z'
^
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: '2027-03-03T00:00:00.000Z',
expected: '2027-02-28T00:00:00.000Z',
operator: 'strictEqual',
diff: 'simple'
}
test at test/subscription.test.js:63:5
✖ TB-A2 exactly at expiration (0.197131ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
true !== false
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual',
diff: 'simple'
}
test at test/subscription.test.js:73:3
✖ TB-I1 Jan 31 2027 start has no access on Mar 1 2027 (0.206896ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
true !== false
{
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual',
diff: 'simple'
}
# exit status: 1The test file
The demo's own test file, which produced both logs. Every expected value is a literal written from the stated requirements.
// SYNTHETIC DEMONSTRATION. Tests target the actual service in
// src/subscription.js through an injected clock. The host clock is never
// changed. Every expected value below is a literal written from the stated
// requirements; none is computed with the code under test.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { createSubscriptionService } from '../src/subscription.js';
function harness(startIso) {
let currentMs = Date.parse(startIso);
const service = createSubscriptionService({ now: () => currentMs });
return {
service,
setNow(iso) {
currentMs = Date.parse(iso);
},
};
}
describe('trial expiration: one UTC calendar month, clamped to month end', () => {
const cases = [
// [scenario, start, independently specified expiration]
['TB-E1 Jan 30 -> Feb, leap year 2028',
'2028-01-30T14:45:10.250Z', '2028-02-29T14:45:10.250Z'],
['TB-E2 Jan 31 -> Feb, leap year 2028',
'2028-01-31T14:45:10.250Z', '2028-02-29T14:45:10.250Z'],
['TB-E3 Jan 30 -> Feb, non-leap year 2027',
'2027-01-30T23:59:59.999Z', '2027-02-28T23:59:59.999Z'],
['TB-E4 Jan 31 -> Feb, non-leap year 2027',
'2027-01-31T00:00:00.000Z', '2027-02-28T00:00:00.000Z'],
['TB-C1 control: Mar 15 -> Apr 15, no clamping needed',
'2027-03-15T08:30:00.000Z', '2027-04-15T08:30:00.000Z'],
];
for (const [name, start, expected] of cases) {
it(name, () => {
const { service } = harness(start);
const trial = service.startTrial('acct-1');
assert.equal(trial.startedAt, start);
assert.equal(trial.expiresAt, expected);
});
}
});
describe('access boundary: valid strictly before expiration', () => {
// Start chosen so expiration needs no clamping; this isolates defect 2.
const start = '2027-03-15T08:30:00.000Z';
// Independently specified expiration (see TB-C1).
const cases = [
['TB-A0 control: mid-trial access',
'2027-03-29T12:00:00.000Z', true],
['TB-A1 1 ms before expiration',
'2027-04-15T08:29:59.999Z', true],
['TB-A2 exactly at expiration',
'2027-04-15T08:30:00.000Z', false],
['TB-A3 1 ms after expiration',
'2027-04-15T08:30:00.001Z', false],
];
for (const [name, at, expected] of cases) {
it(name, () => {
const { service, setNow } = harness(start);
service.startTrial('acct-1');
setNow(at);
assert.equal(service.hasAccess('acct-1'), expected);
});
}
});
describe('interaction: month-end expiration feeds authorization', () => {
it('TB-I1 Jan 31 2027 start has no access on Mar 1 2027', () => {
const { service, setNow } = harness('2027-01-31T00:00:00.000Z');
service.startTrial('acct-1');
// Required expiration is 2027-02-28T00:00:00.000Z, so a day later
// access must already be denied.
setNow('2027-03-01T00:00:00.000Z');
assert.equal(service.hasAccess('acct-1'), false);
});
});The fix
The complete diff between the seeded and fixed src/subscription.js: one change per seeded defect.
--- src/subscription.js (seeded)
+++ src/subscription.js (fixed)
@@ -1,6 +1,8 @@
// SYNTHETIC DEMONSTRATION FIXTURE. Not a real customer application.
// This file was written with two intentionally seeded defects so the
-// TIMEBOMB workflow can be demonstrated end to end. See README.md.
+// TIMEBOMB workflow can be demonstrated end to end; both are now fixed.
+// The pre-fix version is preserved in evidence/subscription.seeded.js.
+// See README.md.
//
// Trial policy (explicit requirements):
// - A trial lasts one calendar month in UTC.
@@ -9,11 +11,21 @@
// - Access is valid strictly before expiration and invalid at or after it.
export function trialExpiresAt(startedAtMs) {
- // SEEDED DEFECT 1: overflowing month arithmetic without month-end clamping.
- // January 31 + 1 month becomes "February 31", which Date rolls into March.
- const expires = new Date(startedAtMs);
- expires.setUTCMonth(expires.getUTCMonth() + 1);
- return expires.getTime();
+ // FIX for seeded defect 1: build the destination month directly and clamp
+ // the day to its last day instead of letting Date overflow into March.
+ const start = new Date(startedAtMs);
+ const year = start.getUTCFullYear();
+ const month = start.getUTCMonth() + 1; // Date.UTC carries 12 into next year
+ const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+ return Date.UTC(
+ year,
+ month,
+ Math.min(start.getUTCDate(), lastDay),
+ start.getUTCHours(),
+ start.getUTCMinutes(),
+ start.getUTCSeconds(),
+ start.getUTCMilliseconds(),
+ );
}
export function createSubscriptionService({ now }) {
@@ -41,8 +53,8 @@
function hasAccess(accountId) {
const record = trials.get(accountId);
if (!record) return false;
- // SEEDED DEFECT 2: grants access at the exact expiration instant.
- return now() <= record.expiresAtMs;
+ // FIX for seeded defect 2: the expiration instant itself is not valid.
+ return now() < record.expiresAtMs;
}
return { startTrial, hasAccess };Bundled demo
The skill ships with a self-contained example. It was run as-is before the audit.
$ cd timebomb && node examples/temporal-demo.mjs
# node v22.23.2, captured 2026-10-20T19:15:06Z
PASS: access expires at the exact boundary
PASS: January 31 clamps to February 28
PASS: late and duplicate events preserve newer state
3/3 demo cases: broken fixtures rejected; corrected fixtures passed.
# exit status: 0Paths shortened.
Second agent: Codex CLI
Run after the Claude Code run, to test the normal skill-folder installation.
- Install: the unzipped package was copied to
~/.codex/skills/timebomb/, byte-identical to the release (diff -r). - Discovery: a fresh
codex execsession (Codex CLI 0.153.2) was asked to list its skills and listedtimebomb. Evidence:evidence/codex/01-discovery.jsonl. - Run: in a fresh copy of the seeded service (seeded source, unchanged tests,
package.jsononly),codex exec -s workspace-writegot the marketplace request, naming only "the timebomb skill". Codex read~/.codex/skills/timebomb/SKILL.md,references/scenarios.md, and ran the bundled demo without being given any path. Evidence:evidence/codex/02-run.jsonl,02-run-transcript.md. - Baseline: Codex's direct run (
node test/subscription.test.js) failed TB-E1, TB-E2, TB-E3, TB-E4, TB-A2, and TB-I1, and passed TB-C1, TB-A0, TB-A1, and TB-A3. This matches the Claude Code run. - Fix: Codex changed only
src/subscription.js(evidence/codex/03-codex-fix.diff). The test file's sha256 matched the original. - Independent rerun: I ran
npm teston Codex's fixed source outside Codex. It gave 10 tests, 10 pass, 0 fail, exit 0 (evidence/codex/04-independent-rerun.txt). - Verdict: - Install, discovery, and the subscription demo: tested and working. - Full workflow compliance: partial. Codex skipped TIMEBOMB's required report format, so its final answer had no evidence classes or scenario ledger.
- Other observed differences: - Inside Codex's sandbox,
npm testreported only one file-level result, so Codex ran the file directly to see each case. The cause was not established.
Retest with v1.0.1
v1.0.1 made the section-10 report mandatory in SKILL.md's completion criteria. The same prompt, which doesn't mention a report, was run in fresh sessions with the skill installed in each agent's skill folder. Evidence: evidence/retest-v1.0.1/ (private).
| Agent | Install and discovery | Demo run | Full report compliance |
|---|---|---|---|
Claude Code 2.1.304 (~/.claude/skills/timebomb/) | Listed; loaded via the Skill tool | Same 6 baseline failures; both defects fixed; added 3 tests (none changed); independent rerun 13/13; original 10 tests 10/10 | Partial: ledger lacks the Result and Evidence columns |
Codex CLI 0.153.2 (~/.codex/skills/timebomb/) | Opened the installed SKILL.md unprompted | Same 6 baseline failures; only src/ changed; independent rerun 10/10 | Partial: report present, but no NOT APPLICABLE count and no statement that the defects were seeded |
One run each, on one machine. The Claude Code session loaded the user's global plugins and hooks. Bundled demo from the v1.0.1 zip: 3/3, exit 0.
$ npm test # Codex-fixed src/subscription.js, rerun by Claude Code outside the Codex sandbox
# node v22.23.2, captured 2026-10-20T19:32:29Z
> timebomb-subscription-demo@1.0.0 test
> node --test --test-reporter=spec
▶ trial expiration: one UTC calendar month, clamped to month end
✔ TB-E1 Jan 30 -> Feb, leap year 2028 (1.194271ms)
✔ TB-E2 Jan 31 -> Feb, leap year 2028 (0.086981ms)
✔ TB-E3 Jan 30 -> Feb, non-leap year 2027 (0.109622ms)
✔ TB-E4 Jan 31 -> Feb, non-leap year 2027 (0.060144ms)
✔ TB-C1 control: Mar 15 -> Apr 15, no clamping needed (0.103119ms)
✔ trial expiration: one UTC calendar month, clamped to month end (2.089363ms)
▶ access boundary: valid strictly before expiration
✔ TB-A0 control: mid-trial access (0.156567ms)
✔ TB-A1 1 ms before expiration (0.132619ms)
✔ TB-A2 exactly at expiration (0.153095ms)
✔ TB-A3 1 ms after expiration (0.138528ms)
✔ access boundary: valid strictly before expiration (0.789148ms)
▶ interaction: month-end expiration feeds authorization
✔ TB-I1 Jan 31 2027 start has no access on Mar 1 2027 (0.153625ms)
✔ interaction: month-end expiration feeds authorization (0.214007ms)
ℹ tests 10
ℹ suites 3
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 46.002806
# exit status: 0Limitations
- The defects were planted and described in the request. This shows the workflow can reproduce and verify them. It does not show unaided defect discovery.
- The fixture is a single module with in-memory storage and one injected clock. It has no persistence, timezone database, browser, scheduler, webhooks, or billing provider.
- Not tested: other month ends (Mar 31 → Apr 30, Dec → Jan), leap day → non-leap year, renewals after the first month, or non-UTC calendar policies.
- The workflow ran in two agents on one Linux machine with Node v22.23.2: Claude Code, which was told to read
SKILL.md, and Codex CLI 0.153.2, from its installed skill folder. Claude Code's skill-folder install, other agents, other versions, and other runtimes were not tested. - None of this establishes coverage of any production system.
Process note
- My first test run failed because of a test-script mistake:
node --test test/does not accept a directory on Node 22. It was a harness error, not a finding. I fixed the script and reran it. The log is kept inevidence/00b-harness-error-before-fix.txt. - The bundled
examples/temporal-demo.mjsran with exit 0 and 3/3 cases (evidence/00-bundled-demo.txt).
Demo response
The agent's reply to the request, as written for the marketplace listing.
Synthetic fixture, seeded defects. I reproduced the two defects you described. The audit did not discover them independently.
Setup. Node v22.23.2 and node:test. The service takes an injected now() clock. Every expected value is a literal written from your requirements. The system clock was not changed.
Before the fix: npm test gave 10 tests, 4 pass, 6 fail, exit 1.
| Scenario | Expected | Actual |
|---|---|---|
| 2028-01-30 (leap) | 2028-02-29 | 2028-03-01 |
| 2028-01-31 (leap) | 2028-02-29 | 2028-03-02 |
| 2027-01-30 | 2027-02-28 | 2027-03-02 |
| 2027-01-31 | 2027-02-28 | 2027-03-03 |
| Access exactly at expiration | false | true |
| Jan 31 start, access on Mar 1 | false | true |
The controls passed before the fix: Mar 15 → Apr 15, mid-trial access, and access 1 ms before and 1 ms after expiration.
Fixes.
- Build the next month with
Date.UTCand clamp the day to that month's last day. - Change
<=to<in the access check.
After the fix: npm test gave 10 tests, 10 pass, 0 fail, exit 0.
Limitations. This is one in-memory module with one injected clock. Other month ends, renewals after the first month, timezones, persistence, schedulers, and providers were not tested. Passing these scenarios does not prove that all temporal behavior is correct.