From b1eb99d961d7137ed24668a4cbe4598b0815e85c Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 03:29:04 -0800 Subject: [PATCH] fix: use local-time Date constructor in session-manager to prevent timezone day shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new Date('YYYY-MM-DD') creates UTC midnight, which in negative UTC offset timezones (e.g., Hawaii) causes getDate() to return the previous day. Replaced with new Date(year, month - 1, day) for correct local-time behavior. Added 15 tests: session-manager datetime verification and edge cases (7), package-manager getCommandPattern special characters (4), and validators model/skill-reference validation (4). Tests: 651 → 666. --- scripts/lib/session-manager.js | 6 ++- tests/ci/validators.test.js | 62 +++++++++++++++++++++++++++++++ tests/lib/package-manager.test.js | 31 ++++++++++++++++ tests/lib/session-manager.test.js | 58 +++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/scripts/lib/session-manager.js b/scripts/lib/session-manager.js index 9e6e50b5..c77c4e6e 100644 --- a/scripts/lib/session-manager.js +++ b/scripts/lib/session-manager.js @@ -47,8 +47,10 @@ function parseSessionFilename(filename) { filename, shortId, date: dateStr, - // Convert date string to Date object - datetime: new Date(dateStr) + // Use local-time constructor (consistent with validation on line 40) + // new Date(dateStr) interprets YYYY-MM-DD as UTC midnight which shows + // as the previous day in negative UTC offset timezones + datetime: new Date(year, month - 1, day) }; } diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index 9db06b08..e2d24d9e 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -1359,6 +1359,68 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + // ── Round 30: validate-commands skill warnings and workflow edge cases ── + console.log('\nRound 30: validate-commands (skill warnings):'); + + if (test('warns (not errors) when skill directory reference is not found', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + // Create a command that references a skill via path (skills/name/) format + // but the skill doesn't exist — should warn, not error + fs.writeFileSync(path.join(testDir, 'cmd-a.md'), + '# Command A\nSee skills/nonexistent-skill/ for details.'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + // Skill directory references produce warnings, not errors — exit 0 + assert.strictEqual(result.code, 0, 'Skill path references should warn, not error'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + if (test('passes when command has no slash references at all', () => { + const testDir = createTestDir(); + const agentsDir = createTestDir(); + const skillsDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'cmd-simple.md'), + '# Simple Command\nThis command has no references to other commands.'); + + const result = runValidatorWithDirs('validate-commands', { + COMMANDS_DIR: testDir, AGENTS_DIR: agentsDir, SKILLS_DIR: skillsDir + }); + assert.strictEqual(result.code, 0, 'Should pass with no references'); + cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); + })) passed++; else failed++; + + console.log('\nRound 30: validate-agents (model validation):'); + + if (test('rejects agent with unrecognized model value', () => { + const testDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'bad-model.md'), + '---\nmodel: gpt-4\ntools: Read, Write\n---\n# Bad Model Agent'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 1, 'Should reject unrecognized model'); + assert.ok(result.stderr.includes('gpt-4'), 'Should mention the invalid model'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('accepts all valid model values (haiku, sonnet, opus)', () => { + const testDir = createTestDir(); + fs.writeFileSync(path.join(testDir, 'haiku.md'), + '---\nmodel: haiku\ntools: Read\n---\n# Haiku Agent'); + fs.writeFileSync(path.join(testDir, 'sonnet.md'), + '---\nmodel: sonnet\ntools: Read, Write\n---\n# Sonnet Agent'); + fs.writeFileSync(path.join(testDir, 'opus.md'), + '---\nmodel: opus\ntools: Read, Write, Bash\n---\n# Opus Agent'); + + const result = runValidatorWithDir('validate-agents', 'AGENTS_DIR', testDir); + assert.strictEqual(result.code, 0, 'All valid models should pass'); + assert.ok(result.stdout.includes('3'), 'Should validate 3 agent files'); + cleanupTestDir(testDir); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 6d57a6b0..8691d97c 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -993,6 +993,37 @@ function runTests() { } })) passed++; else failed++; + // ── Round 30: getCommandPattern with special action patterns ── + console.log('\nRound 30: getCommandPattern edge cases:'); + + if (test('escapes pipe character in action name', () => { + const pattern = pm.getCommandPattern('lint|fix'); + const regex = new RegExp(pattern); + assert.ok(regex.test('npm run lint|fix'), 'Should match literal pipe'); + assert.ok(!regex.test('npm run lint'), 'Pipe should be literal, not regex OR'); + })) passed++; else failed++; + + if (test('escapes dollar sign in action name', () => { + const pattern = pm.getCommandPattern('deploy$prod'); + const regex = new RegExp(pattern); + assert.ok(regex.test('npm run deploy$prod'), 'Should match literal dollar sign'); + })) passed++; else failed++; + + if (test('handles action with leading/trailing spaces gracefully', () => { + // Spaces aren't special in regex but good to test the full pattern + const pattern = pm.getCommandPattern(' dev '); + const regex = new RegExp(pattern); + assert.ok(regex.test('npm run dev '), 'Should match action with spaces'); + })) passed++; else failed++; + + if (test('known action "dev" does NOT use escapeRegex path', () => { + // "dev" is a known action with hardcoded patterns, not the generic path + const pattern = pm.getCommandPattern('dev'); + // Should match pnpm dev (without "run") + const regex = new RegExp(pattern); + assert.ok(regex.test('pnpm dev'), 'Known action pnpm dev should match'); + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/lib/session-manager.test.js b/tests/lib/session-manager.test.js index 57f15b59..ac2d663f 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -951,6 +951,64 @@ src/main.ts // best-effort } + // ── Round 30: datetime local-time fix and parseSessionFilename edge cases ── + console.log('\nRound 30: datetime local-time fix:'); + + if (test('datetime day matches the filename date (local-time constructor)', () => { + const result = sessionManager.parseSessionFilename('2026-06-15-abcdef12-session.tmp'); + assert.ok(result); + // With the fix, getDate()/getMonth() should return local-time values + // matching the filename, regardless of timezone + assert.strictEqual(result.datetime.getDate(), 15, 'Day should be 15 (local time)'); + assert.strictEqual(result.datetime.getMonth(), 5, 'Month should be 5 (June, 0-indexed)'); + assert.strictEqual(result.datetime.getFullYear(), 2026, 'Year should be 2026'); + })) passed++; else failed++; + + if (test('datetime matches for January 1 (timezone-sensitive date)', () => { + // Jan 1 at UTC midnight is Dec 31 in negative offsets — this tests the fix + const result = sessionManager.parseSessionFilename('2026-01-01-abc12345-session.tmp'); + assert.ok(result); + assert.strictEqual(result.datetime.getDate(), 1, 'Day should be 1 in local time'); + assert.strictEqual(result.datetime.getMonth(), 0, 'Month should be 0 (January)'); + })) passed++; else failed++; + + if (test('datetime matches for December 31 (year boundary)', () => { + const result = sessionManager.parseSessionFilename('2025-12-31-abc12345-session.tmp'); + assert.ok(result); + assert.strictEqual(result.datetime.getDate(), 31); + assert.strictEqual(result.datetime.getMonth(), 11); // December + assert.strictEqual(result.datetime.getFullYear(), 2025); + })) passed++; else failed++; + + console.log('\nRound 30: parseSessionFilename edge cases:'); + + if (test('parses session ID with many dashes (UUID-like)', () => { + const result = sessionManager.parseSessionFilename('2026-02-13-a1b2c3d4-session.tmp'); + assert.ok(result); + assert.strictEqual(result.shortId, 'a1b2c3d4'); + assert.strictEqual(result.date, '2026-02-13'); + })) passed++; else failed++; + + if (test('rejects filename with missing session.tmp suffix', () => { + const result = sessionManager.parseSessionFilename('2026-02-13-abc12345.tmp'); + assert.strictEqual(result, null, 'Should reject filename without -session.tmp'); + })) passed++; else failed++; + + if (test('rejects filename with extra text after suffix', () => { + const result = sessionManager.parseSessionFilename('2026-02-13-abc12345-session.tmp.bak'); + assert.strictEqual(result, null, 'Should reject filenames with extra extension'); + })) passed++; else failed++; + + if (test('handles old-format filename without session ID', () => { + // The regex match[2] is undefined for old format → shortId defaults to 'no-id' + const result = sessionManager.parseSessionFilename('2026-02-13-session.tmp'); + if (result) { + assert.strictEqual(result.shortId, 'no-id', 'Should default to no-id'); + } + // Either null (regex doesn't match) or has no-id — both are acceptable + assert.ok(true, 'Old format handled without crash'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);