# test_thespacedevs.py import deepdiff import pytest from agenda.thespacedevs import format_launch_changes # --- Helper Functions for Tests --- def create_base_launch(): """Creates a base launch dictionary for diffing.""" return { "id": "test-id", "name": "Starship | Flight 10", "status": { "id": 8, "name": "To Be Confirmed", "abbrev": "TBC", "description": "Awaiting official confirmation...", }, "last_updated": "2025-08-08T16:03:39Z", "net": "2025-08-22T23:30:00Z", "window_end": "2025-08-23T01:34:00Z", "window_start": "2025-08-22T23:30:00Z", "net_precision": { "id": 1, "name": "Minute", "abbrev": "MIN", "description": "The T-0 is accurate to the minute.", }, "probability": 75, "pad": { "id": 188, "name": "Orbital Launch Mount A", "location": {"name": "SpaceX Starbase, TX, USA"}, }, "mission": { "name": "Flight 10" # description intentionally omitted initially for test_dictionary_item_added }, # Fields that should be skipped "agency_launch_attempt_count": 550, "location_launch_attempt_count": 18, "pad_launch_attempt_count": 9, "orbital_launch_attempt_count": 5, "agency_launch_attempt_count_year": 100, "location_launch_attempt_count_year": 3, "pad_launch_attempt_count_year": 3, "orbital_launch_attempt_count_year": 1, } # --- Tests for format_launch_changes --- def test_no_changes(): """Test when there are no differences.""" prev = create_base_launch() cur = create_base_launch() diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert result == "No specific changes detected" def test_status_change(): """Test changes to the status name.""" prev = create_base_launch() cur = create_base_launch() cur["status"]["name"] = "Go for Launch" # Note: ID change might happen, but the test focuses on the name change message diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) # print(f"DEBUG test_status_change diff: {diff}") # Uncomment for debugging result = format_launch_changes(diff) # DeepDiff should report this as values_changed assert "Status changed from 'To Be Confirmed' to 'Go for Launch'" in result def test_net_precision_change(): """Test changes to the net precision name.""" prev = create_base_launch() cur = create_base_launch() cur["net_precision"]["name"] = "Hour" # Note: ID change might happen, but the test focuses on the name change message diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) # print(f"DEBUG test_net_precision_change diff: {diff}") # Uncomment for debugging result = format_launch_changes(diff) # DeepDiff should report this as values_changed assert "Launch precision changed from 'Minute' to 'Hour'" in result def test_net_change(): """Test changes to the net (launch time).""" prev = create_base_launch() cur = create_base_launch() cur["net"] = "2025-08-23T00:00:00Z" diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) # The function uses format_date, so the output will be formatted assert ( "Launch time changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC" in result ) def test_window_start_change(): """Test changes to the window start.""" prev = create_base_launch() cur = create_base_launch() cur["window_start"] = "2025-08-22T23:00:00Z" diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert ( "Launch window start changed from 22 Aug 2025 at 23:30 UTC to 22 Aug 2025 at 23:00 UTC" in result ) def test_window_end_change(): """Test changes to the window end.""" prev = create_base_launch() cur = create_base_launch() cur["window_end"] = "2025-08-23T02:00:00Z" diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert ( "Launch window end changed from 23 Aug 2025 at 01:34 UTC to 23 Aug 2025 at 02:00 UTC" in result ) def test_last_updated_change(): """Test changes to the last updated time.""" prev = create_base_launch() cur = create_base_launch() cur["last_updated"] = "2025-08-09T10:00:00Z" diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert "Last updated: 09 Aug 2025 at 10:00 UTC" in result def test_name_change(): """Test changes to the launch name.""" prev = create_base_launch() cur = create_base_launch() cur["name"] = "Starship | Flight 10 - Revised" diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert ( "Mission name changed from 'Starship | Flight 10' to 'Starship | Flight 10 - Revised'" in result ) def test_probability_change(): """Test changes to the launch probability.""" prev = create_base_launch() cur = create_base_launch() cur["probability"] = 85 diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert "Launch probability changed from 75% to 85%" in result def test_probability_set(): """Test setting probability from None.""" prev = create_base_launch() prev["probability"] = None # Start with None cur = create_base_launch() # End with 75 (int) diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert "Launch probability set to 75%" in result def test_probability_removed(): """Test removing probability (setting to None).""" prev = create_base_launch() # Start with 75 (int) cur = create_base_launch() cur["probability"] = None # End with None diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert "Launch probability removed" in result def test_skip_fields(): """Test that specific fields are skipped.""" prev = create_base_launch() cur = create_base_launch() cur["agency_launch_attempt_count"] = 551 cur["pad_launch_attempt_count_year"] = 4 diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) # The only change is in skipped fields, so no user-facing changes assert result == "No specific changes detected" def test_dictionary_item_added(): """Test adding a new field.""" prev = create_base_launch() # Ensure 'description' is not in the base mission dict assert "description" not in prev["mission"] cur = create_base_launch() cur["mission"]["description"] = "New mission description." diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) # print(f"DEBUG test_dictionary_item_added diff: {diff}") # Uncomment for debugging result = format_launch_changes(diff) # DeepDiff path for nested dict item added assert ( "New field added: Mission['Description" in result ) # Matches the output format def test_dictionary_item_removed(): """Test removing a field.""" prev = create_base_launch() # Add 'description' to prev so it can be removed prev["mission"]["description"] = "Old description." cur = create_base_launch() # Ensure 'description' is not in cur's mission dict assert "description" not in cur["mission"] diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) # print(f"DEBUG test_dictionary_item_removed diff: {diff}") # Uncomment for debugging result = format_launch_changes(diff) # DeepDiff path for nested dict item removed assert "Field removed: Mission['Description" in result # Matches the output format def test_type_change(): """Test changing the type of a field.""" prev = create_base_launch() cur = create_base_launch() cur["probability"] = "High" # Change int to string diff = deepdiff.DeepDiff(prev, cur, ignore_order=True) result = format_launch_changes(diff) assert "Probability type changed from int to str" in result # --- Test with Sample Data (Simulated Diff) --- def test_with_sample_data_status_change(): """Simulate a diff like the one that might occur for status change using sample data structure.""" # Simulate a diff where status changes from TBC to Success (ID 3) # This mimics the EXACT structure DeepDiff produces for values_changed # We only include the fields we care about in the test sample_diff_status = deepdiff.DeepDiff( {"status": {"id": 8, "name": "To Be Confirmed"}}, {"status": {"id": 3, "name": "Success"}}, ignore_order=True, ) # print(f"DEBUG test_with_sample_data_status_change diff: {sample_diff_status}") # Debug result = format_launch_changes(sample_diff_status) # Should report the status name change assert "Status changed from 'To Be Confirmed' to 'Success'" in result # Ensure it doesn't report ID change separately due to the 'startswith' logic # (The logic should group them under the name change) # A simple check: if name change is there, and ID is handled, it's likely correct. # The exact number of bullet points might vary based on ID/abbrev handling, # but the key message should be present. def test_with_sample_data_net_change(): """Simulate a diff for net change.""" sample_diff_net = { "values_changed": { "root['net']": { "new_value": "2025-08-25T12:00:00Z", "old_value": "2025-08-22T23:30:00Z", } } } result = format_launch_changes(sample_diff_net) assert ( "Launch time changed from 22 Aug 2025 at 23:30 UTC to 25 Aug 2025 at 12:00 UTC" in result ) def test_status_name_change(): diffs = { "values_changed": { "root['status']['name']": { "old_value": "To Be Confirmed", "new_value": "Go for Launch", } } } out = format_launch_changes(diffs) assert "Status changed from 'To Be Confirmed' to 'Go for Launch'" in out def test_net_precision_change(): diffs = { "values_changed": { "root['net_precision']['name']": { "old_value": "Hour", "new_value": "Minute", } } } out = format_launch_changes(diffs) assert "Launch precision changed from 'Hour' to 'Minute'" in out @pytest.mark.parametrize( "field,label", [ ("net", "Launch time"), ("window_start", "Launch window start"), ("window_end", "Launch window end"), ], ) def test_datetime_changes_formatted(field: str, label: str): diffs = { "values_changed": { f"root['{field}']": { "old_value": "2025-08-22T23:30:00Z", "new_value": "2025-08-23T01:34:00Z", } } } out = format_launch_changes(diffs) assert ( f"{label} changed from 22 Aug 2025 at 23:30 UTC " f"to 23 Aug 2025 at 01:34 UTC" ) in out def test_last_updated_formatted(): diffs = { "values_changed": { "root['last_updated']": { "old_value": "2025-08-08T16:03:39Z", "new_value": "2025-08-09T12:00:00Z", } } } out = format_launch_changes(diffs) # Only the new value is shown for last_updated assert "Last updated: 09 Aug 2025 at 12:00 UTC" in out assert "16:03" not in out # ensure old value not included def test_name_and_probability_changes(): diffs = { "values_changed": { "root['name']": { "old_value": "Starship | Flight 10", "new_value": "Starship | Flight X", }, "root['probability']": {"old_value": 70, "new_value": 85}, } } out = format_launch_changes(diffs) assert ( "Mission name changed from 'Starship | Flight 10' to 'Starship | Flight X'" in out ) assert "Launch probability changed from 70% to 85%" in out def test_probability_set_and_removed(): # Set from None diffs_set = { "values_changed": {"root['probability']": {"old_value": None, "new_value": 40}} } out_set = format_launch_changes(diffs_set) assert "Launch probability set to 40%" in out_set # Removed to None diffs_removed = { "values_changed": {"root['probability']": {"old_value": 30, "new_value": None}} } out_removed = format_launch_changes(diffs_removed) assert "Launch probability removed" in out_removed def test_skipped_fields_are_not_reported(): diffs = { "values_changed": { "root['agency_launch_attempt_count']": { "old_value": 556, "new_value": 557, }, "root['pad_launch_attempt_count_year']": { "old_value": 3, "new_value": 4, }, } } out = format_launch_changes(diffs) # No bullet points should be produced for skipped fields assert out == "No specific changes detected" def test_generic_value_change_fallback(): diffs = { "values_changed": { "root['rocket']['configuration']['variant']": { "old_value": "", "new_value": "Block 2", } } } out = format_launch_changes(diffs) assert ( "rocket']['configuration']['variant'] changed from '' to 'Block 2'" not in out ) # ensure path cleaning happened assert "rocket']['configuration']['variant" not in out # sanity: no stray brackets assert "rocket']['configuration']['variant" not in out # redundant but explicit # Expected cleaned path from the function logic: assert ( "rocket']['configuration']['variant" not in out ) # path is cleaned to "rocket']['configuration']['variant" # The function replaces prefixes; the fallback prints the cleaned field string: # "rocket']['configuration']['variant changed from '' to 'Block 2'" # Because there is no special handling for deeper nesting, we assert a substring: assert "variant changed from '' to 'Block 2'" in out def test_dictionary_item_added_removed_and_type_change(): diffs = { "dictionary_item_added": {"root['new_field']"}, "dictionary_item_removed": {"root['old_field_name']"}, "type_changes": { "root['probability']": { "old_type": int, "new_type": str, "old_value": 70, "new_value": "70%", } }, } out = format_launch_changes(diffs) assert "New field added: New Field" in out assert "Field removed: Old Field Name" in out assert "Probability type changed from int to str" in out def test_no_changes_message(): diffs = {} out = format_launch_changes(diffs) assert out == "No specific changes detected" def test_no_changes(): """ Tests that no message is generated when there are no differences. """ old_launch = {"id": 1, "name": "Launch A"} new_launch = {"id": 1, "name": "Launch A"} diff = deepdiff.DeepDiff(old_launch, new_launch) assert format_launch_changes(diff) == "No specific changes detected" def test_mission_name_change(): """ Tests a simple change to the mission name. """ old_launch = {"name": "Starship | Flight 9"} new_launch = {"name": "Starship | Flight 10"} diff = deepdiff.DeepDiff(old_launch, new_launch) expected = "• Mission name changed from 'Starship | Flight 9' to 'Starship | Flight 10'" assert format_launch_changes(diff) == expected def test_status_change(): """ Tests that a change in status name is formatted correctly and that other changes within the 'status' dictionary are ignored for a cleaner output. """ old_launch = {"status": {"id": 8, "name": "To Be Confirmed", "abbrev": "TBC"}} new_launch = {"status": {"id": 1, "name": "Go for Launch", "abbrev": "Go"}} diff = deepdiff.DeepDiff(old_launch, new_launch) expected = "• Status changed from 'To Be Confirmed' to 'Go for Launch'" assert format_launch_changes(diff) == expected def test_datetime_changes(): """ Tests the custom formatting for various datetime fields. """ old_launch = { "net": "2025-08-22T23:30:00Z", "window_start": "2025-08-22T23:30:00Z", "window_end": "2025-08-23T01:34:00Z", "last_updated": "2025-08-08T16:03:39Z", } new_launch = { "net": "2025-08-23T00:00:00Z", "window_start": "2025-08-23T00:00:00Z", "window_end": "2025-08-23T02:00:00Z", "last_updated": "2025-08-09T10:00:00Z", } diff = deepdiff.DeepDiff(old_launch, new_launch) result_lines = format_launch_changes(diff).split("\n") expected_changes = [ "• Launch time changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC", "• Launch window start changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC", "• Launch window end changed from 23 Aug 2025 at 01:34 UTC to 23 Aug 2025 at 02:00 UTC", "• Last updated: 09 Aug 2025 at 10:00 UTC", ] assert len(result_lines) == len(expected_changes) for expected_line in expected_changes: assert expected_line in result_lines def test_datetime_change_fallback(): """ Tests the fallback for malformed datetime strings. """ old_launch = {"net": "an-invalid-date"} new_launch = {"net": "another-invalid-date"} diff = deepdiff.DeepDiff(old_launch, new_launch) expected = "• Launch time changed from an-invalid-date to another-invalid-date" assert format_launch_changes(diff) == expected def test_probability_changes(): """ Tests the three scenarios for probability changes: 1. From a value to another value. 2. From None to a value. 3. From a value to None. """ # From value to value old = {"probability": 70} new = {"probability": 80} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• Launch probability changed from 70% to 80%" # From None to value old = {"probability": None} new = {"probability": 90} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• Launch probability set to 90%" # From value to None old = {"probability": 90} new = {"probability": None} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• Launch probability removed" def test_skipped_field_is_ignored(): """ Tests that changes to fields in the SKIP_FIELDS list are ignored. """ old = {"pad_launch_attempt_count_year": 4} new = {"pad_launch_attempt_count_year": 5} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "No specific changes detected" def test_dictionary_item_added(): """ Tests that newly added fields are reported. """ old = {"name": "Launch"} new = {"name": "Launch", "hashtag": "#launchday"} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• New field added: Hashtag" def test_dictionary_item_removed(): """ Tests that removed fields are reported. """ old = {"name": "Launch", "hashtag": "#launchday"} new = {"name": "Launch"} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• Field removed: Hashtag" def test_type_change(): """ Tests that a change in the data type of a field is reported. """ old = {"probability": 50} new = {"probability": "50%"} diff = deepdiff.DeepDiff(old, new) assert format_launch_changes(diff) == "• Probability type changed from int to str" def test_multiple_changes(): """ Tests a scenario with multiple, mixed changes to ensure all are reported. Order is not guaranteed, so we check for the presence of each line. """ old_launch = { "status": {"name": "To Be Confirmed"}, "probability": 90, "name": "Old Mission Name", } new_launch = { "status": {"name": "Go for Launch"}, "name": "New Mission Name", "weather_concerns": "High winds.", } diff = deepdiff.DeepDiff(old_launch, new_launch) result_lines = format_launch_changes(diff).split("\n") expected_changes = [ "• Status changed from 'To Be Confirmed' to 'Go for Launch'", "• Mission name changed from 'Old Mission Name' to 'New Mission Name'", "• Field removed: Probability", "• New field added: Weather Concerns", ] assert len(result_lines) == len(expected_changes) for expected_line in expected_changes: assert expected_line in result_lines