sourcebot/packages/web/src/features/search/dateUtils.test.ts
Wayne Sun ba4c8a58e7 feat: add temporal filtering to search and repository APIs
Add temporal filtering capabilities for searches by git branch/revision
and repository index dates (since/until). Integrates with the refactored
QueryIR-based search architecture.

- Add gitRevision, since, until parameters to SearchOptions
- Implement temporal repo filtering by indexedAt field
- Add branch filtering via QueryIR wrapper
- Add search_commits MCP tool for commit-based searches
- Update list_repos with activeAfter/activeBefore filtering
- Add 88 new tests (all passing)

Signed-off-by: Wayne Sun <gsun@redhat.com>
2025-12-07 15:50:48 -05:00

379 lines
14 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
parseTemporalDate,
validateDateRange,
toDbDate,
toGitDate,
} from './dateUtils';
describe('dateUtils', () => {
// Mock the current time for consistent testing
const MOCK_NOW = new Date('2024-06-15T12:00:00.000Z');
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(MOCK_NOW);
});
afterEach(() => {
vi.useRealTimers();
});
describe('parseTemporalDate', () => {
describe('ISO 8601 dates', () => {
it('should parse ISO date (YYYY-MM-DD)', () => {
const result = parseTemporalDate('2024-01-01');
expect(result).toBe('2024-01-01T00:00:00.000Z');
});
it('should parse ISO datetime with timezone', () => {
const result = parseTemporalDate('2024-01-01T12:30:00Z');
expect(result).toBe('2024-01-01T12:30:00.000Z');
});
it('should parse ISO datetime without timezone', () => {
const result = parseTemporalDate('2024-01-01T12:30:00');
expect(result).toBeDefined();
expect(result).toContain('2024-01-01');
});
it('should return undefined for undefined input', () => {
const result = parseTemporalDate(undefined);
expect(result).toBeUndefined();
});
it('should return undefined for empty string', () => {
const result = parseTemporalDate('');
expect(result).toBeUndefined();
});
});
describe('relative dates - yesterday', () => {
it('should parse "yesterday"', () => {
const result = parseTemporalDate('yesterday');
expect(result).toBe('2024-06-14T12:00:00.000Z');
});
it('should parse "YESTERDAY" (case insensitive)', () => {
const result = parseTemporalDate('YESTERDAY');
expect(result).toBe('2024-06-14T12:00:00.000Z');
});
});
describe('relative dates - N units ago', () => {
it('should parse "1 day ago"', () => {
const result = parseTemporalDate('1 day ago');
expect(result).toBe('2024-06-14T12:00:00.000Z');
});
it('should parse "30 days ago"', () => {
const result = parseTemporalDate('30 days ago');
expect(result).toBe('2024-05-16T12:00:00.000Z');
});
it('should parse "1 week ago"', () => {
const result = parseTemporalDate('1 week ago');
expect(result).toBe('2024-06-08T12:00:00.000Z');
});
it('should parse "2 weeks ago"', () => {
const result = parseTemporalDate('2 weeks ago');
expect(result).toBe('2024-06-01T12:00:00.000Z');
});
it('should parse "1 month ago"', () => {
const result = parseTemporalDate('1 month ago');
expect(result).toBe('2024-05-15T12:00:00.000Z');
});
it('should parse "3 months ago"', () => {
const result = parseTemporalDate('3 months ago');
expect(result).toBe('2024-03-15T12:00:00.000Z');
});
it('should parse "1 year ago"', () => {
const result = parseTemporalDate('1 year ago');
expect(result).toBe('2023-06-15T12:00:00.000Z');
});
it('should parse "2 hours ago"', () => {
const result = parseTemporalDate('2 hours ago');
expect(result).toBe('2024-06-15T10:00:00.000Z');
});
it('should parse "30 minutes ago"', () => {
const result = parseTemporalDate('30 minutes ago');
expect(result).toBe('2024-06-15T11:30:00.000Z');
});
it('should parse "45 seconds ago"', () => {
const result = parseTemporalDate('45 seconds ago');
expect(result).toBe('2024-06-15T11:59:15.000Z');
});
it('should handle singular "day" without "s"', () => {
const result = parseTemporalDate('1 day ago');
expect(result).toBe('2024-06-14T12:00:00.000Z');
});
it('should be case insensitive', () => {
const result = parseTemporalDate('30 DAYS AGO');
expect(result).toBe('2024-05-16T12:00:00.000Z');
});
});
describe('relative dates - last unit', () => {
it('should parse "last week"', () => {
const result = parseTemporalDate('last week');
expect(result).toBe('2024-06-08T12:00:00.000Z');
});
it('should parse "last month"', () => {
const result = parseTemporalDate('last month');
expect(result).toBe('2024-05-15T12:00:00.000Z');
});
it('should parse "last year"', () => {
const result = parseTemporalDate('last year');
expect(result).toBe('2023-06-15T12:00:00.000Z');
});
it('should be case insensitive', () => {
const result = parseTemporalDate('LAST WEEK');
expect(result).toBe('2024-06-08T12:00:00.000Z');
});
});
describe('invalid or unknown formats', () => {
it('should return original string for unrecognized format', () => {
const result = parseTemporalDate('some random string');
expect(result).toBe('some random string');
});
it('should return original string for git-specific formats', () => {
// Git understands these but our parser doesn't convert them
const result = parseTemporalDate('2 weeks 3 days ago');
expect(result).toBe('2 weeks 3 days ago');
});
});
});
describe('validateDateRange', () => {
it('should return null for valid date range', () => {
const error = validateDateRange('2024-01-01', '2024-12-31');
expect(error).toBeNull();
});
it('should return null when only since is provided', () => {
const error = validateDateRange('2024-01-01', undefined);
expect(error).toBeNull();
});
it('should return null when only until is provided', () => {
const error = validateDateRange(undefined, '2024-12-31');
expect(error).toBeNull();
});
it('should return null when both are undefined', () => {
const error = validateDateRange(undefined, undefined);
expect(error).toBeNull();
});
it('should return error when since > until', () => {
const error = validateDateRange('2024-12-31', '2024-01-01');
expect(error).toContain('since');
expect(error).toContain('until');
expect(error).toContain('before');
});
it('should validate relative dates', () => {
const error = validateDateRange('30 days ago', '1 day ago');
expect(error).toBeNull();
});
it('should return error for invalid relative date range', () => {
const error = validateDateRange('1 day ago', '30 days ago');
expect(error).toContain('since');
expect(error).toContain('until');
});
it('should handle mixed ISO and relative dates', () => {
const error = validateDateRange('2024-01-01', '30 days ago');
expect(error).toBeNull(); // 2024-01-01 is before 30 days ago
});
it('should return null for same date', () => {
const error = validateDateRange('2024-06-15', '2024-06-15');
expect(error).toBeNull();
});
});
describe('toDbDate', () => {
it('should convert ISO date to Date object', () => {
const result = toDbDate('2024-01-01');
expect(result).toBeInstanceOf(Date);
expect(result?.toISOString()).toBe('2024-01-01T00:00:00.000Z');
});
it('should convert relative date to Date object', () => {
const result = toDbDate('30 days ago');
expect(result).toBeInstanceOf(Date);
expect(result?.toISOString()).toBe('2024-05-16T12:00:00.000Z');
});
it('should return undefined for undefined input', () => {
const result = toDbDate(undefined);
expect(result).toBeUndefined();
});
it('should return undefined for empty string', () => {
const result = toDbDate('');
expect(result).toBeUndefined();
});
it('should handle "yesterday"', () => {
const result = toDbDate('yesterday');
expect(result).toBeInstanceOf(Date);
expect(result?.toISOString()).toBe('2024-06-14T12:00:00.000Z');
});
it('should handle "last week"', () => {
const result = toDbDate('last week');
expect(result).toBeInstanceOf(Date);
expect(result?.toISOString()).toBe('2024-06-08T12:00:00.000Z');
});
});
describe('toGitDate', () => {
it('should preserve ISO date format', () => {
const result = toGitDate('2024-01-01');
expect(result).toBe('2024-01-01');
});
it('should preserve ISO datetime format', () => {
const result = toGitDate('2024-01-01T12:30:00Z');
expect(result).toBe('2024-01-01T12:30:00Z');
});
it('should preserve "N days ago" format', () => {
const result = toGitDate('30 days ago');
expect(result).toBe('30 days ago');
});
it('should preserve "yesterday" format', () => {
const result = toGitDate('yesterday');
expect(result).toBe('yesterday');
});
it('should preserve "last week" format', () => {
const result = toGitDate('last week');
expect(result).toBe('last week');
});
it('should preserve "last month" format', () => {
const result = toGitDate('last month');
expect(result).toBe('last month');
});
it('should preserve "last year" format', () => {
const result = toGitDate('last year');
expect(result).toBe('last year');
});
it('should return undefined for undefined input', () => {
const result = toGitDate(undefined);
expect(result).toBeUndefined();
});
it('should pass through unrecognized format unchanged', () => {
// For formats git doesn't natively understand, pass through to git
const result = toGitDate('some random string');
expect(result).toBe('some random string');
});
it('should preserve relative time formats', () => {
const result = toGitDate('2 weeks ago');
expect(result).toBe('2 weeks ago');
});
});
describe('edge cases', () => {
it('should handle dates at month boundaries', () => {
vi.setSystemTime(new Date('2024-03-31T12:00:00.000Z'));
const result = parseTemporalDate('1 month ago');
// JavaScript Date handles month rollover
expect(result).toBeDefined();
});
it('should handle dates at year boundaries', () => {
vi.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
const result = parseTemporalDate('1 month ago');
expect(result).toBe('2023-12-15T12:00:00.000Z');
});
it('should handle leap year February', () => {
vi.setSystemTime(new Date('2024-03-01T12:00:00.000Z'));
const result = parseTemporalDate('1 month ago');
expect(result).toBe('2024-02-01T12:00:00.000Z');
});
it('should handle midnight times', () => {
vi.setSystemTime(new Date('2024-06-15T00:00:00.000Z'));
const result = parseTemporalDate('1 day ago');
expect(result).toBe('2024-06-14T00:00:00.000Z');
});
it('should handle end of day times', () => {
vi.setSystemTime(new Date('2024-06-15T23:59:59.999Z'));
const result = parseTemporalDate('1 day ago');
expect(result).toBe('2024-06-14T23:59:59.999Z');
});
});
describe('integration scenarios', () => {
it('should correctly validate a typical user query range', () => {
const since = '30 days ago';
const until = 'yesterday';
const parsedSince = parseTemporalDate(since);
const parsedUntil = parseTemporalDate(until);
const validationError = validateDateRange(since, until);
expect(parsedSince).toBe('2024-05-16T12:00:00.000Z');
expect(parsedUntil).toBe('2024-06-14T12:00:00.000Z');
expect(validationError).toBeNull();
});
it('should correctly convert for database queries', () => {
const since = '7 days ago';
const until = 'yesterday';
const dbSince = toDbDate(since);
const dbUntil = toDbDate(until);
expect(dbSince).toBeInstanceOf(Date);
expect(dbUntil).toBeInstanceOf(Date);
expect(dbSince!.getTime()).toBeLessThan(dbUntil!.getTime());
});
it('should correctly preserve for git commands', () => {
const since = '30 days ago';
const until = 'yesterday';
const gitSince = toGitDate(since);
const gitUntil = toGitDate(until);
// Git natively understands these, so they're preserved
expect(gitSince).toBe('30 days ago');
expect(gitUntil).toBe('yesterday');
});
it('should handle mixed ISO and relative dates in range validation', () => {
const since = '2024-01-01';
const until = '7 days ago';
const validationError = validateDateRange(since, until);
expect(validationError).toBeNull();
});
});
});