""" Tests for timestamp-based slug generation (ADR-062) Tests the new generate_timestamp_slug() function introduced in v1.5.0 to replace content-based slug generation with timestamp-based slugs. """ import pytest from datetime import datetime from starpunk.slug_utils import generate_timestamp_slug class TestTimestampSlugGeneration: """Test timestamp-based slug generation per ADR-062""" def test_basic_timestamp_slug_format(self): """Test that timestamp slug matches YYYYMMDDHHMMSS format""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20251216143052" def test_no_collision_returns_base_slug(self): """Test that when no collision exists, base slug is returned""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) existing_slugs = {"some-other-slug", "20251216140000"} slug = generate_timestamp_slug(fixed_time, existing_slugs) assert slug == "20251216143052" def test_first_collision_gets_suffix_1(self): """Test that first collision gets -1 suffix per ADR-062""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) existing_slugs = {"20251216143052"} slug = generate_timestamp_slug(fixed_time, existing_slugs) assert slug == "20251216143052-1" def test_second_collision_gets_suffix_2(self): """Test that second collision gets -2 suffix""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) existing_slugs = {"20251216143052", "20251216143052-1"} slug = generate_timestamp_slug(fixed_time, existing_slugs) assert slug == "20251216143052-2" def test_sequential_suffixes_up_to_10(self): """Test sequential suffix generation up to -10""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) base_slug = "20251216143052" # Create existing slugs from base to -9 existing_slugs = {base_slug} existing_slugs.update(f"{base_slug}-{i}" for i in range(1, 10)) slug = generate_timestamp_slug(fixed_time, existing_slugs) assert slug == "20251216143052-10" def test_uses_utcnow_when_no_timestamp_provided(self): """Test that function defaults to current UTC time""" slug = generate_timestamp_slug(None, set()) # Should be 14 characters (YYYYMMDDHHMMSS) assert len(slug) == 14 assert slug.isdigit() def test_empty_existing_slugs_set(self): """Test with explicitly empty set""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20251216143052" def test_none_existing_slugs_defaults_to_empty(self): """Test that None existing_slugs is handled as empty set""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) slug = generate_timestamp_slug(fixed_time, None) assert slug == "20251216143052" def test_different_timestamps_produce_different_slugs(self): """Test that different timestamps produce different slugs""" time1 = datetime(2025, 12, 16, 14, 30, 52) time2 = datetime(2025, 12, 16, 14, 30, 53) slug1 = generate_timestamp_slug(time1, set()) slug2 = generate_timestamp_slug(time2, set()) assert slug1 != slug2 assert slug1 == "20251216143052" assert slug2 == "20251216143053" def test_midnight_timestamp(self): """Test timestamp at midnight""" fixed_time = datetime(2025, 1, 1, 0, 0, 0) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20250101000000" def test_end_of_day_timestamp(self): """Test timestamp at end of day""" fixed_time = datetime(2025, 12, 31, 23, 59, 59) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20251231235959" def test_leap_year_timestamp(self): """Test timestamp on leap day""" fixed_time = datetime(2024, 2, 29, 12, 30, 45) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20240229123045" def test_single_digit_month_and_day(self): """Test that single-digit months and days are zero-padded""" fixed_time = datetime(2025, 1, 5, 9, 5, 3) slug = generate_timestamp_slug(fixed_time, set()) assert slug == "20250105090503" assert len(slug) == 14 # Ensure proper padding def test_collision_with_gap_in_sequence(self): """Test collision handling when there's a gap in sequence""" fixed_time = datetime(2025, 12, 16, 14, 30, 52) # Base exists, -1 exists, but -2 doesn't exist existing_slugs = {"20251216143052", "20251216143052-1", "20251216143052-3"} slug = generate_timestamp_slug(fixed_time, existing_slugs) # Should fill the gap and return -2 assert slug == "20251216143052-2" class TestTimestampSlugIntegration: """Test timestamp slug integration with note creation""" def test_create_note_generates_timestamp_slug(self, app): """Test that creating a note without custom slug generates timestamp""" from starpunk.notes import create_note import re with app.app_context(): note = create_note("Test content for timestamp slug") # Should match timestamp pattern per ADR-062 timestamp_pattern = re.compile(r'^\d{14}(-\d+)?$') assert timestamp_pattern.match(note.slug), \ f"Slug '{note.slug}' does not match timestamp format" def test_create_multiple_notes_same_second(self, app): """Test creating multiple notes in same second gets sequential suffixes""" from starpunk.notes import create_note from datetime import datetime with app.app_context(): fixed_time = datetime(2025, 12, 16, 14, 30, 52) # Create 3 notes at same timestamp note1 = create_note("First note", created_at=fixed_time) note2 = create_note("Second note", created_at=fixed_time) note3 = create_note("Third note", created_at=fixed_time) # Verify sequential slug assignment assert note1.slug == "20251216143052" assert note2.slug == "20251216143052-1" assert note3.slug == "20251216143052-2" def test_custom_slug_still_works(self, app): """Test that custom slugs via mp-slug still work unchanged""" from starpunk.notes import create_note with app.app_context(): note = create_note("Test content", custom_slug="my-custom-slug") assert note.slug == "my-custom-slug" def test_timestamp_slug_does_not_collide_with_reserved(self, app): """Test that timestamp slugs cannot collide with reserved slugs""" from starpunk.slug_utils import RESERVED_SLUGS from datetime import datetime with app.app_context(): # Timestamp slugs are all numeric, reserved slugs are alphabetic # So collision is impossible by construction fixed_time = datetime(2025, 12, 16, 14, 30, 52) slug = generate_timestamp_slug(fixed_time, set()) # Verify slug is all numeric assert slug.replace('-', '').isdigit() # Verify it doesn't match any reserved slug assert slug not in RESERVED_SLUGS