@@ -98,6 +98,38 @@ def test_service_autostart_reuses_compatible_singleton(monkeypatch, tmp_path: Pa
9898 assert checked == [("127.0.0.1" , 48267 )]
9999
100100
101+ def test_service_autostart_rejects_incompatible_port_owner (monkeypatch , tmp_path : Path ) -> None :
102+ from tradingcodex_cli import service_autostart
103+
104+ monkeypatch .setattr (service_autostart , "_tcp_open" , lambda host , port : True )
105+ monkeypatch .setattr (service_autostart , "_service_health" , lambda host , port : {})
106+ monkeypatch .setattr (service_autostart , "_start_service" , lambda * args : (_ for _ in ()).throw (AssertionError ("started over occupied port" )))
107+
108+ with pytest .raises (RuntimeError , match = "non-TradingCodex service" ):
109+ service_autostart .ensure_service_up (tmp_path , timeout = 0.01 )
110+
111+
112+ def test_service_autostart_rejects_version_and_db_mismatch (monkeypatch ) -> None :
113+ from tradingcodex_cli import service_autostart
114+
115+ monkeypatch .setattr (
116+ service_autostart ,
117+ "_service_health" ,
118+ lambda host , port : {"service" : "tradingcodex" , "version" : "999.0.0" , "db_path" : str (Path ("/tmp/current.sqlite3" ))},
119+ )
120+ with pytest .raises (RuntimeError , match = "version mismatch" ):
121+ service_autostart ._assert_compatible_service ("127.0.0.1" , 48267 )
122+
123+ monkeypatch .setattr (service_autostart , "tradingcodex_db_path" , lambda : Path ("/tmp/current.sqlite3" ))
124+ monkeypatch .setattr (
125+ service_autostart ,
126+ "_service_health" ,
127+ lambda host , port : {"service" : "tradingcodex" , "version" : TRADINGCODEX_VERSION , "db_path" : str (Path ("/tmp/other.sqlite3" ))},
128+ )
129+ with pytest .raises (RuntimeError , match = "DB mismatch" ):
130+ service_autostart ._assert_compatible_service ("127.0.0.1" , 48267 )
131+
132+
101133def test_service_runserver_uses_fixed_port_and_skips_duplicate (monkeypatch , tmp_path : Path , capsys ) -> None :
102134 from django .core import management
103135 from tradingcodex_cli import __main__ as cli_main
@@ -122,6 +154,23 @@ def test_service_runserver_uses_fixed_port_and_skips_duplicate(monkeypatch, tmp_
122154 assert "TradingCodex service already running at http://127.0.0.1:48267/" in capsys .readouterr ().out
123155
124156
157+ def test_service_ensure_uses_autostart_helper (monkeypatch , tmp_path : Path , capsys ) -> None :
158+ from tradingcodex_cli import __main__ as cli_main
159+ from tradingcodex_cli import service_autostart
160+
161+ calls : list [tuple [Path , str ]] = []
162+ monkeypatch .chdir (tmp_path )
163+ monkeypatch .delenv ("TRADINGCODEX_WORKSPACE_ROOT" , raising = False )
164+ monkeypatch .setattr (service_autostart , "ensure_service_up" , lambda root , addr : calls .append ((root , addr )) or True )
165+
166+ cli_main .service (["ensure" ])
167+
168+ assert calls == [(tmp_path .resolve (), "127.0.0.1:48267" )]
169+ output = capsys .readouterr ().out
170+ assert "TradingCodex service started at http://127.0.0.1:48267/" in output
171+ assert "Health: http://127.0.0.1:48267/api/health" in output
172+
173+
125174def test_manage_runserver_uses_fixed_port_and_skips_duplicate (monkeypatch , capsys ) -> None :
126175 from tradingcodex_cli import service_autostart
127176
@@ -501,6 +550,65 @@ def test_user_prompt_hook_auto_routes_plain_investment_requests(tmp_path: Path)
501550 )
502551 assert subagent_brief_gate is None
503552
553+
554+ def test_session_start_update_recommendation_respects_home_preference (tmp_path : Path ) -> None :
555+ workspace = make_workspace (tmp_path )
556+ home = tmp_path / "tc-home"
557+ module_lock_path = workspace / ".tradingcodex" / "generated" / "module-lock.json"
558+ module_lock = json .loads (module_lock_path .read_text (encoding = "utf-8" ))
559+ module_lock ["tradingcodex_version" ] = "0.0.1"
560+ module_lock_path .write_text (json .dumps (module_lock , indent = 2 ) + "\n " , encoding = "utf-8" )
561+
562+ run (
563+ [sys .executable , str (workspace / ".codex" / "hooks" / "tradingcodex_hook.py" ), "session-start" ],
564+ workspace ,
565+ input_text = json .dumps ({}),
566+ env_extra = {"TRADINGCODEX_HOME" : str (home ), "TRADINGCODEX_LATEST_RELEASE_VERSION" : TRADINGCODEX_VERSION },
567+ )
568+ server_status = json .loads ((workspace / ".tradingcodex" / "mainagent" / "server-status.json" ).read_text (encoding = "utf-8" ))
569+ update_status = server_status ["update_status" ]
570+ assert update_status ["workspace_version" ] == "0.0.1"
571+ assert update_status ["installed_version" ] == TRADINGCODEX_VERSION
572+ assert update_status ["package_version" ] == TRADINGCODEX_VERSION
573+ assert update_status ["latest_release_version" ] == TRADINGCODEX_VERSION
574+ assert update_status ["latest_release_status" ] == "ok"
575+ assert update_status ["versions_match" ] is False
576+ assert update_status ["workspace_update_available" ] is True
577+ assert update_status ["workspace_update_allowed" ] is True
578+ assert update_status ["workspace_update_recommended" ] is True
579+ assert update_status ["package_update_required_first" ] is False
580+ preference_path = home / "preferences" / "update.json"
581+ assert update_status ["preference_path" ] == str (preference_path )
582+
583+ run (
584+ [sys .executable , str (workspace / ".codex" / "hooks" / "tradingcodex_hook.py" ), "session-start" ],
585+ workspace ,
586+ input_text = json .dumps ({}),
587+ env_extra = {"TRADINGCODEX_HOME" : str (home ), "TRADINGCODEX_LATEST_RELEASE_VERSION" : "999.0.0" },
588+ )
589+ blocked_status = json .loads ((workspace / ".tradingcodex" / "mainagent" / "server-status.json" ).read_text (encoding = "utf-8" ))
590+ blocked_update = blocked_status ["update_status" ]
591+ assert blocked_update ["workspace_update_available" ] is True
592+ assert blocked_update ["workspace_update_allowed" ] is False
593+ assert blocked_update ["workspace_update_recommended" ] is False
594+ assert blocked_update ["package_update_required_first" ] is True
595+ assert "older than the latest release" in blocked_update ["blocked_reason" ]
596+
597+ preference_path .parent .mkdir (parents = True )
598+ preference_path .write_text (json .dumps ({"suppress_update_recommendation" : True }) + "\n " , encoding = "utf-8" )
599+ run (
600+ [sys .executable , str (workspace / ".codex" / "hooks" / "tradingcodex_hook.py" ), "session-start" ],
601+ workspace ,
602+ input_text = json .dumps ({}),
603+ env_extra = {"TRADINGCODEX_HOME" : str (home ), "TRADINGCODEX_LATEST_RELEASE_VERSION" : TRADINGCODEX_VERSION },
604+ )
605+ suppressed_status = json .loads ((workspace / ".tradingcodex" / "mainagent" / "server-status.json" ).read_text (encoding = "utf-8" ))
606+ suppressed_update = suppressed_status ["update_status" ]
607+ assert suppressed_update ["workspace_update_available" ] is True
608+ assert suppressed_update ["workspace_update_allowed" ] is True
609+ assert suppressed_update ["workspace_update_recommended" ] is False
610+ assert suppressed_update ["update_recommendation_suppressed" ] is True
611+
504612 assert run_user_prompt_hook (workspace , "Update the docs table" ) is None
505613 assert run_user_prompt_hook (workspace , "Create a quality income strategy for dividend stocks" ) is None
506614
@@ -603,6 +711,16 @@ def test_repo_skill_templates_keep_instruction_boundary() -> None:
603711 assert (use_server / "scripts" / "summarize_connector_status.py" ).exists ()
604712 skill_text = (use_server / "SKILL.md" ).read_text (encoding = "utf-8" )
605713 assert "name: use-tradingcodex-server" in skill_text
714+ assert "## Startup Health And Dashboard" in skill_text
715+ assert "./tcx service ensure" in skill_text
716+ assert "http://127.0.0.1:48267/api/health" in skill_text
717+ assert "required startup action for a new conversation" in skill_text
718+ assert "make the browser visible" in skill_text
719+ assert "workspace_update_allowed" in skill_text
720+ assert "package_update_required_first" in skill_text
721+ assert "~/.tradingcodex/preferences/update.json" in skill_text
722+ assert "suppress_update_recommendation" in skill_text
723+ assert "fully quit and restart Codex" in skill_text
606724 assert "BrokerCapabilityProfile" not in skill_text
607725 use_server_metadata = yaml .safe_load ((use_server / "agents" / "openai.yaml" ).read_text (encoding = "utf-8" ))
608726 assert "$use-tradingcodex-server" in use_server_metadata ["interface" ]["default_prompt" ]
@@ -759,6 +877,23 @@ def test_python_generator_creates_workspace_contract(tmp_path: Path) -> None:
759877 assert "task_name" not in orchestration_guidance
760878 hook_text = (workspace / ".codex" / "hooks" / "tradingcodex_hook.py" ).read_text (encoding = "utf-8" )
761879 assert 'payload.get("agent_type")' in hook_text
880+ assert "server-status.json" in hook_text
881+ session_start = run (
882+ [sys .executable , str (workspace / ".codex" / "hooks" / "tradingcodex_hook.py" ), "session-start" ],
883+ workspace ,
884+ input_text = json .dumps ({}),
885+ )
886+ assert session_start .stdout == ""
887+ server_status = json .loads ((workspace / ".tradingcodex" / "mainagent" / "server-status.json" ).read_text (encoding = "utf-8" ))
888+ assert server_status ["service_addr" ] == "127.0.0.1:48267"
889+ assert server_status ["dashboard_url" ] == "http://127.0.0.1:48267/"
890+ assert server_status ["health_url" ] == "http://127.0.0.1:48267/api/health"
891+ assert server_status ["mcp_config_present" ] is True
892+ assert server_status ["restart_codex_required" ] is False
893+ assert server_status ["update_status" ]["versions_match" ] is True
894+ assert server_status ["update_status" ]["workspace_update_available" ] is False
895+ assert server_status ["update_status" ]["workspace_update_recommended" ] is False
896+ assert server_status ["recommended_action" ]
762897 assert not (workspace / ".tradingcodex" / "state" / "tradingcodex.sqlite3" ).exists ()
763898 assert not (workspace / ".tradingcodex" / "state" / "paper-portfolio.json" ).exists ()
764899 db_path = run (["./tcx" , "db" , "path" ], workspace ).stdout .strip ()
@@ -819,6 +954,7 @@ def test_python_generator_creates_workspace_contract(tmp_path: Path) -> None:
819954 assert hooks ["SubagentStart" ][0 ]["matcher" ]
820955 service_usage = run (["./tcx" , "service" , "nope" ], workspace , expect_ok = False )
821956 assert "Usage: tcx service runserver [addrport] [django runserver args]" in service_usage .stderr
957+ assert "tcx service ensure [addrport]" in service_usage .stderr
822958 agent_files = sorted ((workspace / ".codex" / "agents" ).glob ("*.toml" ))
823959 assert len (agent_files ) == 9
824960 actual_mcp_tools = {tool ["name" ] for tool in static_mcp_tools ()}
@@ -831,6 +967,18 @@ def test_python_generator_creates_workspace_contract(tmp_path: Path) -> None:
831967 assert "You are the `head-manager` agent" in head_manager_instructions
832968 assert "Codex-based local trading harness" in head_manager_instructions
833969 assert "asset-management workflow team" in head_manager_instructions
970+ assert "## New conversation health" in head_manager_instructions
971+ assert "When a new Codex conversation starts" in head_manager_instructions
972+ assert "before greeting with a task menu" in head_manager_instructions
973+ assert ".tradingcodex/mainagent/server-status.json" in head_manager_instructions
974+ assert "Opening the TradingCodex dashboard is mandatory" in head_manager_instructions
975+ assert "Do not merely" in head_manager_instructions
976+ assert "workspace_update_allowed" in head_manager_instructions
977+ assert "package_update_required_first" in head_manager_instructions
978+ assert "~/.tradingcodex/preferences/update.json" in head_manager_instructions
979+ assert "suppress_update_recommendation" in head_manager_instructions
980+ assert "fully quit and restart" in head_manager_instructions
981+ assert "Codex may not hot" in head_manager_instructions
834982 assert "not an autonomous trading bot" not in head_manager_instructions
835983 assert "# How you work" in head_manager_instructions
836984 assert "# TradingCodex guardrails" in head_manager_instructions
0 commit comments