- Reference docs: known issues, user journey, location redesign, mission merge plan - Scripts: fill_location_state, merge_missions, migrate_location, migrate_state - Tests: e2e test suite, asset entries unit tests - Demo: location board, hifi prototypes, test screenshots - Data backup: asset log entries and state nodes - Update .gitignore Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
286 lines
11 KiB
Python
286 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import datetime
|
|
|
|
from app import create_app
|
|
from app.config import Config
|
|
from app.extensions import db
|
|
from app.models import Asset, AssetLogEntry, DockingEvent
|
|
|
|
|
|
class TestConfig(Config):
|
|
TESTING = True
|
|
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
|
|
WTF_CSRF_ENABLED = False
|
|
|
|
|
|
class AssetEntryTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.app = create_app(TestConfig)
|
|
self.client = self.app.test_client()
|
|
with self.app.app_context():
|
|
db.create_all()
|
|
asset = Asset(
|
|
name="Codex Test Asset",
|
|
asset_type="Station",
|
|
program="Codex",
|
|
home_region="LEO",
|
|
)
|
|
db.session.add(asset)
|
|
db.session.commit()
|
|
self.asset_id = asset.id
|
|
|
|
def tearDown(self) -> None:
|
|
with self.app.app_context():
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
def _latest_entry(self) -> AssetLogEntry:
|
|
with self.app.app_context():
|
|
return (
|
|
db.session.query(AssetLogEntry)
|
|
.filter_by(asset_id=self.asset_id)
|
|
.order_by(AssetLogEntry.created_at.desc())
|
|
.first()
|
|
)
|
|
|
|
def test_form_state_nodes_save_all_location_and_state_fields(self) -> None:
|
|
response = self.client.post(
|
|
f"/assets/{self.asset_id}/entries/new",
|
|
data={
|
|
"entry_kind": "state",
|
|
"title": "Mars transfer",
|
|
"start_at": "2060-03-12T09:00",
|
|
"end_at": "2060-03-13T09:00",
|
|
"state_node_id": [""],
|
|
"state_node_title": ["Arrival burn"],
|
|
"state_node_at": ["2060-03-12T10:00"],
|
|
"state_node_detail": ["Inserted into Mars orbit"],
|
|
"state_node_state_label": ["transit-complete"],
|
|
"state_node_previous_location": ["LEO"],
|
|
"state_node_transit_location": ["Transfer"],
|
|
"state_node_target_location": ["Mars Orbit"],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
entry = self._latest_entry()
|
|
self.assertEqual(len(entry.state_nodes), 1)
|
|
node = entry.state_nodes[0]
|
|
self.assertEqual(node.state_label, "transit-complete")
|
|
self.assertEqual(node.previous_location, "LEO")
|
|
self.assertEqual(node.transit_location, "Transfer")
|
|
self.assertEqual(node.target_location, "Mars Orbit")
|
|
|
|
def test_form_edit_state_nodes_updates_all_location_and_state_fields(self) -> None:
|
|
self.client.post(
|
|
f"/assets/{self.asset_id}/entries/new",
|
|
data={
|
|
"entry_kind": "state",
|
|
"title": "Initial interval",
|
|
"start_at": "2060-03-12T09:00",
|
|
"end_at": "2060-03-13T09:00",
|
|
"state_node_id": [""],
|
|
"state_node_title": ["Initial node"],
|
|
"state_node_at": ["2060-03-12T10:00"],
|
|
"state_node_detail": ["Before edit"],
|
|
"state_node_state_label": ["Docked"],
|
|
"state_node_previous_location": ["LEO"],
|
|
"state_node_transit_location": ["Transfer"],
|
|
"state_node_target_location": ["Mars Orbit"],
|
|
},
|
|
)
|
|
entry = self._latest_entry()
|
|
node_id = entry.state_nodes[0].id
|
|
|
|
response = self.client.post(
|
|
f"/assets/{self.asset_id}/entries/{entry.id}/edit",
|
|
data={
|
|
"entry_kind": "state",
|
|
"title": "Edited interval",
|
|
"start_at": "2060-03-12T09:00",
|
|
"end_at": "2060-03-13T09:00",
|
|
"state_node_id": [str(node_id)],
|
|
"state_node_title": ["Edited node"],
|
|
"state_node_at": ["2060-03-12T11:00"],
|
|
"state_node_detail": ["After edit"],
|
|
"state_node_state_label": ["Exploration"],
|
|
"state_node_previous_location": ["Mars Orbit"],
|
|
"state_node_transit_location": ["Transfer"],
|
|
"state_node_target_location": ["Mars Surface"],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
updated = self._latest_entry()
|
|
node = updated.state_nodes[0]
|
|
self.assertEqual(node.title, "Edited node")
|
|
self.assertEqual(node.state_label, "Exploration")
|
|
self.assertEqual(node.previous_location, "Mars Orbit")
|
|
self.assertEqual(node.transit_location, "Transfer")
|
|
self.assertEqual(node.target_location, "Mars Surface")
|
|
|
|
def test_json_state_nodes_save_all_location_and_state_fields(self) -> None:
|
|
response = self.client.post(
|
|
f"/assets/{self.asset_id}/entries/new",
|
|
json={
|
|
"entry_kind": "state",
|
|
"title": "Quick state",
|
|
"start_at": "2060-03-12T09:00",
|
|
"end_at": "2060-03-13T09:00",
|
|
"state_label": "Exploration",
|
|
"location": "Mars Surface",
|
|
"state_nodes": [
|
|
{
|
|
"title": "Landed",
|
|
"at_time": "2060-03-12T10:00",
|
|
"detail": "Surface operations",
|
|
"state_label": "Exploration",
|
|
"previous_location": "Mars Orbit",
|
|
"transit_location": "Transfer",
|
|
"target_location": "Mars Surface",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
entry = self._latest_entry()
|
|
self.assertEqual(len(entry.state_nodes), 1)
|
|
node = entry.state_nodes[0]
|
|
self.assertEqual(node.state_label, "Exploration")
|
|
self.assertEqual(node.previous_location, "Mars Orbit")
|
|
self.assertEqual(node.transit_location, "Transfer")
|
|
self.assertEqual(node.target_location, "Mars Surface")
|
|
|
|
def test_json_event_without_start_at_uses_sim_time(self) -> None:
|
|
response = self.client.post(
|
|
f"/assets/{self.asset_id}/entries/new",
|
|
json={
|
|
"entry_kind": "event",
|
|
"title": "Quick event",
|
|
"start_at": None,
|
|
"sim_time": "2060-03-12T09:00",
|
|
"location": "LEO",
|
|
"summary": "Created from the quick modal",
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
entry = self._latest_entry()
|
|
self.assertEqual(entry.entry_kind, "event")
|
|
self.assertEqual(entry.start_at, datetime(2060, 3, 12, 9, 0))
|
|
self.assertEqual(entry.state_nodes[0].target_location, "LEO")
|
|
|
|
def test_form_event_location_is_saved_for_display(self) -> None:
|
|
response = self.client.post(
|
|
f"/assets/{self.asset_id}/entries/new",
|
|
data={
|
|
"entry_kind": "event",
|
|
"title": "Quick sighting",
|
|
"start_at": "2060-03-12T09:00",
|
|
"location": "Mars Orbit",
|
|
"summary": "Point event with a location",
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
entry = self._latest_entry()
|
|
self.assertEqual(entry.entry_kind, "event")
|
|
self.assertEqual(len(entry.state_nodes), 1)
|
|
self.assertEqual(entry.state_nodes[0].target_location, "Mars Orbit")
|
|
|
|
def test_dock_existing_vehicle_creates_active_parent_child_relation_and_logs(self) -> None:
|
|
with self.app.app_context():
|
|
child = Asset(name="Codex Test Shuttle", asset_type="Vehicle", home_region="LEO")
|
|
db.session.add(child)
|
|
db.session.commit()
|
|
child_id = child.id
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/assets/{self.asset_id}/dock",
|
|
json={
|
|
"mode": "dock_vehicle",
|
|
"vehicle_id": str(child_id),
|
|
"vehicle_name": "Codex Test Shuttle",
|
|
"sim_time": "2060-03-12T09:00",
|
|
"note": "Docked in test",
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
with self.app.app_context():
|
|
event = db.session.query(DockingEvent).one()
|
|
self.assertEqual(event.parent_asset_id, self.asset_id)
|
|
self.assertEqual(event.child_asset_id, child_id)
|
|
self.assertIsNone(event.undocked_at)
|
|
|
|
parent = db.session.get(Asset, self.asset_id)
|
|
child = db.session.get(Asset, child_id)
|
|
parent_snapshot = self.app.view_functions["web.asset_detail"].__globals__["_build_asset_snapshot"](
|
|
parent, datetime(2060, 3, 12, 9, 0)
|
|
)
|
|
child_snapshot = self.app.view_functions["web.asset_detail"].__globals__["_build_asset_snapshot"](
|
|
child, datetime(2060, 3, 12, 9, 0)
|
|
)
|
|
self.assertEqual(parent_snapshot["docked_vehicles"][0]["id"], child_id)
|
|
self.assertEqual(child_snapshot["docking_target"]["id"], self.asset_id)
|
|
self.assertEqual(len(parent.log_entries), 1)
|
|
self.assertEqual(len(child.log_entries), 1)
|
|
|
|
def test_dock_to_target_creates_reverse_relation(self) -> None:
|
|
with self.app.app_context():
|
|
target = Asset(name="Codex Test Station", asset_type="Station", home_region="Mars Orbit")
|
|
db.session.add(target)
|
|
db.session.commit()
|
|
target_id = target.id
|
|
|
|
response = self.client.post(
|
|
f"/api/v1/assets/{self.asset_id}/dock",
|
|
json={
|
|
"mode": "dock_to_target",
|
|
"target_asset_id": str(target_id),
|
|
"sim_time": "2060-03-12T09:00",
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
with self.app.app_context():
|
|
event = db.session.query(DockingEvent).one()
|
|
self.assertEqual(event.parent_asset_id, target_id)
|
|
self.assertEqual(event.child_asset_id, self.asset_id)
|
|
|
|
current = db.session.get(Asset, self.asset_id)
|
|
target = db.session.get(Asset, target_id)
|
|
current_snapshot = self.app.view_functions["web.asset_detail"].__globals__["_build_asset_snapshot"](
|
|
current, datetime(2060, 3, 12, 9, 0)
|
|
)
|
|
target_snapshot = self.app.view_functions["web.asset_detail"].__globals__["_build_asset_snapshot"](
|
|
target, datetime(2060, 3, 12, 9, 0)
|
|
)
|
|
self.assertEqual(current_snapshot["docking_target"]["id"], target_id)
|
|
self.assertEqual(target_snapshot["docked_vehicles"][0]["id"], self.asset_id)
|
|
|
|
def test_dock_custom_vehicle_creates_label_relation(self) -> None:
|
|
response = self.client.post(
|
|
f"/api/v1/assets/{self.asset_id}/dock",
|
|
json={
|
|
"mode": "dock_vehicle",
|
|
"vehicle_id": None,
|
|
"vehicle_name": "External Cargo Pod",
|
|
"sim_time": "2060-03-12T09:00",
|
|
},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
with self.app.app_context():
|
|
event = db.session.query(DockingEvent).one()
|
|
self.assertEqual(event.parent_asset_id, self.asset_id)
|
|
self.assertIsNone(event.child_asset_id)
|
|
self.assertEqual(event.child_label, "External Cargo Pod")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|