fix: use local-time Date constructor in session-manager to prevent timezone day shift

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.
This commit is contained in:
Affaan Mustafa
2026-02-13 03:29:04 -08:00
parent 992688a674
commit b1eb99d961
4 changed files with 155 additions and 2 deletions

View File

@@ -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)
};
}

View File

@@ -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);

View File

@@ -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}`);

View File

@@ -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);