personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4import asyncio
5import importlib
6import io
7import json
8import logging
9import os
10import socket
11import subprocess
12import sys
13import threading
14import time
15from datetime import date
16from pathlib import Path
17from types import SimpleNamespace
18from unittest.mock import MagicMock
19
20import psutil
21import pytest
22
23from solstone.think.maint import MaintTask, MaintTaskResult
24from solstone.think.processing import (
25 DISPLAY_POWERSAVE_UNAVAILABLE,
26 DisplayPowersaveReading,
27 DisplayPowersaveSettings,
28 GateSettings,
29 ProcessingSettings,
30 TimeWindowSettings,
31)
32from solstone.think.providers.artifact_proof import ReadinessOutcome
33from tests.helpers.module_mocks import (
34 capturing_thread_constructor,
35 module_mock,
36)
37
38
39def _mlx_readiness(
40 *,
41 model_installed: bool = True,
42 ram_sufficient: bool = True,
43 platform_supported: bool = True,
44 package_available: bool = True,
45 runtime_dir: str = "/tmp/snap",
46 model_id: str = "mlx-model",
47) -> ReadinessOutcome:
48 ready = (
49 model_installed and ram_sufficient and platform_supported and package_available
50 )
51 return ReadinessOutcome(
52 provider="local",
53 status="ready" if ready else "missing-or-mismatched",
54 reason_code="ready" if ready else "manifest_missing",
55 target={"model_id": model_id},
56 install={
57 "install_state": "idle",
58 "install_error": None,
59 "error_code": None,
60 "attempt_id": None,
61 "progress_bytes_received": None,
62 "progress_bytes_total": None,
63 "last_transition_at": None,
64 "last_progress_at": None,
65 },
66 host={
67 "platform_supported": platform_supported,
68 "package_available": package_available,
69 "ram_sufficient": ram_sufficient,
70 },
71 artifacts={
72 "model_installed": model_installed,
73 "runtime_dir": runtime_dir,
74 },
75 proof={},
76 )
77
78
79@pytest.fixture(autouse=True)
80def _default_thinking_engine_selected(monkeypatch):
81 mod = importlib.import_module("solstone.think.supervisor")
82 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: False)
83
84
85def test_sd_notify_no_socket_is_noop(monkeypatch):
86 from solstone.think.supervisor import _sd_notify
87
88 monkeypatch.delenv("NOTIFY_SOCKET", raising=False)
89 _sd_notify("READY=1")
90
91
92def test_sd_notify_sends_payload(tmp_path, monkeypatch):
93 from solstone.think.supervisor import _sd_notify
94
95 sock_path = tmp_path / "notify.sock"
96 with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as listener:
97 listener.bind(str(sock_path))
98 listener.settimeout(1)
99 monkeypatch.setenv("NOTIFY_SOCKET", str(sock_path))
100
101 _sd_notify("READY=1")
102
103 assert listener.recv(1024) == b"READY=1"
104
105
106def test_start_sense(tmp_path, mock_callosum, monkeypatch):
107 """Test that sense launches correctly."""
108 mod = importlib.import_module("solstone.think.supervisor")
109
110 started = []
111
112 class DummyProc:
113 def __init__(self):
114 self.stdout = io.StringIO()
115 self.stderr = io.StringIO()
116 self.pid = 12345
117
118 def terminate(self):
119 pass
120
121 def wait(self, timeout=None):
122 pass
123
124 def fake_popen(
125 cmd,
126 stdin=None,
127 stdout=None,
128 stderr=None,
129 text=False,
130 bufsize=-1,
131 process_group=None,
132 env=None,
133 **_kwargs,
134 ):
135 proc = DummyProc()
136 started.append((cmd, stdout, stderr))
137 return proc
138
139 monkeypatch.setattr(mod.subprocess, "Popen", fake_popen)
140 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
141
142 # Test start_sense()
143 sense_proc = mod.start_sense()
144 assert sense_proc is not None
145 assert any(cmd == ["journal", "sense", "-v"] for cmd, _, _ in started)
146
147 # Check that stdout and stderr capture pipes
148 for cmd, stdout, stderr in started:
149 assert stdout == subprocess.PIPE
150 assert stderr == subprocess.PIPE
151
152
153def test_launch_process_records_service_state(monkeypatch):
154 mod = importlib.import_module("solstone.think.supervisor")
155 mod._SERVICE_STATE.clear()
156
157 process = MagicMock()
158 process.pid = 12345
159 managed = mod.RunnerManagedProcess(
160 process=process,
161 name="unit",
162 log_writer=MagicMock(),
163 cmd=["journal", "sense"],
164 _threads=[],
165 ref="ref-1",
166 _start_time=100.0,
167 _callosum=None,
168 )
169
170 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
171 assert cmd == ["journal", "sense"]
172 assert ref == "ref-1"
173 assert day is None
174 return managed
175
176 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
177
178 result = mod._launch_process(
179 "unit",
180 ["journal", "sense"],
181 restart=True,
182 shutdown_timeout=7,
183 ref="ref-1",
184 )
185
186 assert result is managed
187 assert isinstance(result, mod.RunnerManagedProcess)
188 assert mod._SERVICE_STATE["unit"] == {
189 "restart": True,
190 "shutdown_timeout": 7,
191 }
192
193
194def test_parse_args_remote_flag():
195 """Test that parse_args includes --remote flag."""
196 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
197
198 parser = mod.parse_args()
199 args = parser.parse_args(["--remote", "https://server/ingest/key"])
200
201 assert args.remote == "https://server/ingest/key"
202
203
204def test_parse_args_remote_flag_optional():
205 """Test that --remote is optional."""
206 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
207
208 parser = mod.parse_args()
209 args = parser.parse_args([])
210
211 assert args.remote is None
212
213
214def test_parse_args_app_supervised_flag():
215 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
216
217 parser = mod.parse_args()
218 args = parser.parse_args(
219 ["5016", "--app-supervised", "--no-daily", "--no-schedule"]
220 )
221
222 assert args.port == 5016
223 assert args.app_supervised is True
224 assert args.no_daily is True
225 assert args.no_schedule is True
226
227
228def test_parse_args_lifecycle_verb_hint(monkeypatch, capsys):
229 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
230 monkeypatch.setattr(sys, "argv", ["sol", "supervisor", "stop"])
231
232 parser = mod.parse_args()
233 with pytest.raises(SystemExit) as exc_info:
234 parser.parse_args(["stop"])
235
236 captured = capsys.readouterr()
237 assert exc_info.value.code == 2
238 assert (
239 "journal supervisor is the server-launch command (takes a port). "
240 "For lifecycle, use: journal service <verb>. "
241 "Did you mean: journal service stop ?"
242 ) in captured.err
243
244
245def test_shutdown_stops_in_reverse_order(monkeypatch):
246 """Shutdown stops services in reverse order."""
247 mod = importlib.import_module("solstone.think.supervisor")
248 operations = []
249
250 class MockManaged:
251 def __init__(self, name):
252 self.name = name
253 self.terminate = MagicMock(
254 side_effect=lambda timeout=None: operations.append(
255 ("terminate", self.name, timeout)
256 )
257 )
258 self.cleanup = MagicMock(
259 side_effect=lambda: operations.append(("cleanup", self.name))
260 )
261
262 procs = [
263 MockManaged("convey"),
264 MockManaged("sense"),
265 MockManaged("cortex"),
266 ]
267 mod._SERVICE_STATE.clear()
268 for managed in procs:
269 mod._SERVICE_STATE[managed.name] = {
270 "restart": True,
271 "shutdown_timeout": 15,
272 }
273
274 for managed in reversed(procs):
275 mod._stop_process(managed)
276
277 assert operations == [
278 ("terminate", "cortex", 15),
279 ("cleanup", "cortex"),
280 ("terminate", "sense", 15),
281 ("cleanup", "sense"),
282 ("terminate", "convey", 15),
283 ("cleanup", "convey"),
284 ]
285
286
287def test_graceful_shutdown_calls_stop_process_for_each_managed_proc(
288 tmp_path, monkeypatch
289):
290 """The main shutdown path stops managed services in reverse startup order."""
291 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
292 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
293 monkeypatch.delenv("SOL_SUPERVISOR_SPAWNED", raising=False)
294 monkeypatch.setattr(
295 sys,
296 "argv",
297 ["supervisor", "0", "--no-daily", "--no-schedule"],
298 )
299 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [])
300 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
301 monkeypatch.setattr(mod.time, "sleep", lambda _seconds: None)
302 monkeypatch.setattr(mod, "start_callosum_in_process", lambda: None)
303 monkeypatch.setattr(mod, "stop_callosum_in_process", lambda **_kwargs: None)
304 monkeypatch.setattr(mod, "wait_for_convey_ready", lambda _proc: True)
305
306 class FakeCallosumConnection:
307 def __init__(self, *args, **kwargs):
308 pass
309
310 def start(self, *args, **kwargs):
311 pass
312
313 def emit(self, *args, **kwargs):
314 pass
315
316 def stop(self):
317 pass
318
319 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosumConnection)
320
321 procs = []
322 for name in ["convey", "sense", "cortex", "spl"]:
323 managed = _TaskManagedStub(cmd=["journal", name])
324 managed.name = name
325 procs.append(managed)
326
327 monkeypatch.setattr(
328 mod,
329 "start_convey_server",
330 lambda verbose, debug=False, port=0: (procs[0], 5015),
331 )
332 monkeypatch.setattr(mod, "start_sense", lambda: procs[1])
333 monkeypatch.setattr(mod, "start_cortex_server", lambda: procs[2])
334 monkeypatch.setattr(mod, "start_spl_service", lambda: procs[3])
335
336 stop_order = []
337 monkeypatch.setattr(
338 mod,
339 "_stop_process",
340 lambda managed, **_kwargs: stop_order.append(managed.name),
341 )
342
343 def interrupt_supervise(coro):
344 coro.close()
345 raise KeyboardInterrupt
346
347 monkeypatch.setattr(mod.asyncio, "run", interrupt_supervise)
348
349 try:
350 mod.main()
351 finally:
352 os.environ.pop("SOL_SUPERVISOR_SPAWNED", None)
353
354 assert stop_order == ["spl", "cortex", "sense", "convey"]
355
356
357@pytest.mark.parametrize(
358 ("convey_accepting", "expected_ready"),
359 [(True, True), (False, False)],
360)
361def test_supervisor_readiness_marker_requires_started_convey_accepting(
362 tmp_path, monkeypatch, convey_accepting, expected_ready
363):
364 """A started Convey process must still accept before supervisor marks ready."""
365 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
366 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
367 monkeypatch.delenv("SOL_SUPERVISOR_SPAWNED", raising=False)
368 monkeypatch.setattr(
369 sys,
370 "argv",
371 ["supervisor", "0", "--no-daily", "--no-schedule"],
372 )
373 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [])
374 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
375 monkeypatch.setattr(mod.time, "sleep", lambda _seconds: None)
376 monkeypatch.setattr(mod, "start_callosum_in_process", lambda: None)
377 monkeypatch.setattr(mod, "stop_callosum_in_process", lambda **_kwargs: None)
378 monkeypatch.setattr(mod, "wait_for_convey_ready", lambda _proc: True)
379 monkeypatch.setattr(mod, "is_solstone_up", lambda timeout=1.0: convey_accepting)
380 monkeypatch.setattr(mod, "is_local_provider_needed", lambda: False)
381 monkeypatch.setattr(mod, "read_service_port", lambda _name: 5015)
382
383 class FakeCallosumConnection:
384 def __init__(self, *args, **kwargs):
385 pass
386
387 def start(self, *args, **kwargs):
388 pass
389
390 def emit(self, *args, **kwargs):
391 pass
392
393 def stop(self):
394 pass
395
396 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosumConnection)
397
398 procs = []
399 for name in ["convey", "sense", "cortex", "spl"]:
400 managed = _TaskManagedStub(cmd=["journal", name])
401 managed.name = name
402 procs.append(managed)
403
404 monkeypatch.setattr(
405 mod,
406 "start_convey_server",
407 lambda verbose, debug=False, port=0: (procs[0], 5015),
408 )
409 monkeypatch.setattr(mod, "start_sense", lambda: procs[1])
410 monkeypatch.setattr(mod, "start_cortex_server", lambda: procs[2])
411 monkeypatch.setattr(mod, "start_spl_service", lambda: procs[3])
412 monkeypatch.setattr(mod, "_stop_process", lambda managed, **_kwargs: None)
413
414 events: list[str] = []
415 monkeypatch.setattr(mod, "signal_ready", lambda: events.append("ready"))
416
417 def interrupt_supervise(coro):
418 coro.close()
419 raise KeyboardInterrupt
420
421 monkeypatch.setattr(mod.asyncio, "run", interrupt_supervise)
422
423 try:
424 mod.main()
425 finally:
426 os.environ.pop("SOL_SUPERVISOR_SPAWNED", None)
427
428 assert ("ready" in events) is expected_ready
429
430
431def _run_supervisor_main_for_shutdown_knobs(tmp_path, monkeypatch, *, argv):
432 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
433 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
434 monkeypatch.delenv("SOL_SUPERVISOR_SPAWNED", raising=False)
435 monkeypatch.delenv("SOLSTONE_APP_SUPERVISED", raising=False)
436 monkeypatch.setattr(sys, "argv", argv)
437 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [])
438 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
439 monkeypatch.setattr(mod.time, "sleep", lambda _seconds: None)
440 monkeypatch.setattr(mod, "start_callosum_in_process", lambda: None)
441 monkeypatch.setattr(mod, "is_local_provider_needed", lambda: False)
442
443 class FakeCallosumConnection:
444 def __init__(self, *args, **kwargs):
445 pass
446
447 def start(self, *args, **kwargs):
448 pass
449
450 def emit(self, *args, **kwargs):
451 pass
452
453 def stop(self):
454 pass
455
456 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosumConnection)
457
458 managed = _TaskManagedStub(cmd=["journal", "sense"])
459 managed.name = "sense"
460 monkeypatch.setattr(mod, "start_sense", lambda: managed)
461
462 events: list[str] = []
463 captures: dict[str, list] = {
464 "task_shutdown": [],
465 "stop_process": [],
466 "callosum_join": [],
467 }
468
469 class FakeTaskQueue:
470 def __init__(self, *args, **kwargs):
471 self.caps = {}
472
473 @staticmethod
474 def get_command_name(cmd):
475 return mod._command_partition(cmd)
476
477 def set_cap(self, cmd_name, seconds):
478 self.caps[cmd_name] = seconds
479
480 def set_ready(self):
481 pass
482
483 def shutdown(self, *, timeout):
484 captures["task_shutdown"].append(timeout)
485
486 monkeypatch.setattr(mod, "TaskQueue", FakeTaskQueue)
487 monkeypatch.setattr(mod, "signal_ready", lambda: events.append("ready"))
488
489 def record_watcher_start():
490 events.append("watcher")
491 assert mod._managed_procs == [managed]
492
493 monkeypatch.setattr(mod, "start_parent_death_watcher", record_watcher_start)
494 monkeypatch.setattr(
495 mod,
496 "_stop_process",
497 lambda proc, *, timeout_cap=None: captures["stop_process"].append(
498 (proc.name, timeout_cap)
499 ),
500 )
501 monkeypatch.setattr(
502 mod,
503 "stop_callosum_in_process",
504 lambda *, join_timeout=5.0: captures["callosum_join"].append(join_timeout),
505 )
506 exit_now = MagicMock()
507 monkeypatch.setattr(mod.os, "_exit", exit_now)
508
509 def interrupt_supervise(coro):
510 events.append("run")
511 coro.close()
512 raise KeyboardInterrupt
513
514 monkeypatch.setattr(mod.asyncio, "run", interrupt_supervise)
515
516 try:
517 mod.main()
518 finally:
519 os.environ.pop("SOL_SUPERVISOR_SPAWNED", None)
520
521 return mod, captures, events, exit_now
522
523
524def test_app_supervised_main_uses_watcher_and_compressed_shutdown_knobs(
525 tmp_path, monkeypatch
526):
527 from solstone.think import install_guard, service
528
529 reconcile = MagicMock()
530 install_wrappers = MagicMock()
531 monkeypatch.setattr(service, "reconcile_installed_unit", reconcile)
532 monkeypatch.setattr(install_guard, "install_wrappers", install_wrappers)
533
534 mod, captures, events, exit_now = _run_supervisor_main_for_shutdown_knobs(
535 tmp_path,
536 monkeypatch,
537 argv=[
538 "supervisor",
539 "0",
540 "--app-supervised",
541 "--no-daily",
542 "--no-schedule",
543 "--no-convey",
544 "--no-cortex",
545 "--no-spl",
546 ],
547 )
548
549 assert events == ["ready", "watcher", "run"]
550 assert captures["task_shutdown"] == [mod.APP_SUPERVISED_TASK_DRAIN_S]
551 assert captures["stop_process"] == [("sense", mod.APP_SUPERVISED_CHILD_STOP_S)]
552 assert captures["callosum_join"] == [mod.APP_SUPERVISED_CALLOSUM_JOIN_S]
553 exit_now.assert_not_called()
554 reconcile.assert_not_called()
555 install_wrappers.assert_not_called()
556
557
558def test_default_main_uses_default_shutdown_knobs(tmp_path, monkeypatch):
559 mod, captures, events, _exit_now = _run_supervisor_main_for_shutdown_knobs(
560 tmp_path,
561 monkeypatch,
562 argv=[
563 "supervisor",
564 "0",
565 "--no-daily",
566 "--no-schedule",
567 "--no-convey",
568 "--no-cortex",
569 "--no-spl",
570 ],
571 )
572
573 assert events == ["ready", "run"]
574 assert captures["task_shutdown"] == [10]
575 assert captures["stop_process"] == [("sense", None)]
576 assert captures["callosum_join"] == [5.0]
577
578
579def test_get_command_name():
580 """Test command name extraction for queue serialization."""
581 mod = importlib.import_module("solstone.think.supervisor")
582 get = mod.TaskQueue.get_command_name
583
584 # sol X -> X
585 assert get(["journal", "indexer", "--rescan"]) == "indexer"
586 assert get(["sol", "insight", "20240101"]) == "insight"
587 assert get(["journal", "think", "--day", "20240101"]) == "daily"
588 assert get(["journal", "brain", "renew-prerequisites"]) == "brain"
589 assert get(["journal", "maintenance", "list"]) == "maintenance"
590 assert get(["journal", "maintenance", "run", "foo:bar"]) == "maintenance:foo:bar"
591 assert get(["journal", "maintenance", "run", "baz:qux"]) == "maintenance:baz:qux"
592 assert get(["journal", "maintenance", "run", "foo:bar"]) == get(
593 ["journal", "maintenance", "run", "foo:bar"]
594 )
595 assert get(["journal", "maintenance", "run", "foo:bar"]) != get(
596 ["journal", "maintenance", "run", "baz:qux"]
597 )
598
599 # Other commands -> basename
600 assert get(["/usr/bin/python", "script.py"]) == "python"
601 assert get(["custom-tool"]) == "custom-tool"
602
603 # Empty -> unknown
604 assert get([]) == "unknown"
605
606
607@pytest.mark.parametrize(
608 "cmd",
609 [
610 ["journal", "think", "--day", "20260527"],
611 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
612 [
613 "journal",
614 "think",
615 "--day",
616 "20260527",
617 "--segment",
618 "120000_300",
619 "--stream",
620 "screen",
621 ],
622 [
623 "journal",
624 "think",
625 "--day",
626 "20260527",
627 "--segment",
628 "120000_300",
629 "--flush",
630 ],
631 ["journal", "think", "--day", "20260527", "--segments"],
632 [
633 "journal",
634 "think",
635 "--activity",
636 "activity-id",
637 "--facet",
638 "work",
639 "--day",
640 "20260527",
641 ],
642 ["journal", "think", "--weekly", "-v"],
643 ["journal", "think"],
644 ["journal", "indexer", "--rescan"],
645 ["journal", "sense", "--day", "20260101"],
646 ["journal", "maintenance", "list"],
647 ["journal", "maintenance", "run", "foo:bar"],
648 ],
649)
650def test_command_partition_matches_task_queue_get_command_name(cmd):
651 mod = importlib.import_module("solstone.think.supervisor")
652 runner = importlib.import_module("solstone.think.runner")
653
654 assert runner._command_partition(cmd) == mod.TaskQueue.get_command_name(cmd)
655
656
657def _fresh_task_queue(mod, *, on_queue_change=None):
658 mod._task_queue = mod.TaskQueue(on_queue_change=on_queue_change)
659 mod._supervisor_callosum = None
660 return mod._task_queue
661
662
663def _capture_thread_starts(monkeypatch, mod):
664 spawned = []
665 monkeypatch.setattr(
666 mod,
667 "threading",
668 module_mock(
669 mod.threading,
670 Thread=capturing_thread_constructor(
671 spawned,
672 capture=lambda thread: thread._args,
673 ),
674 ),
675 )
676 return spawned
677
678
679class _CaptureTaskQueue:
680 def __init__(self):
681 self.submissions = []
682
683 def submit(self, cmd, day=None):
684 self.submissions.append({"cmd": cmd, "day": day})
685
686
687def _supervisor_processing_settings(
688 mode: str,
689 *,
690 display_powersave_enabled: bool = False,
691) -> ProcessingSettings:
692 return ProcessingSettings(
693 mode=mode,
694 gate=GateSettings(
695 time_window=TimeWindowSettings(
696 enabled=True,
697 start="02:00",
698 end="06:00",
699 ),
700 display_powersave=DisplayPowersaveSettings(
701 enabled=display_powersave_enabled
702 ),
703 ),
704 )
705
706
707def test_handle_segment_observed_live_command_marks_live(monkeypatch):
708 mod = importlib.import_module("solstone.think.supervisor")
709 capture = _CaptureTaskQueue()
710 monkeypatch.setattr(mod, "_task_queue", capture)
711
712 mod._handle_segment_observed(
713 {
714 "tract": "observe",
715 "event": "observed",
716 "day": "20260527",
717 "segment": "120000_300",
718 }
719 )
720
721 assert len(capture.submissions) == 1
722 assert capture.submissions[0]["day"] == "20260527"
723 assert "--live" in capture.submissions[0]["cmd"]
724
725
726def test_handle_segment_observed_batch_submits_nothing(monkeypatch):
727 mod = importlib.import_module("solstone.think.supervisor")
728 capture = _CaptureTaskQueue()
729 monkeypatch.setattr(mod, "_task_queue", capture)
730
731 mod._handle_segment_observed(
732 {
733 "tract": "observe",
734 "event": "observed",
735 "day": "20260527",
736 "segment": "120000_300",
737 "batch": True,
738 }
739 )
740
741 assert len(capture.submissions) == 0
742
743
744def test_handle_segment_observed_batch_leaves_flush_state_untouched(monkeypatch):
745 mod = importlib.import_module("solstone.think.supervisor")
746 capture = _CaptureTaskQueue()
747 monkeypatch.setattr(mod, "_task_queue", capture)
748
749 mod._flush_state["last_segment_ts"] = 0.0
750 mod._flush_state["day"] = None
751 mod._flush_state["segment"] = None
752 mod._flush_state["flushed"] = True
753
754 mod._handle_segment_observed(
755 {
756 "tract": "observe",
757 "event": "observed",
758 "day": "20260527",
759 "segment": "120000_300",
760 }
761 )
762
763 assert len(capture.submissions) == 1
764 live_state = {
765 "day": mod._flush_state["day"],
766 "segment": mod._flush_state["segment"],
767 "flushed": mod._flush_state["flushed"],
768 "last_segment_ts": mod._flush_state["last_segment_ts"],
769 }
770 assert live_state["day"] == "20260527"
771 assert live_state["segment"] == "120000_300"
772 assert live_state["flushed"] is False
773 assert live_state["last_segment_ts"] > 0
774
775 mod._handle_segment_observed(
776 {
777 "tract": "observe",
778 "event": "observed",
779 "day": "20260101",
780 "segment": "090000_300",
781 "batch": True,
782 }
783 )
784
785 assert len(capture.submissions) == 1
786 assert mod._flush_state["day"] == live_state["day"]
787 assert mod._flush_state["segment"] == live_state["segment"]
788 assert mod._flush_state["flushed"] == live_state["flushed"]
789 assert mod._flush_state["last_segment_ts"] == live_state["last_segment_ts"]
790
791
792def test_handle_segment_observed_live_stream_command(monkeypatch):
793 mod = importlib.import_module("solstone.think.supervisor")
794 capture = _CaptureTaskQueue()
795 monkeypatch.setattr(mod, "_task_queue", capture)
796
797 mod._handle_segment_observed(
798 {
799 "tract": "observe",
800 "event": "observed",
801 "day": "20260527",
802 "segment": "120000_300",
803 "stream": "archon",
804 }
805 )
806
807 assert len(capture.submissions) == 1
808 cmd = capture.submissions[0]["cmd"]
809 assert "--live" in cmd
810 stream_index = cmd.index("--stream")
811 assert cmd[stream_index + 1] == "archon"
812
813
814def test_handle_segment_observed_deferred_live_submits_nothing(monkeypatch):
815 mod = importlib.import_module("solstone.think.supervisor")
816 capture = _CaptureTaskQueue()
817 monkeypatch.setattr(mod, "_task_queue", capture)
818 monkeypatch.setattr(
819 mod,
820 "load_processing_settings",
821 lambda: _supervisor_processing_settings("deferred"),
822 )
823 mod._flush_state.update(
824 {
825 "last_segment_ts": 123.0,
826 "day": "20260526",
827 "segment": "110000_300",
828 "stream": "archon",
829 "flushed": True,
830 }
831 )
832 before = dict(mod._flush_state)
833
834 mod._handle_segment_observed(
835 {
836 "tract": "observe",
837 "event": "observed",
838 "day": "20260527",
839 "segment": "120000_300",
840 }
841 )
842
843 assert capture.submissions == []
844 assert dict(mod._flush_state) == before
845
846
847def test_handle_segment_observed_no_engine_submits_nothing(monkeypatch):
848 mod = importlib.import_module("solstone.think.supervisor")
849 capture = _CaptureTaskQueue()
850 monkeypatch.setattr(mod, "_task_queue", capture)
851 monkeypatch.setattr(
852 mod,
853 "load_processing_settings",
854 lambda: _supervisor_processing_settings("realtime"),
855 )
856 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: True)
857 mod._flush_state.update(
858 {
859 "last_segment_ts": 123.0,
860 "day": "20260526",
861 "segment": "110000_300",
862 "stream": "archon",
863 "flushed": True,
864 }
865 )
866 before = dict(mod._flush_state)
867
868 mod._handle_segment_observed(
869 {
870 "tract": "observe",
871 "event": "observed",
872 "day": "20260527",
873 "segment": "120000_300",
874 }
875 )
876
877 assert capture.submissions == []
878 assert dict(mod._flush_state) == before
879
880
881def test_handle_segment_observed_reads_processing_mode_fresh(monkeypatch):
882 mod = importlib.import_module("solstone.think.supervisor")
883 capture = _CaptureTaskQueue()
884 monkeypatch.setattr(mod, "_task_queue", capture)
885 modes = ["realtime", "deferred"]
886
887 def load_settings():
888 return _supervisor_processing_settings(modes.pop(0))
889
890 monkeypatch.setattr(mod, "load_processing_settings", load_settings)
891
892 mod._handle_segment_observed(
893 {
894 "tract": "observe",
895 "event": "observed",
896 "day": "20260527",
897 "segment": "120000_300",
898 }
899 )
900 after_realtime = dict(mod._flush_state)
901 mod._handle_segment_observed(
902 {
903 "tract": "observe",
904 "event": "observed",
905 "day": "20260527",
906 "segment": "120500_300",
907 }
908 )
909
910 assert len(capture.submissions) == 1
911 assert capture.submissions[0]["cmd"][-1] == "--live"
912 assert dict(mod._flush_state) == after_realtime
913
914
915def test_check_segment_flush_deferred_guard_submits_nothing(monkeypatch):
916 mod = importlib.import_module("solstone.think.supervisor")
917 capture = _CaptureTaskQueue()
918 monkeypatch.setattr(mod, "_task_queue", capture)
919 monkeypatch.setattr(
920 mod,
921 "load_processing_settings",
922 lambda: _supervisor_processing_settings("deferred"),
923 )
924 mod._flush_state.update(
925 {
926 "last_segment_ts": 1.0,
927 "day": "20260527",
928 "segment": "120000_300",
929 "stream": None,
930 "flushed": False,
931 }
932 )
933
934 mod._check_segment_flush()
935
936 assert capture.submissions == []
937 assert mod._flush_state["flushed"] is False
938
939
940def test_check_segment_flush_no_engine_guard_submits_nothing(monkeypatch):
941 mod = importlib.import_module("solstone.think.supervisor")
942 capture = _CaptureTaskQueue()
943 monkeypatch.setattr(mod, "_task_queue", capture)
944 monkeypatch.setattr(
945 mod,
946 "load_processing_settings",
947 lambda: _supervisor_processing_settings("realtime"),
948 )
949 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: True)
950 mod._flush_state.update(
951 {
952 "last_segment_ts": 1.0,
953 "day": "20260527",
954 "segment": "120000_300",
955 "stream": None,
956 "flushed": False,
957 }
958 )
959
960 mod._check_segment_flush()
961
962 assert capture.submissions == []
963 assert mod._flush_state["flushed"] is False
964
965
966def test_check_segment_flush_realtime_submits(monkeypatch):
967 mod = importlib.import_module("solstone.think.supervisor")
968 capture = _CaptureTaskQueue()
969 monkeypatch.setattr(mod, "_task_queue", capture)
970 monkeypatch.setattr(
971 mod,
972 "load_processing_settings",
973 lambda: _supervisor_processing_settings("realtime"),
974 )
975 monkeypatch.setattr(mod.time, "time", lambda: 10_000.0)
976 mod._flush_state.update(
977 {
978 "last_segment_ts": 1.0,
979 "day": "20260527",
980 "segment": "120000_300",
981 "stream": None,
982 "flushed": False,
983 }
984 )
985
986 mod._check_segment_flush()
987
988 assert len(capture.submissions) == 1
989 assert "--flush" in capture.submissions[0]["cmd"]
990 assert mod._flush_state["flushed"] is True
991
992
993def test_task_queue_daily_and_segment_run_independently(monkeypatch):
994 mod = importlib.import_module("solstone.think.supervisor")
995 queue = _fresh_task_queue(mod)
996 _capture_thread_starts(monkeypatch, mod)
997
998 queue.submit(["journal", "think", "--day", "20260527"], ref="daily-ref")
999 queue.submit(
1000 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1001 ref="segment-ref",
1002 )
1003
1004 assert set(queue._running) == {"daily", "segment"}
1005 assert queue._queues == {}
1006
1007
1008def test_task_queue_segment_and_flush_run_independently(monkeypatch):
1009 mod = importlib.import_module("solstone.think.supervisor")
1010 queue = _fresh_task_queue(mod)
1011 _capture_thread_starts(monkeypatch, mod)
1012
1013 queue.submit(
1014 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1015 ref="segment-ref",
1016 )
1017 queue.submit(
1018 [
1019 "journal",
1020 "think",
1021 "--day",
1022 "20260527",
1023 "--segment",
1024 "120000_300",
1025 "--flush",
1026 ],
1027 ref="flush-ref",
1028 )
1029
1030 assert set(queue._running) == {"segment", "flush"}
1031 assert queue._queues == {}
1032
1033
1034def test_task_queue_daily_and_activity_run_independently(monkeypatch):
1035 mod = importlib.import_module("solstone.think.supervisor")
1036 queue = _fresh_task_queue(mod)
1037 _capture_thread_starts(monkeypatch, mod)
1038
1039 queue.submit(["journal", "think", "--day", "20260527"], ref="daily-ref")
1040 queue.submit(
1041 [
1042 "journal",
1043 "think",
1044 "--activity",
1045 "activity-id",
1046 "--facet",
1047 "work",
1048 "--day",
1049 "20260527",
1050 ],
1051 ref="activity-ref",
1052 )
1053
1054 assert set(queue._running) == {"daily", "activity"}
1055 assert queue._queues == {}
1056
1057
1058def test_task_queue_daily_and_weekly_run_independently(monkeypatch):
1059 mod = importlib.import_module("solstone.think.supervisor")
1060 queue = _fresh_task_queue(mod)
1061 _capture_thread_starts(monkeypatch, mod)
1062
1063 queue.submit(["journal", "think", "--day", "20260527"], ref="daily-ref")
1064 queue.submit(["journal", "think", "--weekly", "-v"], ref="weekly-ref")
1065
1066 assert set(queue._running) == {"daily", "weekly"}
1067 assert queue._queues == {}
1068
1069
1070def test_task_queue_segments_plural_shares_segment_partition(monkeypatch):
1071 mod = importlib.import_module("solstone.think.supervisor")
1072 queue = _fresh_task_queue(mod)
1073 _capture_thread_starts(monkeypatch, mod)
1074
1075 queue.submit(
1076 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1077 ref="segment-ref",
1078 )
1079 queue.submit(
1080 ["journal", "think", "--day", "20260527", "--segments"],
1081 ref="segments-ref",
1082 )
1083
1084 assert set(queue._running) == {"segment"}
1085 assert queue._queues["segment"][0]["refs"] == ["segments-ref"]
1086 assert queue._queues["segment"][0]["cmd"] == [
1087 "journal",
1088 "think",
1089 "--day",
1090 "20260527",
1091 "--segments",
1092 ]
1093
1094
1095def test_task_queue_flush_flag_precedes_segment_flag():
1096 runner = importlib.import_module("solstone.think.runner")
1097
1098 assert (
1099 runner._command_partition(
1100 ["journal", "think", "--day", "20260527", "--segment", "00", "--flush"]
1101 )
1102 == "flush"
1103 )
1104
1105
1106def test_task_queue_within_mode_serialization_segment(monkeypatch):
1107 mod = importlib.import_module("solstone.think.supervisor")
1108 queue = _fresh_task_queue(mod)
1109 spawned = _capture_thread_starts(monkeypatch, mod)
1110
1111 queue.submit(
1112 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1113 ref="first-ref",
1114 )
1115 queue.submit(
1116 ["journal", "think", "--day", "20260527", "--segment", "120500_300"],
1117 ref="second-ref",
1118 )
1119
1120 assert set(queue._running) == {"segment"}
1121 assert queue._running["segment"]["ref"] == "first-ref"
1122 assert set(queue._queues) == {"segment"}
1123 assert queue._queues["segment"][0]["refs"] == ["second-ref"]
1124
1125 queue._process_next("segment")
1126
1127 assert queue._running["segment"]["ref"] == "second-ref"
1128 assert queue._queues["segment"] == []
1129 assert spawned[-1][0] == ["second-ref"]
1130
1131
1132def test_task_queue_dedup_within_segment_partition(monkeypatch):
1133 mod = importlib.import_module("solstone.think.supervisor")
1134 queue = _fresh_task_queue(mod)
1135 _capture_thread_starts(monkeypatch, mod)
1136 cmd = ["journal", "think", "--day", "20260527", "--segment", "120000_300"]
1137
1138 queue.submit(cmd, ref="first-ref")
1139 queue.submit(cmd, ref="second-ref")
1140 queue.submit(cmd, ref="third-ref")
1141
1142 assert queue._running["segment"]["ref"] == "first-ref"
1143 assert len(queue._queues["segment"]) == 1
1144 assert queue._queues["segment"][0]["cmd"] == cmd
1145 assert queue._queues["segment"][0]["refs"] == ["second-ref", "third-ref"]
1146
1147
1148def test_task_queue_stale_thread_reclamation_per_partition(monkeypatch):
1149 mod = importlib.import_module("solstone.think.supervisor")
1150 dead_thread = threading.Thread(target=lambda: None)
1151 dead_thread.start()
1152 dead_thread.join()
1153
1154 queue = _fresh_task_queue(mod)
1155 _capture_thread_starts(monkeypatch, mod)
1156
1157 queue.submit(["journal", "think", "--day", "20260527"], ref="old-daily-ref")
1158 queue.submit(
1159 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1160 ref="segment-ref",
1161 )
1162
1163 queue._running["daily"]["thread"] = dead_thread
1164
1165 queue.submit(["journal", "think", "--day", "20260528"], ref="new-daily-ref")
1166
1167 assert queue._running["daily"]["ref"] == "new-daily-ref"
1168 assert queue._running["segment"]["ref"] == "segment-ref"
1169 assert set(queue._running) == {"daily", "segment"}
1170
1171
1172def test_handle_task_request_routes_to_mode_partition(monkeypatch):
1173 mod = importlib.import_module("solstone.think.supervisor")
1174 queue = _fresh_task_queue(mod)
1175 _capture_thread_starts(monkeypatch, mod)
1176
1177 queue.submit(["journal", "think", "--day", "20260527"], ref="daily-ref")
1178 mod._handle_task_request(
1179 {
1180 "tract": "supervisor",
1181 "event": "request",
1182 "cmd": ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1183 "ref": "segment-ref",
1184 }
1185 )
1186
1187 assert queue._running["daily"]["ref"] == "daily-ref"
1188 assert queue._running["segment"]["ref"] == "segment-ref"
1189 assert queue._queues == {}
1190
1191
1192def test_scheduler_weekly_cap_registers_under_weekly(monkeypatch):
1193 mod = importlib.import_module("solstone.think.supervisor")
1194 queue = mod.TaskQueue(on_queue_change=None)
1195 monkeypatch.setattr(
1196 mod.scheduler,
1197 "collect_runtime_caps",
1198 lambda: [(["journal", "think", "--weekly", "-v"], 60.0)],
1199 )
1200
1201 for cmd, seconds in mod.scheduler.collect_runtime_caps():
1202 queue.set_cap(mod.TaskQueue.get_command_name(cmd), seconds)
1203
1204 assert queue._caps == {"weekly": 60.0}
1205
1206
1207def test_reactive_task_caps_values():
1208 mod = importlib.import_module("solstone.think.supervisor")
1209
1210 assert mod.REACTIVE_TASK_CAPS == {
1211 "daily": 21600,
1212 "segment": 4500,
1213 "indexer": 7200,
1214 "importer": 3600,
1215 }
1216
1217
1218def test_register_baseline_caps_sets_explicit_caps():
1219 mod = importlib.import_module("solstone.think.supervisor")
1220 queue = mod.TaskQueue(on_queue_change=None)
1221
1222 mod.register_baseline_caps(queue)
1223
1224 backup_partition = mod.TaskQueue.get_command_name(
1225 ["journal", "maintenance", "run", "backup:run"]
1226 )
1227 expected = {
1228 "daily": 21600,
1229 "segment": 4500,
1230 "indexer": 7200,
1231 "importer": 3600,
1232 backup_partition: 49 * 60 * 60,
1233 }
1234 for name, seconds in expected.items():
1235 assert queue._effective_cap(name) == seconds
1236 assert queue._effective_cap(name) != mod.DEFAULT_TASK_MAX_RUNTIME
1237
1238
1239def test_from_scratch_reprocess_resolves_to_daily():
1240 mod = importlib.import_module("solstone.think.supervisor")
1241
1242 assert (
1243 mod.TaskQueue.get_command_name(
1244 ["journal", "think", "-v", "--day", "20260527", "--from-scratch"]
1245 )
1246 == "daily"
1247 )
1248
1249
1250def test_queue_event_carries_mode_partition_name(monkeypatch):
1251 mod = importlib.import_module("solstone.think.supervisor")
1252 events = []
1253 queue = _fresh_task_queue(
1254 mod,
1255 on_queue_change=lambda command, running, queued: events.append(
1256 (command, running, queued)
1257 ),
1258 )
1259 _capture_thread_starts(monkeypatch, mod)
1260
1261 queue.submit(
1262 ["journal", "think", "--day", "20260527", "--segment", "120000_300"],
1263 ref="first-ref",
1264 )
1265 queue.submit(
1266 ["journal", "think", "--day", "20260527", "--segment", "120500_300"],
1267 ref="second-ref",
1268 )
1269
1270 assert events[-1][0] == "segment"
1271
1272
1273def test_no_literal_think_queue_keys_in_source():
1274 mod = importlib.import_module("solstone.think.supervisor")
1275
1276 assert mod.TaskQueue.get_command_name(
1277 ["journal", "think", "--day", "20260527"]
1278 ) != ("think")
1279
1280
1281def test_task_queue_same_command_queued(monkeypatch):
1282 """Test that same command is queued when already running."""
1283 mod = importlib.import_module("solstone.think.supervisor")
1284
1285 # Create fresh task queue (no callback to avoid callosum events)
1286 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1287
1288 spawned = _capture_thread_starts(monkeypatch, mod)
1289
1290 # First request - should run immediately
1291 msg1 = {
1292 "tract": "supervisor",
1293 "event": "request",
1294 "cmd": ["journal", "indexer", "--rescan"],
1295 }
1296 mod._handle_task_request(msg1)
1297
1298 assert "indexer" in mod._task_queue._running
1299 assert len(spawned) == 1
1300
1301 # Second request (different args) - should be queued
1302 msg2 = {
1303 "tract": "supervisor",
1304 "event": "request",
1305 "cmd": ["journal", "indexer", "--rescan-full"],
1306 }
1307 mod._handle_task_request(msg2)
1308
1309 assert len(spawned) == 1 # No new spawn
1310 assert "indexer" in mod._task_queue._queues
1311 assert len(mod._task_queue._queues["indexer"]) == 1
1312 # Queue entries are {refs, cmd} dicts (refs is a list for coalescing)
1313 assert mod._task_queue._queues["indexer"][0]["cmd"] == [
1314 "journal",
1315 "indexer",
1316 "--rescan-full",
1317 ]
1318 assert len(mod._task_queue._queues["indexer"][0]["refs"]) == 1
1319
1320
1321def test_task_queue_dedupe_exact_match(monkeypatch):
1322 """Test that exact same command is deduped in queue."""
1323 mod = importlib.import_module("solstone.think.supervisor")
1324
1325 # Create fresh task queue (no callback to avoid callosum events)
1326 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1327
1328 _capture_thread_starts(monkeypatch, mod)
1329
1330 # First request - runs
1331 msg1 = {
1332 "tract": "supervisor",
1333 "event": "request",
1334 "cmd": ["journal", "indexer", "--rescan"],
1335 }
1336 mod._handle_task_request(msg1)
1337
1338 # Second request (same cmd) - queued
1339 msg2 = {
1340 "tract": "supervisor",
1341 "event": "request",
1342 "cmd": ["journal", "indexer", "--rescan"],
1343 }
1344 mod._handle_task_request(msg2)
1345
1346 assert len(mod._task_queue._queues["indexer"]) == 1
1347
1348 # Third request (same cmd again) - deduped, not added
1349 msg3 = {
1350 "tract": "supervisor",
1351 "event": "request",
1352 "cmd": ["journal", "indexer", "--rescan"],
1353 }
1354 mod._handle_task_request(msg3)
1355
1356 assert len(mod._task_queue._queues["indexer"]) == 1 # Still just 1
1357
1358
1359def test_task_queue_different_commands_independent(monkeypatch):
1360 """Test that different commands have independent queues."""
1361 mod = importlib.import_module("solstone.think.supervisor")
1362
1363 # Create fresh task queue (no callback to avoid callosum events)
1364 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1365
1366 spawned = _capture_thread_starts(monkeypatch, mod)
1367
1368 # Indexer request - runs
1369 msg1 = {
1370 "tract": "supervisor",
1371 "event": "request",
1372 "cmd": ["journal", "indexer", "--rescan"],
1373 }
1374 mod._handle_task_request(msg1)
1375
1376 # Insight request - also runs (different command)
1377 msg2 = {
1378 "tract": "supervisor",
1379 "event": "request",
1380 "cmd": ["sol", "insight", "20240101"],
1381 }
1382 mod._handle_task_request(msg2)
1383
1384 assert len(spawned) == 2 # Both spawned
1385 assert "indexer" in mod._task_queue._running
1386 assert "insight" in mod._task_queue._running
1387
1388
1389def test_process_queue_spawns_next(monkeypatch):
1390 """Test that _process_next spawns next queued task."""
1391 mod = importlib.import_module("solstone.think.supervisor")
1392
1393 # Create task queue with pre-set state
1394 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1395 mod._task_queue._running = {"indexer": {"ref": "ref123", "thread": None}}
1396 mod._task_queue._queues = {
1397 "indexer": [
1398 {"refs": ["queued-ref"], "cmd": ["journal", "indexer", "--rescan-full"]}
1399 ]
1400 }
1401
1402 spawned = _capture_thread_starts(monkeypatch, mod)
1403
1404 # Process queue
1405 mod._task_queue._process_next("indexer")
1406
1407 # Should have spawned the queued task with its refs list
1408 assert len(spawned) == 1
1409 assert spawned[0][0] == ["queued-ref"] # refs list preserved from queue
1410 assert spawned[0][1] == ["journal", "indexer", "--rescan-full"] # cmd
1411 assert spawned[0][2] == "indexer" # cmd_name
1412
1413 # Queue should be empty now
1414 assert mod._task_queue._queues["indexer"] == []
1415
1416
1417def test_process_queue_clears_running_when_empty(monkeypatch):
1418 """Test that _process_next clears running state when queue is empty."""
1419 mod = importlib.import_module("solstone.think.supervisor")
1420
1421 # Create task queue with pre-set state (no queued tasks)
1422 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1423 mod._task_queue._running = {"indexer": {"ref": "ref123", "thread": None}}
1424 mod._task_queue._queues = {"indexer": []}
1425
1426 spawned = _capture_thread_starts(monkeypatch, mod)
1427
1428 # Process queue
1429 mod._task_queue._process_next("indexer")
1430
1431 # No spawn (queue was empty)
1432 assert len(spawned) == 0
1433
1434 # Running state should be cleared
1435 assert "indexer" not in mod._task_queue._running
1436
1437
1438def test_task_request_uses_caller_provided_ref(monkeypatch):
1439 """Test that caller-provided ref is used and preserved through queue."""
1440 mod = importlib.import_module("solstone.think.supervisor")
1441
1442 # Create fresh task queue (no callback to avoid callosum events)
1443 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1444
1445 spawned = _capture_thread_starts(monkeypatch, mod)
1446
1447 # Request with caller-provided ref
1448 msg = {
1449 "tract": "supervisor",
1450 "event": "request",
1451 "cmd": ["journal", "indexer", "--rescan"],
1452 "ref": "my-custom-ref-123",
1453 }
1454 mod._handle_task_request(msg)
1455
1456 # Should use the provided ref
1457 assert mod._task_queue._running["indexer"]["ref"] == "my-custom-ref-123"
1458 assert spawned[0][0] == ["my-custom-ref-123"] # refs is a list
1459
1460
1461def test_task_queue_preserves_caller_ref(monkeypatch):
1462 """Test that queued requests preserve their caller-provided ref."""
1463 mod = importlib.import_module("solstone.think.supervisor")
1464
1465 # Create fresh task queue (no callback to avoid callosum events)
1466 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1467
1468 _capture_thread_starts(monkeypatch, mod)
1469
1470 # First request runs immediately
1471 msg1 = {
1472 "tract": "supervisor",
1473 "event": "request",
1474 "cmd": ["journal", "indexer", "--rescan"],
1475 "ref": "first-ref",
1476 }
1477 mod._handle_task_request(msg1)
1478
1479 # Second request gets queued with its own ref
1480 msg2 = {
1481 "tract": "supervisor",
1482 "event": "request",
1483 "cmd": ["journal", "indexer", "--rescan-full"],
1484 "ref": "second-ref",
1485 }
1486 mod._handle_task_request(msg2)
1487
1488 # Verify queued entry has the caller's ref in refs list
1489 assert len(mod._task_queue._queues["indexer"]) == 1
1490 assert mod._task_queue._queues["indexer"][0]["refs"] == ["second-ref"]
1491 assert mod._task_queue._queues["indexer"][0]["cmd"] == [
1492 "journal",
1493 "indexer",
1494 "--rescan-full",
1495 ]
1496
1497
1498def test_task_queue_coalesces_refs_on_dedupe(monkeypatch):
1499 """Test that duplicate queued requests coalesce their refs."""
1500 mod = importlib.import_module("solstone.think.supervisor")
1501
1502 # Create fresh task queue (no callback to avoid callosum events)
1503 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1504
1505 _capture_thread_starts(monkeypatch, mod)
1506
1507 # First request runs immediately
1508 msg1 = {
1509 "tract": "supervisor",
1510 "event": "request",
1511 "cmd": ["journal", "indexer", "--rescan"],
1512 "ref": "first-ref",
1513 }
1514 mod._handle_task_request(msg1)
1515
1516 # Second request (same cmd) gets queued
1517 msg2 = {
1518 "tract": "supervisor",
1519 "event": "request",
1520 "cmd": ["journal", "indexer", "--rescan"],
1521 "ref": "second-ref",
1522 }
1523 mod._handle_task_request(msg2)
1524
1525 # Third request (same cmd) should coalesce its ref into existing queue entry
1526 msg3 = {
1527 "tract": "supervisor",
1528 "event": "request",
1529 "cmd": ["journal", "indexer", "--rescan"],
1530 "ref": "third-ref",
1531 }
1532 mod._handle_task_request(msg3)
1533
1534 # Should still be just one queue entry
1535 assert len(mod._task_queue._queues["indexer"]) == 1
1536 # But it should have both refs
1537 assert mod._task_queue._queues["indexer"][0]["refs"] == [
1538 "second-ref",
1539 "third-ref",
1540 ]
1541
1542
1543def test_process_queue_spawns_with_multiple_refs(monkeypatch):
1544 """Test that dequeued task has all coalesced refs."""
1545 mod = importlib.import_module("solstone.think.supervisor")
1546
1547 # Create task queue with pre-set state (queued task with multiple refs)
1548 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1549 mod._task_queue._running = {"indexer": {"ref": "running-ref", "thread": None}}
1550 mod._task_queue._queues = {
1551 "indexer": [
1552 {
1553 "refs": ["ref-A", "ref-B", "ref-C"],
1554 "cmd": ["journal", "indexer", "--rescan"],
1555 }
1556 ]
1557 }
1558
1559 spawned = _capture_thread_starts(monkeypatch, mod)
1560
1561 # Process queue
1562 mod._task_queue._process_next("indexer")
1563
1564 # Should spawn with all three refs
1565 assert len(spawned) == 1
1566 assert spawned[0][0] == ["ref-A", "ref-B", "ref-C"] # all refs passed
1567 assert spawned[0][1] == ["journal", "indexer", "--rescan"]
1568
1569
1570def test_stale_queue_detected_on_submit(monkeypatch):
1571 """Test that a dead task thread is detected and cleared on next submit."""
1572 import threading
1573
1574 mod = importlib.import_module("solstone.think.supervisor")
1575
1576 mod._task_queue = mod.TaskQueue(on_queue_change=None)
1577
1578 # Create a dead thread BEFORE monkeypatching Thread.start
1579 dead_thread = threading.Thread(target=lambda: None)
1580 dead_thread.start()
1581 dead_thread.join()
1582 assert not dead_thread.is_alive()
1583
1584 spawned = _capture_thread_starts(monkeypatch, mod)
1585
1586 mod._task_queue._running = {"indexer": {"ref": "stale-ref", "thread": dead_thread}}
1587 mod._task_queue._queues = {
1588 "indexer": [
1589 {"refs": ["queued-ref"], "cmd": ["journal", "indexer", "--rescan-full"]}
1590 ]
1591 }
1592
1593 # Submit a new indexer task — should detect stale state and start immediately
1594 msg = {
1595 "tract": "supervisor",
1596 "event": "request",
1597 "cmd": ["journal", "indexer", "--rescan-new"],
1598 "ref": "new-ref",
1599 }
1600 mod._handle_task_request(msg)
1601
1602 # Stale entry should have been cleared, new task started
1603 assert mod._task_queue._running["indexer"]["ref"] == "new-ref"
1604 assert len(spawned) == 1
1605
1606 # Old queued entries should still be in queue (stale clear only removes _running)
1607 assert len(mod._task_queue._queues["indexer"]) == 1
1608
1609
1610class _TaskProcessStub:
1611 def __init__(self):
1612 self.poll = MagicMock(return_value=None)
1613 self.pid = 12345
1614
1615
1616class _TaskManagedStub:
1617 def __init__(self, *, cmd, start_time=100.0):
1618 self.name = "task"
1619 self.cmd = cmd
1620 self.start_time = start_time
1621 self.process = _TaskProcessStub()
1622 self.ref = "ref-1"
1623 self.terminate = MagicMock()
1624 self.cleanup = MagicMock()
1625 self.is_running = MagicMock(return_value=True)
1626
1627
1628BENIGN_LLAMA_LOAD_LOG = (
1629 "2026-06-12T12:00:00+00:00 [llama-server:stderr] "
1630 "llama_model_loader: loading model tensors\n"
1631 "2026-06-12T12:00:02+00:00 [llama-server:stderr] "
1632 "common_init_from_params: setting dry_penalty_last_n to ctx_size = 16384\n"
1633)
1634
1635
1636def _local_artifacts(local_install, binary, gguf, mmproj, *, backend="vulkan"):
1637 return local_install.LocalArtifacts(
1638 backend=backend,
1639 backend_reason=f"test {backend}",
1640 binary_path=binary,
1641 lib_dir=None,
1642 gguf_path=gguf,
1643 mmproj_path=mmproj,
1644 )
1645
1646
1647def _cuda_local_artifacts(local_install, binary, lib_dir, gguf, mmproj):
1648 return local_install.LocalArtifacts(
1649 backend="cuda",
1650 backend_reason="test cuda",
1651 binary_path=binary,
1652 lib_dir=lib_dir,
1653 gguf_path=gguf,
1654 mmproj_path=mmproj,
1655 )
1656
1657
1658class _FakeReservation:
1659 def __init__(self, port: int = 2468):
1660 self.port = port
1661 self.released = False
1662 self.closed = False
1663
1664 def release_for_spawn(self) -> int:
1665 self.released = True
1666 self.closed = True
1667 return self.port
1668
1669 def close(self) -> None:
1670 self.closed = True
1671
1672
1673def _mlx_launch_plan(mod, runtime_dir: Path, *, model_id: str = "mlx-model"):
1674 return mod.LocalServerLaunchPlan(
1675 backend="mlx",
1676 desired_fingerprint_json='{"provider":"local"}',
1677 desired_fingerprint_sha256="fp-local",
1678 runtime_dir=runtime_dir,
1679 model_id=model_id,
1680 )
1681
1682
1683def _vulkan_launch_plan(
1684 mod,
1685 binary: Path,
1686 gguf: Path,
1687 mmproj: Path | None,
1688 *,
1689 vram_mib: int = 6390,
1690):
1691 from solstone.think.providers import local_server
1692
1693 tier = local_server.select_server_tier(vram_mib)
1694 return mod.LocalServerLaunchPlan(
1695 backend="vulkan",
1696 desired_fingerprint_json='{"provider":"local"}',
1697 desired_fingerprint_sha256="fp-local",
1698 binary_path=binary,
1699 model_path=gguf,
1700 mmproj_path=mmproj,
1701 gpu_index=1,
1702 gpu_name="NVIDIA GeForce GTX 1660 Ti",
1703 gpu_vram_mib=vram_mib,
1704 vram_before_mib=512,
1705 context_tokens=tier.context_tokens,
1706 parallel_slots=tier.parallel_slots,
1707 prompt_cache_mib=tier.prompt_cache_mib,
1708 visible_devices_env="GGML_VK_VISIBLE_DEVICES",
1709 visible_devices_value="1",
1710 env_updates={"GGML_VK_VISIBLE_DEVICES": "1"},
1711 backend_reason="test vulkan",
1712 )
1713
1714
1715def _cuda_launch_plan(
1716 mod,
1717 binary: Path,
1718 gguf: Path,
1719 mmproj: Path,
1720 lib_dir: Path,
1721 *,
1722 tiering_memory_mib: int | None = 20000,
1723 visible_device: str = "0",
1724):
1725 from solstone.think.providers import local_server
1726
1727 tier = local_server.select_server_tier(tiering_memory_mib or 0)
1728 existing_ld = os.environ.get("LD_LIBRARY_PATH", "")
1729 ld_library_path = f"{lib_dir}:{existing_ld}" if existing_ld else str(lib_dir)
1730 return mod.LocalServerLaunchPlan(
1731 backend="cuda",
1732 desired_fingerprint_json='{"provider":"local"}',
1733 desired_fingerprint_sha256="fp-local",
1734 binary_path=binary,
1735 model_path=gguf,
1736 mmproj_path=mmproj,
1737 lib_dir=lib_dir,
1738 gpu_index=int(visible_device),
1739 gpu_vram_mib=tiering_memory_mib,
1740 context_tokens=tier.context_tokens,
1741 parallel_slots=tier.parallel_slots,
1742 prompt_cache_mib=tier.prompt_cache_mib,
1743 visible_devices_env="CUDA_VISIBLE_DEVICES",
1744 visible_devices_value=visible_device,
1745 env_updates={
1746 "CUDA_VISIBLE_DEVICES": visible_device,
1747 "LD_LIBRARY_PATH": ld_library_path,
1748 },
1749 backend_reason="test cuda",
1750 )
1751
1752
1753def test_ensure_venv_bin_on_path_prepends_when_missing(monkeypatch):
1754 mod = importlib.import_module("solstone.think.supervisor")
1755 monkeypatch.setenv("PATH", "/usr/bin")
1756 monkeypatch.setattr(sys, "executable", "/fake/venv/bin/python3")
1757
1758 mod._ensure_venv_bin_on_path()
1759
1760 parts = os.environ["PATH"].split(os.pathsep)
1761 assert parts[0] == "/fake/venv/bin"
1762 assert "/usr/bin" in parts[1:]
1763
1764
1765def test_ensure_venv_bin_on_path_idempotent(monkeypatch):
1766 mod = importlib.import_module("solstone.think.supervisor")
1767 monkeypatch.setenv("PATH", "/usr/bin")
1768 monkeypatch.setattr(sys, "executable", "/fake/venv/bin/python3")
1769
1770 mod._ensure_venv_bin_on_path()
1771 mod._ensure_venv_bin_on_path()
1772
1773 parts = os.environ["PATH"].split(os.pathsep)
1774 assert parts.count("/fake/venv/bin") == 1
1775
1776
1777def test_taskqueue_set_cap_records_cap():
1778 mod = importlib.import_module("solstone.think.supervisor")
1779 queue = mod.TaskQueue(on_queue_change=None)
1780
1781 queue.set_cap("import", 1800)
1782
1783 assert queue._caps["import"] == 1800
1784
1785
1786def test_task_queue_history_records_completion(tmp_path, monkeypatch):
1787 mod = importlib.import_module("solstone.think.supervisor")
1788 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
1789 (tmp_path / "health").mkdir(parents=True, exist_ok=True)
1790
1791 queue = mod.TaskQueue(on_queue_change=None)
1792 callosum = MagicMock()
1793
1794 class FakeCallosum:
1795 def start(self, callback=None):
1796 return None
1797
1798 def emit(self, *args, **kwargs):
1799 return callosum.emit(*args, **kwargs)
1800
1801 def stop(self):
1802 return None
1803
1804 managed = MagicMock()
1805 managed.pid = 12345
1806 managed.wait.return_value = 0
1807 managed.cleanup = MagicMock()
1808
1809 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
1810 managed.cmd = cmd
1811 managed.ref = ref
1812 return managed
1813
1814 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
1815 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
1816
1817 queue._run_task(
1818 ["ref-1"],
1819 ["journal", "heartbeat"],
1820 "heartbeat",
1821 None,
1822 "heartbeat",
1823 )
1824
1825 assert list(queue._history) == [
1826 {
1827 "name": "heartbeat",
1828 "cmd": ["journal", "heartbeat"],
1829 "ref": "ref-1",
1830 "ended_at": queue._history[0]["ended_at"],
1831 "exit_status": "ok",
1832 "scheduler_name": "heartbeat",
1833 }
1834 ]
1835
1836
1837def test_scheduler_completion_updates_scheduler_json(tmp_path, monkeypatch):
1838 mod = importlib.import_module("solstone.think.supervisor")
1839 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
1840 health_dir = tmp_path / "health"
1841 health_dir.mkdir(parents=True, exist_ok=True)
1842 state_path = health_dir / "scheduler.json"
1843 state_path.write_text(
1844 '{"heartbeat": {"custom": "kept"}, "other": {"last_run": 1}}',
1845 encoding="utf-8",
1846 )
1847
1848 mod._record_scheduler_completion(
1849 "heartbeat",
1850 ended_at=123.0,
1851 exit_status="ok",
1852 ref="ref-1",
1853 cmd=["journal", "heartbeat"],
1854 )
1855
1856 data = json.loads(state_path.read_text(encoding="utf-8"))
1857 assert data["heartbeat"] == {
1858 "custom": "kept",
1859 "last_run": 123.0,
1860 "last_status": "ok",
1861 "last_ref": "ref-1",
1862 }
1863 assert data["other"] == {"last_run": 1}
1864
1865
1866def test_exit_status_for_code_maps_empty_sentinel():
1867 from solstone.think.supervisor import _exit_status_for_code
1868 from solstone.think.utils import EXIT_EMPTY
1869
1870 assert _exit_status_for_code(0) == "ok"
1871 assert _exit_status_for_code(EXIT_EMPTY) == "empty"
1872 assert _exit_status_for_code(1) == "error"
1873 assert _exit_status_for_code(75) == "error"
1874
1875
1876def test_run_task_completes_when_scheduler_writeback_fails(monkeypatch):
1877 mod = importlib.import_module("solstone.think.supervisor")
1878 queue = mod.TaskQueue(on_queue_change=None)
1879 callosum = MagicMock()
1880
1881 class FakeCallosum:
1882 def start(self, callback=None):
1883 return None
1884
1885 def emit(self, *args, **kwargs):
1886 return callosum.emit(*args, **kwargs)
1887
1888 def stop(self):
1889 return callosum.stop()
1890
1891 managed = MagicMock()
1892 managed.pid = 12345
1893 managed.wait.return_value = 0
1894 managed.cleanup = MagicMock()
1895
1896 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
1897 managed.cmd = cmd
1898 managed.ref = ref
1899 return managed
1900
1901 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
1902 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
1903 monkeypatch.setattr(
1904 mod,
1905 "_record_scheduler_completion",
1906 MagicMock(side_effect=OSError("disk full")),
1907 )
1908 process_next = MagicMock()
1909 monkeypatch.setattr(queue, "_process_next", process_next)
1910
1911 queue._run_task(
1912 ["ref-1"],
1913 ["journal", "heartbeat"],
1914 "heartbeat",
1915 None,
1916 "heartbeat",
1917 )
1918
1919 callosum.stop.assert_called_once()
1920 process_next.assert_called_once_with("heartbeat")
1921
1922
1923def test_run_task_records_attempt_and_outcome_on_spawn(monkeypatch):
1924 mod = importlib.import_module("solstone.think.supervisor")
1925 queue = mod.TaskQueue(on_queue_change=None)
1926 callosum = MagicMock()
1927 record_attempt = MagicMock()
1928 record_outcome = MagicMock(return_value=SimpleNamespace(entered_backoff=False))
1929
1930 class FakeCallosum:
1931 def start(self, callback=None):
1932 return None
1933
1934 def emit(self, *args, **kwargs):
1935 return callosum.emit(*args, **kwargs)
1936
1937 def stop(self):
1938 return callosum.stop()
1939
1940 managed = MagicMock()
1941 managed.pid = 12345
1942 managed.wait.return_value = 0
1943 managed.cleanup = MagicMock()
1944
1945 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
1946 managed.cmd = cmd
1947 managed.ref = ref
1948 return managed
1949
1950 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
1951 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
1952 monkeypatch.setattr(mod, "record_attempt", record_attempt)
1953 monkeypatch.setattr(mod, "record_outcome", record_outcome)
1954 monkeypatch.setattr(queue, "_process_next", MagicMock())
1955
1956 cmd = ["journal", "think", "-v", "--day", "20250101"]
1957 queue._run_task(["ref-1"], cmd, "daily", "20250101")
1958
1959 record_attempt.assert_called_once()
1960 assert record_attempt.call_args.args == (cmd, "20250101", "ref-1")
1961 assert isinstance(record_attempt.call_args.kwargs["started_at"], float)
1962 record_outcome.assert_called_once()
1963 assert record_outcome.call_args.args == (cmd, "20250101", "ref-1")
1964 assert record_outcome.call_args.kwargs["exit_status"] == "ok"
1965 assert isinstance(record_outcome.call_args.kwargs["ended_at"], float)
1966
1967
1968def test_run_task_spawn_failure_does_not_record_attempt_or_outcome(monkeypatch):
1969 mod = importlib.import_module("solstone.think.supervisor")
1970 queue = mod.TaskQueue(on_queue_change=None)
1971 callosum = MagicMock()
1972 record_attempt = MagicMock()
1973 record_outcome = MagicMock()
1974 process_next = MagicMock()
1975
1976 class FakeCallosum:
1977 def start(self, callback=None):
1978 return None
1979
1980 def emit(self, *args, **kwargs):
1981 return callosum.emit(*args, **kwargs)
1982
1983 def stop(self):
1984 return callosum.stop()
1985
1986 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
1987 raise RuntimeError("spawn failed")
1988
1989 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
1990 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
1991 monkeypatch.setattr(mod, "record_attempt", record_attempt)
1992 monkeypatch.setattr(mod, "record_outcome", record_outcome)
1993 monkeypatch.setattr(queue, "_process_next", process_next)
1994
1995 cmd = ["journal", "think", "-v", "--day", "20250101"]
1996 queue._run_task(["ref-1"], cmd, "daily", "20250101")
1997
1998 record_attempt.assert_not_called()
1999 record_outcome.assert_not_called()
2000 callosum.stop.assert_called_once()
2001 process_next.assert_called_once_with("daily")
2002
2003
2004def test_submit_coalesce_does_not_record_attempt(monkeypatch):
2005 mod = importlib.import_module("solstone.think.supervisor")
2006 queue = mod.TaskQueue(on_queue_change=None)
2007 monkeypatch.setattr(mod, "record_attempt", MagicMock())
2008 _capture_thread_starts(monkeypatch, mod)
2009 cmd = ["journal", "think", "-v", "--day", "20250101"]
2010
2011 queue.submit(cmd, ref="running", day="20250101")
2012 queue.submit(cmd, ref="queued", day="20250101")
2013 queue.submit(cmd, ref="coalesced", day="20250101")
2014
2015 mod.record_attempt.assert_not_called()
2016 assert queue._queues["daily"][0]["refs"] == ["queued", "coalesced"]
2017
2018
2019def test_handle_task_request_skip_does_not_record(monkeypatch):
2020 mod = importlib.import_module("solstone.think.supervisor")
2021 queue = mod.TaskQueue(on_queue_change=None)
2022 managed = _TaskManagedStub(
2023 cmd=["journal", "importer", "--sync", "plaud"], start_time=100.0
2024 )
2025 queue._active["active-ref"] = managed
2026 queue.set_cap("importer", 50)
2027
2028 monkeypatch.setattr(mod, "_task_queue", queue)
2029 monkeypatch.setattr(mod, "_supervisor_callosum", MagicMock())
2030 monkeypatch.setattr(mod, "record_attempt", MagicMock())
2031 monkeypatch.setattr(mod.time, "time", lambda: 150.0)
2032
2033 mod._handle_task_request(
2034 {
2035 "tract": "supervisor",
2036 "event": "request",
2037 "cmd": ["journal", "importer", "--sync", "plaud"],
2038 "ref": "requested-ref",
2039 }
2040 )
2041
2042 mod.record_attempt.assert_not_called()
2043
2044
2045def test_run_task_emits_backoff_notification_once(monkeypatch):
2046 mod = importlib.import_module("solstone.think.supervisor")
2047 queue = mod.TaskQueue(on_queue_change=None)
2048 callosum = MagicMock()
2049
2050 class FakeCallosum:
2051 def start(self, callback=None):
2052 return None
2053
2054 def emit(self, *args, **kwargs):
2055 return callosum.emit(*args, **kwargs)
2056
2057 def stop(self):
2058 return callosum.stop()
2059
2060 managed = MagicMock()
2061 managed.pid = 12345
2062 managed.wait.return_value = 0
2063 managed.cleanup = MagicMock()
2064
2065 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
2066 managed.cmd = cmd
2067 managed.ref = ref
2068 return managed
2069
2070 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
2071 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
2072 monkeypatch.setattr(mod, "record_attempt", MagicMock())
2073 record_outcome = MagicMock(
2074 return_value=SimpleNamespace(
2075 entered_backoff=True,
2076 day="20250101",
2077 attempts=3,
2078 consecutive_non_completion=3,
2079 last_outcome="timeout",
2080 )
2081 )
2082 monkeypatch.setattr(mod, "record_outcome", record_outcome)
2083 monkeypatch.setattr(queue, "_process_next", MagicMock())
2084 cmd = ["journal", "think", "-v", "--day", "20250101"]
2085
2086 queue._run_task(["ref-1"], cmd, "daily", "20250101")
2087
2088 emitted = [call_args.args[:2] for call_args in callosum.emit.call_args_list]
2089 assert ("storage", "warning") in emitted
2090 assert ("notification", "show") in emitted
2091
2092 callosum.emit.reset_mock()
2093 record_outcome.return_value = SimpleNamespace(entered_backoff=False)
2094 queue._run_task(["ref-2"], cmd, "daily", "20250101")
2095
2096 emitted = [call_args.args[:2] for call_args in callosum.emit.call_args_list]
2097 assert ("storage", "warning") not in emitted
2098 assert ("notification", "show") not in emitted
2099
2100
2101def test_run_task_nudges_after_uncompleted_daily_only(monkeypatch):
2102 mod = importlib.import_module("solstone.think.supervisor")
2103 queue = mod.TaskQueue(on_queue_change=None)
2104 callosum = MagicMock()
2105 nudge = MagicMock()
2106
2107 class FakeCallosum:
2108 def start(self, callback=None):
2109 return None
2110
2111 def emit(self, *args, **kwargs):
2112 return callosum.emit(*args, **kwargs)
2113
2114 def stop(self):
2115 return callosum.stop()
2116
2117 managed = MagicMock()
2118 managed.pid = 12345
2119 managed.wait.return_value = 0
2120 managed.cleanup = MagicMock()
2121
2122 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
2123 managed.cmd = cmd
2124 managed.ref = ref
2125 return managed
2126
2127 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
2128 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
2129 monkeypatch.setattr(mod, "record_attempt", MagicMock())
2130 record_outcome = MagicMock()
2131 monkeypatch.setattr(mod, "record_outcome", record_outcome)
2132 monkeypatch.setattr(mod, "_nudge_catchup_drain", nudge)
2133 monkeypatch.setattr(queue, "_process_next", MagicMock())
2134
2135 daily_cmd = ["journal", "think", "-v", "--day", "20250101"]
2136 record_outcome.return_value = SimpleNamespace(
2137 recorded=True,
2138 command_kind=mod.KIND_DAILY_CATCHUP,
2139 completed=False,
2140 entered_backoff=False,
2141 )
2142 queue._cap_terminated.add("ref-timeout")
2143 queue._run_task(["ref-timeout"], daily_cmd, "daily", "20250101")
2144
2145 assert record_outcome.call_args.kwargs["exit_status"] == "timeout"
2146 nudge.assert_called_once_with(exclude_today=True)
2147
2148 nudge.reset_mock()
2149 record_outcome.return_value = SimpleNamespace(
2150 recorded=True,
2151 command_kind=mod.KIND_DAILY_CATCHUP,
2152 completed=True,
2153 entered_backoff=False,
2154 )
2155 queue._run_task(["ref-complete"], daily_cmd, "daily", "20250101")
2156 nudge.assert_not_called()
2157
2158 nudge.reset_mock()
2159 record_outcome.return_value = SimpleNamespace(
2160 recorded=True,
2161 command_kind="segment",
2162 completed=False,
2163 entered_backoff=False,
2164 )
2165 segment_cmd = [
2166 "journal",
2167 "think",
2168 "-v",
2169 "--day",
2170 "20250101",
2171 "--segment",
2172 "120000_300",
2173 ]
2174 queue._run_task(["ref-segment"], segment_cmd, "segment", "20250101")
2175 nudge.assert_not_called()
2176
2177
2178def test_handle_supervisor_drain_routes_exclude_today(monkeypatch):
2179 mod = importlib.import_module("solstone.think.supervisor")
2180 calls = []
2181
2182 class FakeDateTime:
2183 @staticmethod
2184 def now():
2185 return SimpleNamespace(date=lambda: date(2025, 1, 2))
2186
2187 def fake_drain(*args, **kwargs):
2188 calls.append((args, kwargs))
2189
2190 monkeypatch.setattr(mod, "datetime", FakeDateTime)
2191 monkeypatch.setattr(mod, "run_catchup_drain", fake_drain)
2192
2193 mod._handle_supervisor_drain(
2194 {
2195 "tract": "supervisor",
2196 "event": "drain",
2197 "day": "20250101",
2198 "exclude_today": True,
2199 }
2200 )
2201 assert calls == [((), {"force_days": {"20250101"}})]
2202
2203 calls.clear()
2204 mod._handle_supervisor_drain(
2205 {"tract": "supervisor", "event": "drain", "exclude_today": True}
2206 )
2207 assert calls == [((), {"exclude": {"20250102"}})]
2208
2209 calls.clear()
2210 mod._handle_supervisor_drain({"tract": "supervisor", "event": "drain"})
2211 assert calls == [((), {})]
2212
2213
2214def test_startup_catchup_drain_reconciles_before_drain(monkeypatch):
2215 mod = importlib.import_module("solstone.think.supervisor")
2216 order = []
2217
2218 def reconcile():
2219 order.append("reconcile")
2220 return []
2221
2222 def drain():
2223 order.append("drain")
2224
2225 monkeypatch.setattr(mod, "reconcile_interrupted_attempts", reconcile)
2226 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2227
2228 mod._startup_catchup_drain()
2229
2230 assert order == ["reconcile", "drain"]
2231
2232
2233def test_run_catchup_drain_no_engine_submits_nothing(monkeypatch):
2234 mod = importlib.import_module("solstone.think.supervisor")
2235 capture = _CaptureTaskQueue()
2236 monkeypatch.setattr(mod, "_task_queue", capture)
2237 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: True)
2238 monkeypatch.setattr(
2239 mod,
2240 "updated_days",
2241 lambda **_kwargs: pytest.fail("drain should not enumerate days"),
2242 )
2243
2244 assert mod.run_catchup_drain() == []
2245 assert capture.submissions == []
2246
2247
2248def test_run_gate_tick_deferred_open_runs_catchup_drain(monkeypatch):
2249 mod = importlib.import_module("solstone.think.supervisor")
2250 drain = MagicMock()
2251 monkeypatch.setattr(
2252 mod,
2253 "load_processing_settings",
2254 lambda: _supervisor_processing_settings("deferred"),
2255 )
2256 monkeypatch.setattr(
2257 mod,
2258 "evaluate_drain_gate",
2259 lambda settings, now, reading: SimpleNamespace(open=True),
2260 )
2261 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2262 mod._last_gate_tick = 0.0
2263
2264 mod._run_gate_tick(60.0)
2265
2266 drain.assert_called_once_with()
2267
2268
2269def test_run_gate_tick_deferred_closed_skips_catchup_drain(monkeypatch):
2270 mod = importlib.import_module("solstone.think.supervisor")
2271 drain = MagicMock()
2272 monkeypatch.setattr(
2273 mod,
2274 "load_processing_settings",
2275 lambda: _supervisor_processing_settings("deferred"),
2276 )
2277 monkeypatch.setattr(
2278 mod,
2279 "evaluate_drain_gate",
2280 lambda settings, now, reading: SimpleNamespace(open=False),
2281 )
2282 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2283 mod._last_gate_tick = 0.0
2284
2285 mod._run_gate_tick(60.0)
2286
2287 drain.assert_not_called()
2288
2289
2290def _write_catchup_state(journal: Path, records: dict[str, dict]) -> None:
2291 """Write catchup-state.json entries keyed '<day>:<kind>'."""
2292 health = journal / "health"
2293 health.mkdir(parents=True, exist_ok=True)
2294 (health / "catchup-state.json").write_text(
2295 json.dumps({"version": 1, "entries": records}), encoding="utf-8"
2296 )
2297
2298
2299def _catchup_record(day: str, kind: str, **overrides) -> dict:
2300 record = {
2301 "day": day,
2302 "command_kind": kind,
2303 "attempts": 1,
2304 "consecutive_non_completion": 1,
2305 "last_attempt_at": 0,
2306 "last_outcome": "timeout",
2307 "next_retry_at": 0,
2308 "entered_backoff_at": None,
2309 "notified_at": None,
2310 "fingerprint": "fp",
2311 "active": None,
2312 "reason_code": None,
2313 "timeout_seconds": None,
2314 "bounded": None,
2315 }
2316 record.update(overrides)
2317 return record
2318
2319
2320def _arm_catchup_retry_tick(mod, monkeypatch, *, mode="realtime", remote=False):
2321 """Put the tick past its cadence gate with a seeded watermark."""
2322 monkeypatch.setattr(
2323 mod, "load_processing_settings", lambda: _supervisor_processing_settings(mode)
2324 )
2325 monkeypatch.setattr(mod, "_is_remote_mode", remote)
2326 mod._last_catchup_retry_tick = 0.0
2327
2328
2329def test_read_catchup_retry_expiries_uses_later_of_both_kinds(tmp_path, monkeypatch):
2330 mod = importlib.import_module("solstone.think.supervisor")
2331 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2332 _write_catchup_state(
2333 tmp_path,
2334 {
2335 "20260101:daily-catchup": _catchup_record(
2336 "20260101", "daily-catchup", next_retry_at=100.0
2337 ),
2338 "20260101:segment-repair": _catchup_record(
2339 "20260101", "segment-repair", next_retry_at=250.0
2340 ),
2341 },
2342 )
2343
2344 assert mod._read_catchup_retry_expiries() == [250.0]
2345
2346
2347def test_read_catchup_retry_expiries_skips_active_and_unretried(tmp_path, monkeypatch):
2348 mod = importlib.import_module("solstone.think.supervisor")
2349 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2350 _write_catchup_state(
2351 tmp_path,
2352 {
2353 # Active on one kind: cannot become eligible by expiry alone.
2354 "20260101:daily-catchup": _catchup_record(
2355 "20260101", "daily-catchup", next_retry_at=100.0, active="pid-1"
2356 ),
2357 "20260101:segment-repair": _catchup_record(
2358 "20260101", "segment-repair", next_retry_at=250.0
2359 ),
2360 # No backoff: already eligible, so no future crossing to detect.
2361 "20260102:daily-catchup": _catchup_record("20260102", "daily-catchup"),
2362 # KIND_SEGMENT never carries a meaningful retry and is not gated on.
2363 "20260103:segment": _catchup_record(
2364 "20260103", "segment", next_retry_at=400.0
2365 ),
2366 },
2367 )
2368
2369 assert mod._read_catchup_retry_expiries() == []
2370
2371
2372def test_catchup_retry_tick_seeds_watermark_without_draining(tmp_path, monkeypatch):
2373 mod = importlib.import_module("solstone.think.supervisor")
2374 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2375 drain = MagicMock()
2376 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2377 _arm_catchup_retry_tick(mod, monkeypatch)
2378 mod._catchup_retry_watermark = 0.0
2379
2380 now = time.time()
2381 _write_catchup_state(
2382 tmp_path,
2383 {
2384 "20260101:daily-catchup": _catchup_record(
2385 "20260101", "daily-catchup", next_retry_at=now - 10
2386 )
2387 },
2388 )
2389
2390 mod._run_catchup_retry_tick(now)
2391
2392 # _startup_catchup_drain() already covered days expired before boot.
2393 drain.assert_not_called()
2394 assert mod._catchup_retry_watermark == now
2395
2396
2397def test_catchup_retry_tick_drains_excluding_today(tmp_path, monkeypatch):
2398 mod = importlib.import_module("solstone.think.supervisor")
2399 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2400 drain = MagicMock()
2401 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2402 _arm_catchup_retry_tick(mod, monkeypatch)
2403
2404 now = time.time()
2405 mod._catchup_retry_watermark = now - 60
2406 _write_catchup_state(
2407 tmp_path,
2408 {
2409 "20260101:daily-catchup": _catchup_record(
2410 "20260101", "daily-catchup", next_retry_at=now - 10
2411 )
2412 },
2413 )
2414
2415 mod._run_catchup_retry_tick(now)
2416
2417 today_str = date.today().strftime("%Y%m%d")
2418 drain.assert_called_once_with(exclude={today_str})
2419
2420
2421def test_catchup_retry_tick_fires_once_per_expiry(tmp_path, monkeypatch):
2422 """A day that re-enters backoff waits for its new expiry, not the next tick."""
2423 mod = importlib.import_module("solstone.think.supervisor")
2424 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2425 drain = MagicMock()
2426 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2427 _arm_catchup_retry_tick(mod, monkeypatch)
2428
2429 now = time.time()
2430 mod._catchup_retry_watermark = now - 60
2431 _write_catchup_state(
2432 tmp_path,
2433 {
2434 "20260101:daily-catchup": _catchup_record(
2435 "20260101", "daily-catchup", next_retry_at=now - 10
2436 )
2437 },
2438 )
2439
2440 mod._run_catchup_retry_tick(now)
2441 mod._last_catchup_retry_tick = 0.0
2442 mod._run_catchup_retry_tick(now + 60)
2443
2444 assert drain.call_count == 1
2445
2446
2447@pytest.mark.parametrize(
2448 "mode,remote", [("deferred", False), ("realtime", True), ("deferred", True)]
2449)
2450def test_catchup_retry_tick_skips_deferred_and_remote(
2451 tmp_path, monkeypatch, mode, remote
2452):
2453 mod = importlib.import_module("solstone.think.supervisor")
2454 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2455 drain = MagicMock()
2456 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2457 _arm_catchup_retry_tick(mod, monkeypatch, mode=mode, remote=remote)
2458
2459 now = time.time()
2460 mod._catchup_retry_watermark = now - 60
2461 _write_catchup_state(
2462 tmp_path,
2463 {
2464 "20260101:daily-catchup": _catchup_record(
2465 "20260101", "daily-catchup", next_retry_at=now - 10
2466 )
2467 },
2468 )
2469
2470 mod._run_catchup_retry_tick(now)
2471
2472 drain.assert_not_called()
2473
2474
2475def test_catchup_retry_tick_without_expiry_does_not_fingerprint(tmp_path, monkeypatch):
2476 mod = importlib.import_module("solstone.think.supervisor")
2477 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2478 drain = MagicMock()
2479 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2480 monkeypatch.setattr(
2481 "solstone.think.catchup_state.read_raw_input_fingerprint",
2482 lambda day: pytest.fail("expiry gate must not hash raw input"),
2483 )
2484 _arm_catchup_retry_tick(mod, monkeypatch)
2485
2486 now = time.time()
2487 mod._catchup_retry_watermark = now - 60
2488 _write_catchup_state(
2489 tmp_path,
2490 {
2491 "20260101:daily-catchup": _catchup_record(
2492 "20260101", "daily-catchup", next_retry_at=now + 3600
2493 )
2494 },
2495 )
2496
2497 mod._run_catchup_retry_tick(now)
2498
2499 drain.assert_not_called()
2500 assert mod._catchup_retry_watermark == now
2501
2502
2503def test_catchup_retry_tick_submits_expired_day_through_drain(tmp_path, monkeypatch):
2504 """End to end: the real drain submits the expired day and skips the backed-off one."""
2505 mod = importlib.import_module("solstone.think.supervisor")
2506 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2507 capture = _CaptureTaskQueue()
2508 monkeypatch.setattr(mod, "_task_queue", capture)
2509 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: False)
2510 monkeypatch.setattr(mod, "updated_days", lambda **_kw: ["20260101", "20260102"])
2511 monkeypatch.setattr(
2512 "solstone.think.catchup_state.read_raw_input_fingerprint", lambda day: "fp"
2513 )
2514 _arm_catchup_retry_tick(mod, monkeypatch)
2515
2516 now = time.time()
2517 mod._catchup_retry_watermark = now - 60
2518 _write_catchup_state(
2519 tmp_path,
2520 {
2521 "20260101:daily-catchup": _catchup_record(
2522 "20260101", "daily-catchup", next_retry_at=now - 10
2523 ),
2524 # Still inside its backoff window, fingerprint unchanged.
2525 "20260102:daily-catchup": _catchup_record(
2526 "20260102", "daily-catchup", next_retry_at=now + 3600
2527 ),
2528 },
2529 )
2530
2531 mod._run_catchup_retry_tick(now)
2532
2533 assert capture.submissions == [
2534 {"cmd": ["journal", "think", "-v", "--day", "20260101"], "day": "20260101"}
2535 ]
2536
2537
2538def test_catchup_retry_tick_respects_drain_engine_gate(tmp_path, monkeypatch):
2539 """The re-fire routes through run_catchup_drain, so its gates still apply."""
2540 mod = importlib.import_module("solstone.think.supervisor")
2541 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2542 capture = _CaptureTaskQueue()
2543 monkeypatch.setattr(mod, "_task_queue", capture)
2544 monkeypatch.setattr(mod, "no_thinking_engine_chosen", lambda: True)
2545 monkeypatch.setattr(
2546 mod,
2547 "updated_days",
2548 lambda **_kw: pytest.fail("drain should not enumerate days"),
2549 )
2550 _arm_catchup_retry_tick(mod, monkeypatch)
2551
2552 now = time.time()
2553 mod._catchup_retry_watermark = now - 60
2554 _write_catchup_state(
2555 tmp_path,
2556 {
2557 "20260101:daily-catchup": _catchup_record(
2558 "20260101", "daily-catchup", next_retry_at=now - 10
2559 )
2560 },
2561 )
2562
2563 mod._run_catchup_retry_tick(now)
2564
2565 assert capture.submissions == []
2566
2567
2568def test_catchup_retry_tick_throttles_to_cadence(tmp_path, monkeypatch):
2569 mod = importlib.import_module("solstone.think.supervisor")
2570 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2571 settings = MagicMock(
2572 side_effect=AssertionError("cadence gate should short-circuit")
2573 )
2574 monkeypatch.setattr(mod, "load_processing_settings", settings)
2575 mod._last_catchup_retry_tick = 100.0
2576
2577 mod._run_catchup_retry_tick(100.0 + mod.CATCHUP_RETRY_TICK_INTERVAL_S - 1)
2578
2579 settings.assert_not_called()
2580
2581
2582def test_run_gate_tick_realtime_skips_catchup_drain(monkeypatch):
2583 mod = importlib.import_module("solstone.think.supervisor")
2584 drain = MagicMock()
2585 evaluate = MagicMock(return_value=SimpleNamespace(open=True))
2586 monkeypatch.setattr(
2587 mod,
2588 "load_processing_settings",
2589 lambda: _supervisor_processing_settings("realtime"),
2590 )
2591 monkeypatch.setattr(mod, "evaluate_drain_gate", evaluate)
2592 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2593 mod._last_gate_tick = 0.0
2594
2595 mod._run_gate_tick(60.0)
2596
2597 evaluate.assert_not_called()
2598 drain.assert_not_called()
2599
2600
2601def test_run_gate_tick_throttles_catchup_drain(monkeypatch):
2602 mod = importlib.import_module("solstone.think.supervisor")
2603 drain = MagicMock()
2604 monkeypatch.setattr(
2605 mod,
2606 "load_processing_settings",
2607 lambda: _supervisor_processing_settings("deferred"),
2608 )
2609 monkeypatch.setattr(
2610 mod,
2611 "evaluate_drain_gate",
2612 lambda settings, now, reading: SimpleNamespace(open=True),
2613 )
2614 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2615 mod._last_gate_tick = 0.0
2616
2617 mod._run_gate_tick(60.0)
2618 mod._run_gate_tick(119.0)
2619 mod._run_gate_tick(121.0)
2620
2621 assert drain.call_count == 2
2622
2623
2624def test_run_gate_tick_disabled_display_powersave_does_not_poll(monkeypatch):
2625 mod = importlib.import_module("solstone.think.supervisor")
2626 drain = MagicMock()
2627 poll = MagicMock(side_effect=AssertionError("poll should not be called"))
2628
2629 def evaluate(settings, now, reading):
2630 assert reading == DISPLAY_POWERSAVE_UNAVAILABLE
2631 return SimpleNamespace(open=True)
2632
2633 monkeypatch.setattr(mod, "_is_remote_mode", False)
2634 monkeypatch.setattr(
2635 mod,
2636 "load_processing_settings",
2637 lambda: _supervisor_processing_settings("deferred"),
2638 )
2639 monkeypatch.setattr(mod, "poll_display_powersave", poll)
2640 monkeypatch.setattr(mod, "evaluate_drain_gate", evaluate)
2641 monkeypatch.setattr(mod, "run_catchup_drain", drain)
2642 mod._last_gate_tick = 0.0
2643
2644 mod._run_gate_tick(60.0)
2645
2646 poll.assert_not_called()
2647 drain.assert_called_once_with()
2648
2649
2650def test_run_gate_tick_enabled_display_powersave_polls(monkeypatch):
2651 mod = importlib.import_module("solstone.think.supervisor")
2652 reading = DisplayPowersaveReading(available=True, asleep=True, debounced=True)
2653 poll = MagicMock(return_value=reading)
2654 captured = {}
2655
2656 def evaluate(settings, now, display_reading):
2657 captured["reading"] = display_reading
2658 return SimpleNamespace(open=False)
2659
2660 monkeypatch.setattr(mod, "_is_remote_mode", False)
2661 monkeypatch.setattr(
2662 mod,
2663 "load_processing_settings",
2664 lambda: _supervisor_processing_settings(
2665 "deferred",
2666 display_powersave_enabled=True,
2667 ),
2668 )
2669 monkeypatch.setattr(mod, "poll_display_powersave", poll)
2670 monkeypatch.setattr(mod, "evaluate_drain_gate", evaluate)
2671 monkeypatch.setattr(mod, "run_catchup_drain", MagicMock())
2672 mod._last_gate_tick = 0.0
2673
2674 mod._run_gate_tick(60.0)
2675
2676 poll.assert_called_once()
2677 assert isinstance(poll.call_args.args[0], float)
2678 assert captured["reading"] == reading
2679
2680
2681def test_supervise_resets_display_powersave_monitor_on_entry(monkeypatch):
2682 mod = importlib.import_module("solstone.think.supervisor")
2683 reset = MagicMock()
2684 monkeypatch.setattr(mod, "reset_display_powersave_monitor", reset)
2685 monkeypatch.setattr(mod, "shutdown_requested", True)
2686
2687 asyncio.run(mod.supervise(daily=False, schedule=False, procs=[]))
2688
2689 reset.assert_called_once_with()
2690
2691
2692def test_supervise_logs_tick_step_failure_and_continues(caplog, monkeypatch):
2693 mod = importlib.import_module("solstone.think.supervisor")
2694 monkeypatch.setattr(mod, "shutdown_requested", False)
2695 monkeypatch.setattr(mod, "_task_queue", None)
2696 monkeypatch.setattr(mod, "_supervisor_callosum", None)
2697 monkeypatch.setattr(mod, "_last_tick_step_failure", None)
2698 monkeypatch.setattr(mod, "reset_display_powersave_monitor", lambda: None)
2699 monkeypatch.setattr(mod, "_run_sync_tick", lambda _now: True)
2700
2701 flush_calls = []
2702 monkeypatch.setattr(
2703 mod, "_check_segment_flush", lambda: flush_calls.append("flush")
2704 )
2705
2706 scheduler_calls = 0
2707
2708 def check_schedule():
2709 nonlocal scheduler_calls
2710 scheduler_calls += 1
2711 if scheduler_calls == 1:
2712 raise Exception("schedule boom")
2713
2714 monkeypatch.setattr(mod.scheduler, "check", check_schedule)
2715
2716 async def stop_after_two_ticks(_seconds):
2717 if scheduler_calls >= 2:
2718 mod.shutdown_requested = True
2719
2720 monkeypatch.setattr(mod.asyncio, "sleep", stop_after_two_ticks)
2721
2722 with caplog.at_level(logging.DEBUG):
2723 asyncio.run(mod.supervise(daily=False, schedule=True, procs=[]))
2724
2725 errors = [
2726 record
2727 for record in caplog.records
2728 if record.levelno == logging.ERROR and "scheduler_check" in record.message
2729 ]
2730 assert len(errors) == 1
2731 assert errors[0].exc_info is not None
2732 assert scheduler_calls == 2
2733 assert len(flush_calls) == 2
2734
2735
2736def test_supervise_propagates_cancelled_error_from_guarded_step(monkeypatch):
2737 mod = importlib.import_module("solstone.think.supervisor")
2738 monkeypatch.setattr(mod, "shutdown_requested", False)
2739 monkeypatch.setattr(mod, "_task_queue", None)
2740 monkeypatch.setattr(mod, "_supervisor_callosum", None)
2741 monkeypatch.setattr(mod, "reset_display_powersave_monitor", lambda: None)
2742 monkeypatch.setattr(mod, "_check_segment_flush", lambda: None)
2743 monkeypatch.setattr(mod, "_run_sync_tick", lambda _now: True)
2744 monkeypatch.setattr(
2745 mod.scheduler,
2746 "check",
2747 lambda: (_ for _ in ()).throw(asyncio.CancelledError()),
2748 )
2749
2750 with pytest.raises(asyncio.CancelledError):
2751 asyncio.run(mod.supervise(daily=False, schedule=True, procs=[]))
2752
2753
2754def test_record_scheduler_completion_serializes_concurrent_writes(
2755 tmp_path, monkeypatch
2756):
2757 mod = importlib.import_module("solstone.think.supervisor")
2758 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
2759
2760 threads = [
2761 threading.Thread(
2762 target=mod._record_scheduler_completion,
2763 args=(name,),
2764 kwargs={
2765 "ended_at": ended_at,
2766 "exit_status": "ok",
2767 "ref": f"ref-{name}",
2768 "cmd": ["sol", name],
2769 },
2770 )
2771 for name, ended_at in [("first", 101.0), ("second", 202.0)]
2772 ]
2773
2774 for thread in threads:
2775 thread.start()
2776 for thread in threads:
2777 thread.join()
2778
2779 state_path = tmp_path / "health" / "scheduler.json"
2780 data = json.loads(state_path.read_text(encoding="utf-8"))
2781 assert data["first"]["last_run"] == 101.0
2782 assert data["second"]["last_run"] == 202.0
2783
2784
2785def test_task_history_records_cap_kill_as_timeout(monkeypatch):
2786 mod = importlib.import_module("solstone.think.supervisor")
2787 queue = mod.TaskQueue(on_queue_change=None)
2788 queue.set_cap("import", 50)
2789 callosum = MagicMock()
2790
2791 class FakeCallosum:
2792 def start(self, callback=None):
2793 return None
2794
2795 def emit(self, *args, **kwargs):
2796 return callosum.emit(*args, **kwargs)
2797
2798 def stop(self):
2799 return None
2800
2801 managed = MagicMock()
2802 managed.pid = 12345
2803 managed.cmd = ["sol", "import"]
2804 managed.ref = "ref-1"
2805 managed.start_time = 100.0
2806 managed.cleanup = MagicMock()
2807
2808 def wait():
2809 queue.enforce_deadlines(200.0)
2810 return -15
2811
2812 managed.wait.side_effect = wait
2813
2814 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
2815 return managed
2816
2817 monkeypatch.setattr(mod, "CallosumConnection", FakeCallosum)
2818 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
2819 monkeypatch.setattr(mod, "_start_termination_thread", MagicMock())
2820
2821 queue._run_task(["ref-1"], ["sol", "import"], "import")
2822
2823 assert queue._history[0]["exit_status"] == "timeout"
2824
2825
2826def test_handle_task_request_skips_still_running(monkeypatch):
2827 mod = importlib.import_module("solstone.think.supervisor")
2828 queue = mod.TaskQueue(on_queue_change=None)
2829 managed = _TaskManagedStub(
2830 cmd=["journal", "importer", "--sync", "plaud"], start_time=100.0
2831 )
2832 queue._active["active-ref"] = managed
2833 queue.set_cap("importer", 50)
2834 callosum = MagicMock()
2835
2836 monkeypatch.setattr(mod, "_task_queue", queue)
2837 monkeypatch.setattr(mod, "_supervisor_callosum", callosum)
2838 monkeypatch.setattr(mod.time, "time", lambda: 150.0)
2839
2840 mod._handle_task_request(
2841 {
2842 "tract": "supervisor",
2843 "event": "request",
2844 "cmd": ["journal", "importer", "--sync", "plaud"],
2845 "ref": "requested-ref",
2846 "scheduler_name": "sync-plaud",
2847 }
2848 )
2849
2850 callosum.emit.assert_called_once_with(
2851 "supervisor",
2852 "skipped",
2853 reason="still_running",
2854 ref="requested-ref",
2855 active_ref="active-ref",
2856 cmd=["journal", "importer", "--sync", "plaud"],
2857 scheduler_name="sync-plaud",
2858 )
2859 assert queue._queues == {}
2860
2861
2862def test_handle_task_request_skips_wedged(monkeypatch):
2863 mod = importlib.import_module("solstone.think.supervisor")
2864 queue = mod.TaskQueue(on_queue_change=None)
2865 managed = _TaskManagedStub(
2866 cmd=["journal", "importer", "--sync", "plaud"], start_time=100.0
2867 )
2868 queue._active["active-ref"] = managed
2869 queue.set_cap("importer", 50)
2870 callosum = MagicMock()
2871
2872 monkeypatch.setattr(mod, "_task_queue", queue)
2873 monkeypatch.setattr(mod, "_supervisor_callosum", callosum)
2874 monkeypatch.setattr(mod.time, "time", lambda: 201.0)
2875
2876 mod._handle_task_request(
2877 {
2878 "tract": "supervisor",
2879 "event": "request",
2880 "cmd": ["journal", "importer", "--sync", "plaud"],
2881 "ref": "requested-ref",
2882 }
2883 )
2884
2885 assert callosum.emit.call_args.kwargs["reason"] == "wedged"
2886 assert callosum.emit.call_args.kwargs["active_ref"] == "active-ref"
2887
2888
2889def test_task_queue_shutdown_terminates_active_tasks():
2890 mod = importlib.import_module("solstone.think.supervisor")
2891 queue = mod.TaskQueue(on_queue_change=None)
2892 first = _TaskManagedStub(cmd=["sol", "import"])
2893 second = _TaskManagedStub(cmd=["journal", "indexer"])
2894 queue._active = {"first": first, "second": second}
2895
2896 assert queue.shutdown() == 2
2897
2898 first.terminate.assert_called_once_with(timeout=10.0)
2899 second.terminate.assert_called_once_with(timeout=10.0)
2900
2901
2902def test_task_queue_shutdown_empty_is_noop():
2903 mod = importlib.import_module("solstone.think.supervisor")
2904 queue = mod.TaskQueue(on_queue_change=None)
2905
2906 assert queue.shutdown() == 0
2907
2908
2909def test_task_queue_shutdown_continues_after_timeout():
2910 mod = importlib.import_module("solstone.think.supervisor")
2911 queue = mod.TaskQueue(on_queue_change=None)
2912 first = _TaskManagedStub(cmd=["sol", "import"])
2913 second = _TaskManagedStub(cmd=["journal", "indexer"])
2914 first.terminate.side_effect = subprocess.TimeoutExpired(
2915 cmd=["sol", "import"], timeout=10
2916 )
2917 queue._active = {"first": first, "second": second}
2918
2919 assert queue.shutdown() == 2
2920
2921 first.terminate.assert_called_once_with(timeout=10.0)
2922 second.terminate.assert_called_once_with(timeout=10.0)
2923
2924
2925def test_enforce_deadlines_terminates_when_elapsed_exceeds_cap(caplog, monkeypatch):
2926 mod = importlib.import_module("solstone.think.supervisor")
2927 queue = mod.TaskQueue(on_queue_change=None)
2928 managed = _TaskManagedStub(
2929 cmd=["journal", "importer", "--sync", "plaud", "--save"],
2930 start_time=100.0,
2931 )
2932 queue._active["ref-1"] = managed
2933 queue.set_cap("importer", 50)
2934
2935 def terminate_now(key, managed_arg, timeout, reason):
2936 assert key == "ref-1"
2937 assert managed_arg is managed
2938 assert timeout == 2.0
2939 assert reason == "cap"
2940 managed_arg.terminate(timeout=timeout)
2941
2942 monkeypatch.setattr(mod, "_start_termination_thread", terminate_now)
2943 caplog.set_level("WARNING")
2944 queue.enforce_deadlines(200.0)
2945
2946 managed.terminate.assert_called_once_with(timeout=2.0)
2947 assert (
2948 "Task importer (cmd=journal importer --sync plaud --save, ref=ref-1) exceeded "
2949 "max_runtime of 50s (elapsed=100s); terminating"
2950 ) in caplog.text
2951
2952
2953def test_collect_task_status_reports_default_cap(monkeypatch):
2954 mod = importlib.import_module("solstone.think.supervisor")
2955 queue = mod.TaskQueue(on_queue_change=None)
2956 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
2957 queue._active["ref-1"] = managed
2958 monkeypatch.setattr(mod.time, "time", lambda: 112.0)
2959
2960 assert queue.collect_task_status() == [
2961 {
2962 "ref": "ref-1",
2963 "name": "providers",
2964 "duration_seconds": 12,
2965 "max_runtime_seconds": mod.DEFAULT_TASK_MAX_RUNTIME,
2966 "slow": False,
2967 "stuck": False,
2968 }
2969 ]
2970
2971
2972def test_collect_task_status_under_cap(monkeypatch):
2973 mod = importlib.import_module("solstone.think.supervisor")
2974 queue = mod.TaskQueue(on_queue_change=None)
2975 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
2976 queue._active["ref-1"] = managed
2977 queue.set_cap("providers", 300)
2978 monkeypatch.setattr(mod.time, "time", lambda: 112.0)
2979
2980 status = queue.collect_task_status()
2981
2982 assert status[0]["max_runtime_seconds"] == 300
2983 assert status[0]["slow"] is False
2984 assert status[0]["stuck"] is False
2985
2986
2987def test_collect_task_status_slow_under_cap(monkeypatch):
2988 mod = importlib.import_module("solstone.think.supervisor")
2989 queue = mod.TaskQueue(on_queue_change=None)
2990 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
2991 queue._active["ref-1"] = managed
2992 queue.set_cap("providers", 15)
2993 monkeypatch.setattr(mod.time, "time", lambda: 112.0)
2994
2995 status = queue.collect_task_status()
2996
2997 assert status[0]["max_runtime_seconds"] == 15
2998 assert status[0]["slow"] is True
2999 assert status[0]["stuck"] is False
3000
3001
3002def test_collect_task_status_over_cap(monkeypatch):
3003 mod = importlib.import_module("solstone.think.supervisor")
3004 queue = mod.TaskQueue(on_queue_change=None)
3005 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
3006 queue._active["ref-1"] = managed
3007 queue.set_cap("providers", 5)
3008 monkeypatch.setattr(mod.time, "time", lambda: 112.0)
3009
3010 status = queue.collect_task_status()
3011
3012 assert status[0]["max_runtime_seconds"] == 5
3013 assert status[0]["slow"] is True
3014 assert status[0]["stuck"] is True
3015
3016
3017def test_collect_task_status_default_cap_stuck(monkeypatch):
3018 mod = importlib.import_module("solstone.think.supervisor")
3019 queue = mod.TaskQueue(on_queue_change=None)
3020 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
3021 queue._active["ref-1"] = managed
3022 monkeypatch.setattr(
3023 mod.time, "time", lambda: 100.0 + mod.DEFAULT_TASK_MAX_RUNTIME + 5
3024 )
3025
3026 status = queue.collect_task_status()
3027
3028 assert status[0]["max_runtime_seconds"] == mod.DEFAULT_TASK_MAX_RUNTIME
3029 assert status[0]["stuck"] is True
3030
3031
3032def test_collect_task_status_snapshots_active_under_lock():
3033 mod = importlib.import_module("solstone.think.supervisor")
3034 queue = mod.TaskQueue(on_queue_change=None)
3035
3036 for index in range(25):
3037 managed = _TaskManagedStub(cmd=["journal", "providers"], start_time=100.0)
3038 managed.is_running = MagicMock(side_effect=lambda: time.sleep(0.0001) or True)
3039 queue._active[f"ref-{index}"] = managed
3040
3041 stop = threading.Event()
3042 thread_errors = []
3043
3044 def mutate_active():
3045 index = 0
3046 try:
3047 while not stop.is_set():
3048 ref = f"bg-{index}"
3049 managed = _TaskManagedStub(cmd=["journal", "providers"])
3050 with queue._lock:
3051 queue._active[ref] = managed
3052 time.sleep(0)
3053 with queue._lock:
3054 queue._active.pop(ref, None)
3055 index += 1
3056 except BaseException as exc:
3057 thread_errors.append(exc)
3058
3059 thread = threading.Thread(target=mutate_active)
3060 thread.start()
3061 try:
3062 for _ in range(100):
3063 queue.collect_task_status()
3064 finally:
3065 stop.set()
3066 thread.join(timeout=2)
3067
3068 assert not thread.is_alive()
3069 assert thread_errors == []
3070
3071
3072def test_enforce_deadlines_terminates_stopped_task(caplog, monkeypatch):
3073 mod = importlib.import_module("solstone.think.supervisor")
3074
3075 class StoppedProcess:
3076 def __init__(self, pid):
3077 self.pid = pid
3078
3079 def status(self):
3080 return mod.psutil.STATUS_STOPPED
3081
3082 monkeypatch.setattr(mod.psutil, "Process", StoppedProcess)
3083 terminate = MagicMock()
3084 monkeypatch.setattr(mod, "_start_termination_thread", terminate)
3085 queue = mod.TaskQueue(on_queue_change=None)
3086 managed = _TaskManagedStub(cmd=["sol", "import"], start_time=100.0)
3087 queue.set_cap("import", 300)
3088 queue._active["ref-1"] = managed
3089 caplog.set_level(logging.WARNING)
3090
3091 queue.enforce_deadlines(110.0)
3092 terminate.assert_not_called()
3093
3094 queue.enforce_deadlines(110.0)
3095
3096 terminate.assert_called_once_with("ref-1", managed, timeout=2.0, reason="stopped")
3097 assert "stopped" in caplog.text
3098
3099
3100def test_enforce_deadlines_does_not_probe_status_under_lock(monkeypatch):
3101 mod = importlib.import_module("solstone.think.supervisor")
3102 queue = mod.TaskQueue(on_queue_change=None)
3103 managed = _TaskManagedStub(cmd=["sol", "import"], start_time=100.0)
3104 managed.process.pid = 123
3105 queue._active["ref-1"] = managed
3106 queue.set_cap("import", 300)
3107
3108 class ProbeProcess:
3109 def __init__(self, pid):
3110 assert pid == 123
3111
3112 def status(self):
3113 acquired = queue._lock.acquire(blocking=False)
3114 assert acquired
3115 queue._lock.release()
3116 return mod.psutil.STATUS_RUNNING
3117
3118 monkeypatch.setattr(mod.psutil, "Process", ProbeProcess)
3119 monkeypatch.setattr(mod, "_start_termination_thread", MagicMock())
3120
3121 queue.enforce_deadlines(110.0)
3122
3123
3124def test_enforce_deadlines_skips_stopped_probe_for_new_cap_kill(monkeypatch):
3125 mod = importlib.import_module("solstone.think.supervisor")
3126 queue = mod.TaskQueue(on_queue_change=None)
3127 managed = _TaskManagedStub(cmd=["sol", "import"], start_time=100.0)
3128 queue._active["ref-1"] = managed
3129 queue.set_cap("import", 50)
3130 probe = MagicMock()
3131 terminate = MagicMock()
3132 monkeypatch.setattr(mod.psutil, "Process", probe)
3133 monkeypatch.setattr(mod, "_start_termination_thread", terminate)
3134
3135 queue.enforce_deadlines(200.0)
3136
3137 probe.assert_not_called()
3138 terminate.assert_called_once_with("ref-1", managed, timeout=2.0, reason="cap")
3139
3140
3141def test_terminate_managed_logs_timeout(caplog):
3142 mod = importlib.import_module("solstone.think.supervisor")
3143 managed = _TaskManagedStub(cmd=["sol", "import"], start_time=100.0)
3144 managed.terminate.side_effect = subprocess.TimeoutExpired(
3145 cmd=managed.cmd, timeout=3
3146 )
3147
3148 caplog.set_level("WARNING")
3149 mod._terminate_managed(managed, 3, reason="test")
3150
3151 managed.terminate.assert_called_once_with(timeout=3)
3152 assert "task did not terminate within 3.0s for test" in caplog.text
3153
3154
3155def test_enforce_deadlines_terminates_uncapped_at_default(monkeypatch):
3156 mod = importlib.import_module("solstone.think.supervisor")
3157 queue = mod.TaskQueue(on_queue_change=None)
3158 managed = _TaskManagedStub(cmd=["sol", "import"], start_time=100.0)
3159 queue._active["ref-1"] = managed
3160
3161 def terminate_now(key, managed_arg, timeout, reason):
3162 assert key == "ref-1"
3163 assert managed_arg is managed
3164 assert timeout == 2.0
3165 assert reason == "cap"
3166 managed_arg.terminate(timeout=timeout)
3167
3168 monkeypatch.setattr(mod, "_start_termination_thread", terminate_now)
3169 queue.enforce_deadlines(100.0 + mod.DEFAULT_TASK_MAX_RUNTIME + 1)
3170
3171 managed.terminate.assert_called_once_with(timeout=2.0)
3172
3173
3174def test_restart_service_uses_single_termination_path(monkeypatch):
3175 mod = importlib.import_module("solstone.think.supervisor")
3176 managed = _TaskManagedStub(cmd=["journal", "sense"], start_time=100.0)
3177 managed.name = "sense"
3178 managed.ref = "ref-sense"
3179 mod._managed_procs = [managed]
3180 mod._SERVICE_STATE.clear()
3181 mod._SERVICE_STATE["sense"] = {
3182 "restart": False,
3183 "shutdown_timeout": 7,
3184 }
3185
3186 def terminate_now(key, managed_arg, timeout, reason):
3187 assert key == "sense"
3188 assert managed_arg is managed
3189 assert timeout == 7
3190 assert reason == "restart"
3191 managed_arg.terminate(timeout=timeout)
3192
3193 monkeypatch.setattr(mod, "_start_termination_thread", terminate_now)
3194
3195 assert mod._restart_service("sense") is True
3196 managed.terminate.assert_called_once_with(timeout=7)
3197 assert mod._SERVICE_STATE["sense"]["restart"] is True
3198
3199
3200def test_stop_process_uses_service_shutdown_timeout():
3201 mod = importlib.import_module("solstone.think.supervisor")
3202 managed = _TaskManagedStub(cmd=["journal", "spl"], start_time=100.0)
3203 managed.name = "spl"
3204 mod._SERVICE_STATE.clear()
3205 mod._SERVICE_STATE["spl"] = {
3206 "restart": True,
3207 "shutdown_timeout": 9,
3208 }
3209
3210 mod._stop_process(managed)
3211
3212 managed.terminate.assert_called_once_with(timeout=9)
3213 managed.cleanup.assert_called_once_with()
3214
3215
3216def test_start_local_server_launches_mlx_server_on_darwin(
3217 tmp_path, monkeypatch, capsys
3218):
3219 mod = importlib.import_module("solstone.think.supervisor")
3220 from solstone.think.providers import local_server, local_vulkan
3221
3222 monkeypatch.setattr(sys, "platform", "darwin")
3223 gpu_gate = MagicMock(side_effect=AssertionError("darwin must not probe Vulkan"))
3224 monkeypatch.setattr(local_vulkan, "detect_gpus", gpu_gate)
3225 mod._SERVICE_STATE.clear()
3226 runtime_dir = tmp_path / "gemma4" / "variant-1120"
3227 written_ports = []
3228 spawned = []
3229 spawned_envs = []
3230 managed = _TaskManagedStub(cmd=[])
3231 managed.name = "mlx-vlm-server"
3232 managed.process.returncode = None
3233
3234 plan = _mlx_launch_plan(
3235 mod,
3236 runtime_dir,
3237 model_id="gemma-4-26b-a4b-it-mlx-4bit",
3238 )
3239 monkeypatch.setattr(
3240 mod,
3241 "write_service_port",
3242 lambda service, port: written_ports.append((service, port)),
3243 )
3244 monkeypatch.setattr(local_server, "_probe_health", lambda port: ("ready", None))
3245
3246 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
3247 spawned.append(cmd)
3248 spawned_envs.append(env)
3249 managed.cmd = cmd
3250 managed.ref = ref
3251 return managed
3252
3253 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
3254
3255 result = mod.start_local_server(plan, _FakeReservation())
3256
3257 assert result.status == "ready"
3258 assert result.reason_code == "probe-ready"
3259 assert result.managed is managed
3260 assert written_ports == []
3261 assert spawned == [
3262 [
3263 str(Path(sys.executable).with_name("mlx-vlm-server")),
3264 "--host",
3265 "127.0.0.1",
3266 "--port",
3267 "2468",
3268 "--model",
3269 str(runtime_dir),
3270 ]
3271 ]
3272 assert "0.0.0.0" not in spawned[0]
3273 assert "--n-gpu-layers" not in spawned[0]
3274 assert "-c" not in spawned[0]
3275 assert "--device" not in spawned[0]
3276 assert "Vulkan0" not in spawned[0]
3277 assert spawned_envs == [None]
3278 assert "--draft-model" not in spawned[0]
3279 assert "--draft-kind" not in spawned[0]
3280 assert mod._SERVICE_STATE["mlx-vlm-server"]["restart"] is False
3281 assert mod.LOCAL_MODEL_WARMING_UP_COPY in capsys.readouterr().out
3282 gpu_gate.assert_not_called()
3283
3284
3285def test_start_local_server_skips_when_mlx_not_installed_on_darwin(monkeypatch):
3286 mod = importlib.import_module("solstone.think.supervisor")
3287 from solstone.think.providers import mlx_install
3288
3289 monkeypatch.setattr(sys, "platform", "darwin")
3290 monkeypatch.setattr(
3291 mlx_install,
3292 "target_fingerprint",
3293 lambda: {"provider": "local", "backend": "mlx"},
3294 )
3295 monkeypatch.setattr(
3296 mlx_install,
3297 "inspect_readiness",
3298 lambda: _mlx_readiness(model_installed=False),
3299 )
3300 launch = MagicMock()
3301 monkeypatch.setattr(mod, "_launch_process", launch)
3302
3303 observation = mod._observe_mlx_local_provider_truth()
3304
3305 assert observation.phase == "artifact-not-ready"
3306 assert observation.plan is None
3307 launch.assert_not_called()
3308
3309
3310def test_start_local_server_skips_when_mlx_memory_blocked_on_darwin(monkeypatch):
3311 mod = importlib.import_module("solstone.think.supervisor")
3312 from solstone.think.providers import mlx_install
3313
3314 monkeypatch.setattr(sys, "platform", "darwin")
3315 monkeypatch.setattr(
3316 mlx_install,
3317 "target_fingerprint",
3318 lambda: {"provider": "local", "backend": "mlx"},
3319 )
3320 monkeypatch.setattr(
3321 mlx_install,
3322 "inspect_readiness",
3323 lambda: _mlx_readiness(ram_sufficient=False),
3324 )
3325 launch = MagicMock()
3326 monkeypatch.setattr(mod, "_launch_process", launch)
3327
3328 observation = mod._observe_mlx_local_provider_truth()
3329
3330 assert observation.phase == "artifact-not-ready"
3331 assert observation.plan is None
3332 launch.assert_not_called()
3333
3334
3335@pytest.mark.parametrize(
3336 ("vram_mib", "expected_context_tokens", "expected_parallel", "expected_cache_ram"),
3337 [
3338 (6390, None, "1", "0"),
3339 (16000, 32768, "2", "2048"),
3340 ],
3341)
3342def test_start_local_server_launches_llama_server_key_and_cmd(
3343 tmp_path,
3344 monkeypatch,
3345 capsys,
3346 vram_mib: int,
3347 expected_context_tokens: int | None,
3348 expected_parallel: str,
3349 expected_cache_ram: str,
3350):
3351 mod = importlib.import_module("solstone.think.supervisor")
3352 from solstone.think.providers import local_install, local_server, local_vulkan
3353
3354 expected_context_tokens = (
3355 local_server.LOCAL_MIN_CONTEXT_TOKENS
3356 if expected_context_tokens is None
3357 else expected_context_tokens
3358 )
3359 expected_launched_context_tokens = expected_context_tokens * int(expected_parallel)
3360 mod._SERVICE_STATE.clear()
3361 binary = tmp_path / "llama-server"
3362 # ensure_artifacts_installed always resolves artifacts under the selected
3363 # model's directory; the spawn guard rejects anything else, so the stub must
3364 # return realistic in-model-dir paths.
3365 model_artifact_dir = local_install.model_dir(mod.LOCAL_MODEL)
3366 gguf = model_artifact_dir / "model.gguf"
3367 mmproj = model_artifact_dir / "mmproj.gguf"
3368 written_ports = []
3369 written_context_windows = []
3370 spawned = []
3371 spawned_envs = []
3372 managed = _TaskManagedStub(cmd=[])
3373 managed.name = "llama-server"
3374 managed.process.returncode = None
3375 log_path = tmp_path / "llama-server.log"
3376 log_path.write_text(BENIGN_LLAMA_LOAD_LOG, encoding="utf-8")
3377 managed.log_writer = type("LogWriter", (), {"path": log_path})()
3378
3379 plan = _vulkan_launch_plan(mod, binary, gguf, mmproj, vram_mib=vram_mib)
3380 monkeypatch.setattr(local_vulkan, "device_local_used_mib", lambda index: 512)
3381 monkeypatch.setattr(
3382 mod,
3383 "write_service_port",
3384 lambda service, port: written_ports.append((service, port)),
3385 )
3386 monkeypatch.setattr(
3387 local_server,
3388 "write_local_context_window",
3389 lambda tokens: written_context_windows.append(tokens),
3390 )
3391 monkeypatch.setattr(local_server, "_probe_health", lambda port: ("ready", None))
3392 monkeypatch.setattr(local_server, "fetch_props", lambda port: None)
3393
3394 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
3395 spawned.append(cmd)
3396 spawned_envs.append(env)
3397 managed.cmd = cmd
3398 managed.ref = ref
3399 return managed
3400
3401 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
3402
3403 result = mod.start_local_server(plan, _FakeReservation())
3404
3405 assert result.status == "ready"
3406 assert result.reason_code == "probe-ready"
3407 assert result.managed is managed
3408 assert written_ports == []
3409 assert written_context_windows == []
3410 assert spawned == [
3411 [
3412 str(binary),
3413 "-m",
3414 str(gguf),
3415 "--alias",
3416 mod.LOCAL_MODEL,
3417 "--host",
3418 "127.0.0.1",
3419 "--port",
3420 "2468",
3421 "--jinja",
3422 "--n-gpu-layers",
3423 "999",
3424 "-c",
3425 str(expected_launched_context_tokens),
3426 "--parallel",
3427 expected_parallel,
3428 "--kv-unified",
3429 "--cache-ram",
3430 expected_cache_ram,
3431 "--no-context-shift",
3432 "--device",
3433 "Vulkan0",
3434 "--mmproj",
3435 str(mmproj),
3436 ]
3437 ]
3438 assert spawned_envs[0]["GGML_VK_VISIBLE_DEVICES"] == "1"
3439 assert "0.0.0.0" not in spawned[0]
3440 assert mod._SERVICE_STATE["llama-server"]["restart"] is False
3441 assert mod.LOCAL_MODEL_WARMING_UP_COPY in capsys.readouterr().out
3442
3443
3444def test_log_context_assertion(caplog):
3445 mod = importlib.import_module("solstone.think.supervisor")
3446 from solstone.think.providers import local_server
3447
3448 def plan_for(tier):
3449 return mod.LocalServerLaunchPlan(
3450 backend="vulkan",
3451 desired_fingerprint_json='{"provider":"local"}',
3452 desired_fingerprint_sha256="fp-local",
3453 context_tokens=tier.context_tokens,
3454 parallel_slots=tier.parallel_slots,
3455 )
3456
3457 floor = plan_for(local_server._FLOOR_TIER)
3458 capable = plan_for(local_server._CAPABLE_TIER)
3459 capable_n_ctx = capable.context_tokens * capable.parallel_slots
3460
3461 with caplog.at_level(logging.INFO):
3462 mod._log_context_assertion(floor, 16384, 1)
3463 assert not any(record.levelno >= logging.WARNING for record in caplog.records)
3464
3465 caplog.clear()
3466 with caplog.at_level(logging.INFO):
3467 mod._log_context_assertion(capable, capable_n_ctx, 2)
3468 assert not any(record.levelno >= logging.WARNING for record in caplog.records)
3469
3470 caplog.clear()
3471 with caplog.at_level(logging.WARNING):
3472 mod._log_context_assertion(capable, capable_n_ctx, 1)
3473 assert any("context MISMATCH" in record.message for record in caplog.records)
3474 assert any("slots MISMATCH" in record.message for record in caplog.records)
3475
3476 caplog.clear()
3477 with caplog.at_level(logging.INFO):
3478 mod._log_context_assertion(capable, capable_n_ctx, None)
3479 assert not any(record.levelno >= logging.WARNING for record in caplog.records)
3480 assert any("context OK" in record.message for record in caplog.records)
3481 assert any("slot count not reported" in record.message for record in caplog.records)
3482
3483 caplog.clear()
3484 with caplog.at_level(logging.WARNING):
3485 mod._log_context_assertion(capable, 12345, 2)
3486 assert any(record.levelno == logging.WARNING for record in caplog.records)
3487
3488 caplog.clear()
3489 with caplog.at_level(logging.INFO):
3490 mod._log_context_assertion(capable, None, None)
3491 assert not any(record.levelno >= logging.WARNING for record in caplog.records)
3492 assert any(
3493 "context assertion skipped" in record.message for record in caplog.records
3494 )
3495
3496
3497def test_start_local_server_skips_missing_artifacts(monkeypatch):
3498 mod = importlib.import_module("solstone.think.supervisor")
3499
3500 readiness = ReadinessOutcome(
3501 provider="local",
3502 status="missing-or-mismatched",
3503 reason_code="manifest_missing",
3504 target={},
3505 install={"install_state": "idle"},
3506 host={},
3507 artifacts={},
3508 proof={},
3509 )
3510 launch = MagicMock()
3511 monkeypatch.setattr(mod, "_launch_process", launch)
3512
3513 observation = mod._readiness_block_observation(
3514 provider="local",
3515 readiness=readiness,
3516 fingerprint_json='{"provider":"local"}',
3517 fingerprint_sha256_value="fp-local",
3518 boot_required=True,
3519 )
3520
3521 assert observation is not None
3522 assert observation.phase == "artifact-not-ready"
3523 assert observation.reason_code == "artifact-missing"
3524 launch.assert_not_called()
3525
3526
3527def _configure_linux_llama_start(
3528 mod,
3529 tmp_path,
3530 monkeypatch,
3531 *,
3532 log_text: str,
3533 poll_return=None,
3534):
3535 from solstone.think.providers import local_install, local_server, local_vulkan
3536
3537 mod._SERVICE_STATE.clear()
3538 binary = tmp_path / "llama-server"
3539 model_artifact_dir = local_install.model_dir(mod.LOCAL_MODEL)
3540 gguf = model_artifact_dir / "model.gguf"
3541 log_path = tmp_path / "llama-server.log"
3542 log_path.write_text(log_text, encoding="utf-8")
3543 managed = _TaskManagedStub(cmd=[])
3544 managed.name = "llama-server"
3545 managed.process.returncode = poll_return
3546 managed.process.poll = MagicMock(return_value=poll_return)
3547 managed.log_writer = type("LogWriter", (), {"path": log_path})()
3548 spawned: list[list[str]] = []
3549 spawned_envs: list[dict[str, str] | None] = []
3550 plan = _vulkan_launch_plan(mod, binary, gguf, None, vram_mib=6390)
3551
3552 monkeypatch.setattr(local_vulkan, "device_local_used_mib", lambda index: 512)
3553 monkeypatch.setattr(mod, "write_service_port", lambda _service, _port: None)
3554 monkeypatch.setattr(
3555 local_server, "write_local_context_window", lambda _tokens: None
3556 )
3557 monkeypatch.setattr(local_server, "_probe_health", lambda _port: ("ready", None))
3558 monkeypatch.setattr(local_server, "fetch_props", lambda _port: None)
3559
3560 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
3561 spawned.append(cmd)
3562 spawned_envs.append(env)
3563 managed.cmd = cmd
3564 managed.ref = ref
3565 return managed
3566
3567 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
3568 return managed, spawned, spawned_envs, plan
3569
3570
3571def test_start_local_server_skips_without_hardware_gpu(tmp_path, monkeypatch):
3572 mod = importlib.import_module("solstone.think.supervisor")
3573 from solstone.think.providers import local_vulkan
3574
3575 launch = MagicMock()
3576 monkeypatch.setattr(mod, "_launch_process", launch)
3577
3578 observation = mod.ProviderTruthObservation(
3579 provider="local",
3580 phase="host-blocked",
3581 reason_code="gpu-unavailable",
3582 desired_fingerprint_json='{"provider":"local"}',
3583 desired_fingerprint_sha256="fp-local",
3584 boot_required=True,
3585 detail={
3586 "readiness_status": "host-ineligible",
3587 "readiness_reason_code": "gpu_unavailable",
3588 "devices": mod._format_vulkan_devices([], local_vulkan),
3589 },
3590 )
3591
3592 assert observation.phase == "host-blocked"
3593 assert observation.reason_code == "gpu-unavailable"
3594 launch.assert_not_called()
3595
3596
3597def test_start_local_server_benign_load_log_returns_ready(tmp_path, monkeypatch):
3598 mod = importlib.import_module("solstone.think.supervisor")
3599 managed, _spawned, spawned_envs, plan = _configure_linux_llama_start(
3600 mod, tmp_path, monkeypatch, log_text=BENIGN_LLAMA_LOAD_LOG
3601 )
3602
3603 result = mod.start_local_server(plan, _FakeReservation())
3604
3605 assert result.status == "ready"
3606 assert result.managed is managed
3607 assert spawned_envs[0]["GGML_VK_VISIBLE_DEVICES"] == "1"
3608 managed.terminate.assert_not_called()
3609
3610
3611def test_start_local_server_process_exit_during_warmup_fails_closed(
3612 tmp_path, monkeypatch
3613):
3614 mod = importlib.import_module("solstone.think.supervisor")
3615 managed, _spawned, _envs, plan = _configure_linux_llama_start(
3616 mod,
3617 tmp_path,
3618 monkeypatch,
3619 log_text="2026-06-12T12:00:00+00:00 [llama-server:stderr] loading\n",
3620 poll_return=1,
3621 )
3622
3623 result = mod.start_local_server(plan, _FakeReservation())
3624
3625 assert result.status == "exited"
3626 assert result.reason_code == "process-exited"
3627 assert result.managed is managed
3628 managed.terminate.assert_not_called()
3629 managed.cleanup.assert_not_called()
3630
3631
3632def test_start_local_server_deadline_with_live_process_returns_managed(
3633 tmp_path, monkeypatch
3634):
3635 mod = importlib.import_module("solstone.think.supervisor")
3636 monkeypatch.setattr(mod, "LOCAL_SERVER_READY_TIMEOUT_S", 0.0)
3637 managed, _spawned, _envs, plan = _configure_linux_llama_start(
3638 mod,
3639 tmp_path,
3640 monkeypatch,
3641 log_text="2026-06-12T12:00:00+00:00 [llama-server:stderr] loading\n",
3642 )
3643
3644 result = mod.start_local_server(plan, _FakeReservation())
3645
3646 assert result.status == "warmup-timeout"
3647 assert result.reason_code == "warmup-timeout"
3648 assert result.managed is managed
3649 managed.terminate.assert_not_called()
3650 managed.cleanup.assert_not_called()
3651
3652
3653def _configure_cuda_llama_start(
3654 mod,
3655 tmp_path,
3656 monkeypatch,
3657 *,
3658 probe,
3659 ready: bool = True,
3660):
3661 from solstone.think.providers import local_install, local_server
3662
3663 mod._SERVICE_STATE.clear()
3664 binary = tmp_path / "llama-server"
3665 model_artifact_dir = local_install.model_dir(mod.LOCAL_MODEL)
3666 gguf = model_artifact_dir / "model.gguf"
3667 mmproj = model_artifact_dir / "mmproj.gguf"
3668 lib_dir = tmp_path / "cuda-lib"
3669 lib_dir.mkdir()
3670 managed = _TaskManagedStub(cmd=[])
3671 managed.name = "llama-server"
3672 managed.process.returncode = None
3673 written_ports = []
3674 written_context_windows = []
3675 spawned: list[list[str]] = []
3676 spawned_envs: list[dict[str, str] | None] = []
3677 plan = _cuda_launch_plan(
3678 mod,
3679 binary,
3680 gguf,
3681 mmproj,
3682 lib_dir,
3683 tiering_memory_mib=probe.tiering_memory_mib,
3684 visible_device=str(probe.index if probe.index is not None else 0),
3685 )
3686
3687 monkeypatch.setattr(
3688 mod,
3689 "write_service_port",
3690 lambda service, port: written_ports.append((service, port)),
3691 )
3692 monkeypatch.setattr(
3693 local_server,
3694 "write_local_context_window",
3695 lambda tokens: written_context_windows.append(tokens),
3696 )
3697 monkeypatch.setattr(
3698 local_server,
3699 "_probe_health",
3700 lambda _port: (
3701 (local_server.STATE_READY, None)
3702 if ready
3703 else (local_server.STATE_FAILED, "loading")
3704 ),
3705 )
3706 monkeypatch.setattr(local_server, "fetch_props", lambda _port: None)
3707
3708 def fake_spawn(cmd, *, ref=None, callosum=None, day=None, env=None):
3709 spawned.append(cmd)
3710 spawned_envs.append(env)
3711 managed.cmd = cmd
3712 managed.ref = ref
3713 return managed
3714
3715 monkeypatch.setattr(mod.RunnerManagedProcess, "spawn", fake_spawn)
3716 return (
3717 managed,
3718 spawned,
3719 spawned_envs,
3720 written_ports,
3721 written_context_windows,
3722 lib_dir,
3723 plan,
3724 )
3725
3726
3727def test_start_local_server_cuda_launches_llama_server_cmd_and_env(
3728 tmp_path, monkeypatch
3729):
3730 mod = importlib.import_module("solstone.think.supervisor")
3731 from solstone.think.providers import local_cuda
3732
3733 monkeypatch.setenv("LD_LIBRARY_PATH", "/existing/lib")
3734 probe = local_cuda.NvidiaProbe(
3735 index=0,
3736 compute_cap="sm_121",
3737 driver_cuda_version=13,
3738 vram_mib=20000,
3739 tiering_memory_mib=20000,
3740 memory_source=local_cuda.MEMORY_SOURCE_NVIDIA_VRAM,
3741 detected=True,
3742 )
3743 (
3744 managed,
3745 spawned,
3746 spawned_envs,
3747 written_ports,
3748 written_context_windows,
3749 lib_dir,
3750 plan,
3751 ) = _configure_cuda_llama_start(mod, tmp_path, monkeypatch, probe=probe)
3752
3753 result = mod.start_local_server(plan, _FakeReservation())
3754
3755 assert result.status == "ready"
3756 assert result.managed is managed
3757 assert written_ports == []
3758 assert written_context_windows == []
3759 assert len(spawned) == 1
3760 cmd = spawned[0]
3761 assert cmd[cmd.index("--device") + 1] == "CUDA0"
3762 assert cmd[cmd.index("-c") + 1] == str(plan.context_tokens * plan.parallel_slots)
3763 assert cmd[cmd.index("--host") + 1] == "127.0.0.1"
3764 assert cmd[cmd.index("--n-gpu-layers") + 1] == "999"
3765 assert "--mmproj" in cmd
3766 assert "0.0.0.0" not in cmd
3767 assert spawned_envs[0]["CUDA_VISIBLE_DEVICES"] == "0"
3768 assert spawned_envs[0]["LD_LIBRARY_PATH"] == f"{lib_dir}:/existing/lib"
3769
3770
3771def test_start_local_server_cuda_missing_vram_uses_floor_tier(
3772 tmp_path, monkeypatch, caplog
3773):
3774 mod = importlib.import_module("solstone.think.supervisor")
3775 from solstone.think.providers import local_cuda, local_server
3776
3777 probe = local_cuda.NvidiaProbe(
3778 index=0,
3779 compute_cap="sm_121",
3780 driver_cuda_version=13,
3781 vram_mib=None,
3782 tiering_memory_mib=None,
3783 memory_source=local_cuda.MEMORY_SOURCE_UNAVAILABLE,
3784 detected=True,
3785 )
3786 (
3787 managed,
3788 spawned,
3789 _spawned_envs,
3790 _ports,
3791 written_context_windows,
3792 _lib_dir,
3793 plan,
3794 ) = _configure_cuda_llama_start(mod, tmp_path, monkeypatch, probe=probe)
3795
3796 with caplog.at_level(logging.INFO):
3797 result = mod.start_local_server(plan, _FakeReservation())
3798
3799 assert result.status == "ready"
3800 assert result.managed is managed
3801 assert written_context_windows == []
3802 assert spawned[0][spawned[0].index("-c") + 1] == str(
3803 local_server.LOCAL_MIN_CONTEXT_TOKENS
3804 )
3805 assert any(
3806 "local server backend=cuda" in record.message for record in caplog.records
3807 )
3808
3809
3810def test_start_local_server_cuda_unified_memory_uses_capable_tier(
3811 tmp_path, monkeypatch, caplog
3812):
3813 mod = importlib.import_module("solstone.think.supervisor")
3814 from solstone.think.providers import local_cuda
3815
3816 probe = local_cuda.NvidiaProbe(
3817 index=0,
3818 compute_cap="sm_121",
3819 driver_cuda_version=13,
3820 vram_mib=None,
3821 tiering_memory_mib=26724,
3822 memory_source=local_cuda.MEMORY_SOURCE_SYSTEM_AVAILABLE,
3823 detected=True,
3824 )
3825 (
3826 managed,
3827 spawned,
3828 _spawned_envs,
3829 _ports,
3830 written_context_windows,
3831 _lib_dir,
3832 plan,
3833 ) = _configure_cuda_llama_start(mod, tmp_path, monkeypatch, probe=probe)
3834
3835 with caplog.at_level(logging.INFO):
3836 result = mod.start_local_server(plan, _FakeReservation())
3837
3838 assert result.status == "ready"
3839 assert result.managed is managed
3840 assert written_context_windows == []
3841 assert spawned[0][spawned[0].index("-c") + 1] == str(
3842 plan.context_tokens * plan.parallel_slots
3843 )
3844 assert any(
3845 "local server backend=cuda" in record.message for record in caplog.records
3846 )
3847
3848
3849def test_start_local_server_cuda_low_unified_memory_uses_floor_tier(
3850 tmp_path, monkeypatch
3851):
3852 mod = importlib.import_module("solstone.think.supervisor")
3853 from solstone.think.providers import local_cuda, local_server
3854
3855 probe = local_cuda.NvidiaProbe(
3856 index=0,
3857 compute_cap="sm_121",
3858 driver_cuda_version=13,
3859 vram_mib=None,
3860 tiering_memory_mib=8000,
3861 memory_source=local_cuda.MEMORY_SOURCE_SYSTEM_AVAILABLE,
3862 detected=True,
3863 )
3864 (
3865 _managed,
3866 spawned,
3867 _spawned_envs,
3868 _ports,
3869 written_context_windows,
3870 _lib_dir,
3871 plan,
3872 ) = _configure_cuda_llama_start(mod, tmp_path, monkeypatch, probe=probe)
3873
3874 mod.start_local_server(plan, _FakeReservation())
3875
3876 assert written_context_windows == []
3877 assert spawned[0][spawned[0].index("-c") + 1] == str(
3878 local_server.LOCAL_MIN_CONTEXT_TOKENS
3879 )
3880
3881
3882def test_start_local_server_cuda_warmup_timeout_fails_closed(tmp_path, monkeypatch):
3883 mod = importlib.import_module("solstone.think.supervisor")
3884 from solstone.think.providers import local_cuda
3885
3886 monkeypatch.setattr(mod, "LOCAL_SERVER_READY_TIMEOUT_S", 0.0)
3887 probe = local_cuda.NvidiaProbe(
3888 index=0,
3889 compute_cap="sm_121",
3890 driver_cuda_version=13,
3891 vram_mib=20000,
3892 tiering_memory_mib=20000,
3893 memory_source=local_cuda.MEMORY_SOURCE_NVIDIA_VRAM,
3894 detected=True,
3895 )
3896 (
3897 managed,
3898 _spawned,
3899 _spawned_envs,
3900 _ports,
3901 _contexts,
3902 _lib_dir,
3903 plan,
3904 ) = _configure_cuda_llama_start(
3905 mod, tmp_path, monkeypatch, probe=probe, ready=False
3906 )
3907 terminated = []
3908
3909 def terminate_managed(managed_arg, timeout, *, reason):
3910 terminated.append((managed_arg, timeout, reason))
3911
3912 monkeypatch.setattr(mod, "_terminate_managed", terminate_managed)
3913
3914 result = mod.start_local_server(plan, _FakeReservation())
3915
3916 assert result.status == "warmup-timeout"
3917 assert result.reason_code == "warmup-timeout"
3918 assert result.managed is managed
3919 assert terminated == []
3920 managed.cleanup.assert_not_called()
3921
3922
3923def test_supervisor_provider_start_lifecycle_events_are_ignored():
3924 mod = importlib.import_module("solstone.think.supervisor")
3925
3926 mod._handle_callosum_message({"tract": "supervisor", "event": "start_local"})
3927 mod._handle_callosum_message({"tract": "supervisor", "event": "start_parakeet"})
3928
3929 assert not hasattr(mod, "_request_provider_runtime_retry")
3930
3931
3932def test_supervisor_provider_start_lifecycle_handlers_are_absent():
3933 mod = importlib.import_module("solstone.think.supervisor")
3934
3935 assert not hasattr(mod, "_handle_supervisor_start_local")
3936 assert not hasattr(mod, "_handle_supervisor_start_parakeet")
3937 assert not hasattr(mod, "_request_local_server_start")
3938 assert not hasattr(mod, "_request_parakeet_server_start")
3939
3940
3941def test_handle_runner_exits_reports_llama_server_to_reconciler(monkeypatch):
3942 mod = importlib.import_module("solstone.think.supervisor")
3943 mod._SERVICE_STATE.clear()
3944 mod._RESTART_POLICIES.clear()
3945 monkeypatch.setattr(mod.time, "time", lambda: 100.0)
3946 monkeypatch.setattr(mod, "shutdown_requested", False)
3947
3948 managed = _TaskManagedStub(cmd=["/tmp/llama-server", "-m", "/tmp/model.gguf"])
3949 managed.name = "llama-server"
3950 managed.process.poll.return_value = 1
3951 managed.process.returncode = 1
3952 state = mod.ProviderRuntimeState("local")
3953 state.generation = 2
3954 state.desired_fingerprint = "fp-local"
3955 state.latest_phase = "ready"
3956 state.latest_plan = _vulkan_launch_plan(
3957 mod,
3958 Path("/tmp/llama-server"),
3959 Path("/tmp/model.gguf"),
3960 None,
3961 vram_mib=8000,
3962 )
3963 writes = []
3964
3965 mod._SERVICE_STATE["llama-server"] = {
3966 "restart": True,
3967 "shutdown_timeout": 12,
3968 }
3969 monkeypatch.setattr(
3970 mod,
3971 "_provider_runtime_states",
3972 {
3973 "local": state,
3974 "parakeet": mod.ProviderRuntimeState("parakeet"),
3975 },
3976 )
3977 monkeypatch.setattr(
3978 mod,
3979 "_recovery_state",
3980 {
3981 "local": mod.ProviderRecoveryState(),
3982 "parakeet": mod.ProviderRecoveryState(),
3983 },
3984 )
3985 monkeypatch.setattr(
3986 mod,
3987 "_write_provider_runtime",
3988 lambda state_arg, **kwargs: writes.append((state_arg, kwargs)),
3989 )
3990
3991 def fake_launch(name, cmd, *, restart=False, shutdown_timeout=15, ref=None):
3992 raise AssertionError("provider-owned exits must not use generic restart")
3993
3994 monkeypatch.setattr(mod, "_launch_process", fake_launch)
3995 monkeypatch.setattr(mod, "_supervisor_callosum", None)
3996
3997 procs = [managed]
3998 asyncio.run(mod.handle_runner_exits(procs))
3999
4000 assert procs == []
4001 assert state.generation == 3
4002 assert state.latest_phase == "stopped"
4003 assert state.latest_plan is None
4004 assert state.next_truth_at == 0.0
4005 assert mod._recovery_state["local"].down_generation == 3
4006 assert mod._SERVICE_STATE["llama-server"] == {
4007 "restart": True,
4008 "shutdown_timeout": 12,
4009 }
4010 assert writes[-1][1]["phase"] == "stopped"
4011 assert writes[-1][1]["reason_code"] == "process-exited"
4012
4013
4014def test_supervisor_singleton_lock_acquired(tmp_path, monkeypatch):
4015 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4016
4017 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4018 (tmp_path / "health").mkdir(parents=True, exist_ok=True)
4019 monkeypatch.setattr(sys, "argv", ["supervisor"])
4020
4021 def stop_after_lock():
4022 raise SystemExit(0)
4023
4024 # Skip maint discovery/subprocess runs — unrelated to lock acquisition and
4025 # slow enough on a fresh tmp_path to blow the 5s pytest-timeout under load.
4026 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [])
4027 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
4028 monkeypatch.setattr(mod.time, "sleep", lambda _seconds: None)
4029 monkeypatch.setattr(mod, "start_callosum_in_process", stop_after_lock)
4030
4031 with pytest.raises(SystemExit) as exc:
4032 mod.main()
4033
4034 assert exc.value.code == 0
4035 assert (tmp_path / "health" / "supervisor.lock").exists()
4036 assert (tmp_path / "health" / "supervisor.pid").read_text().strip() == str(
4037 os.getpid()
4038 )
4039 start_time = float(
4040 (tmp_path / "health" / "supervisor.start_time").read_text().strip()
4041 )
4042 assert start_time == psutil.Process(os.getpid()).create_time()
4043 assert mod.is_supervisor_up() is True
4044
4045
4046def test_supervisor_blocks_before_callosum_on_blocking_maint_failure(
4047 tmp_path, monkeypatch, capsys
4048):
4049 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4050 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4051 (tmp_path / "health").mkdir(parents=True, exist_ok=True)
4052 monkeypatch.setattr(sys, "argv", ["supervisor"])
4053 task = MaintTask(
4054 app="thinking",
4055 name="001_migrate_provider_install_state",
4056 script_path=Path("/dummy.py"),
4057 retry_on_next_start=True,
4058 blocks_supervisor_start=True,
4059 )
4060 state_file = tmp_path / "maint" / "thinking" / f"{task.name}.jsonl"
4061 result = MaintTaskResult(
4062 task=task,
4063 success=False,
4064 exit_code=7,
4065 state_file=state_file,
4066 )
4067 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [result])
4068 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
4069 start_mock = MagicMock()
4070 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4071
4072 with pytest.raises(SystemExit) as exc:
4073 mod.main()
4074
4075 assert exc.value.code == 1
4076 start_mock.assert_not_called()
4077 captured = capsys.readouterr()
4078 assert "thinking:001_migrate_provider_install_state" in captured.err
4079 assert str(state_file) in captured.err
4080 assert "retry-on-next-start" in captured.err
4081
4082
4083def test_supervisor_continues_after_successful_blocking_maint_task(
4084 tmp_path, monkeypatch
4085):
4086 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4087 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4088 (tmp_path / "health").mkdir(parents=True, exist_ok=True)
4089 monkeypatch.setattr(sys, "argv", ["supervisor"])
4090 task = MaintTask(
4091 app="thinking",
4092 name="001_migrate_provider_install_state",
4093 script_path=Path("/dummy.py"),
4094 retry_on_next_start=True,
4095 blocks_supervisor_start=True,
4096 )
4097 result = MaintTaskResult(
4098 task=task,
4099 success=True,
4100 exit_code=0,
4101 state_file=tmp_path / "maint" / "thinking" / f"{task.name}.jsonl",
4102 )
4103 monkeypatch.setattr(mod, "run_pending_tasks", lambda *a, **k: [result])
4104 monkeypatch.setattr(mod, "_sweep_orphaned_sol_processes", lambda *_a, **_k: 0)
4105
4106 def stop_after_callosum():
4107 raise SystemExit(0)
4108
4109 start_mock = MagicMock(side_effect=stop_after_callosum)
4110 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4111
4112 with pytest.raises(SystemExit) as exc:
4113 mod.main()
4114
4115 assert exc.value.code == 0
4116 start_mock.assert_called_once()
4117
4118
4119def test_supervisor_singleton_lock_blocked(tmp_path, monkeypatch, capsys):
4120 import fcntl
4121
4122 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4123
4124 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4125 monkeypatch.delenv("INVOCATION_ID", raising=False)
4126 health_dir = tmp_path / "health"
4127 health_dir.mkdir(parents=True, exist_ok=True)
4128 lock_file = open(health_dir / "supervisor.lock", "w")
4129 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
4130 (health_dir / "supervisor.pid").write_text("12345")
4131 monkeypatch.setattr(sys, "argv", ["supervisor"])
4132
4133 start_mock = MagicMock()
4134 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4135
4136 try:
4137 with pytest.raises(SystemExit) as exc:
4138 mod.main()
4139 finally:
4140 lock_file.close()
4141
4142 assert exc.value.code == 1
4143 output = capsys.readouterr().out
4144 assert "Supervisor already running" in output
4145 assert "PID 12345" in output
4146 start_mock.assert_not_called()
4147
4148
4149def test_supervisor_singleton_lock_blocked_under_systemd_exits_cleanly(
4150 tmp_path, monkeypatch, capsys
4151):
4152 import fcntl
4153
4154 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4155
4156 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4157 monkeypatch.setenv("INVOCATION_ID", "test-invocation-uuid")
4158 health_dir = tmp_path / "health"
4159 health_dir.mkdir(parents=True, exist_ok=True)
4160 lock_file = open(health_dir / "supervisor.lock", "w")
4161 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
4162 (health_dir / "supervisor.pid").write_text("12345")
4163 monkeypatch.setattr(sys, "argv", ["supervisor"])
4164
4165 start_mock = MagicMock()
4166 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4167
4168 try:
4169 with pytest.raises(SystemExit) as exc:
4170 mod.main()
4171 finally:
4172 lock_file.close()
4173
4174 assert exc.value.code == 0
4175 output = capsys.readouterr().out
4176 assert (
4177 "Supervisor already running (PID 12345) - exiting cleanly under "
4178 "systemd activation"
4179 ) in output
4180 start_mock.assert_not_called()
4181
4182
4183def test_supervisor_singleton_lock_blocked_with_health(tmp_path, monkeypatch, capsys):
4184 import fcntl
4185
4186 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4187
4188 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4189 monkeypatch.delenv("INVOCATION_ID", raising=False)
4190 health_dir = tmp_path / "health"
4191 health_dir.mkdir(parents=True, exist_ok=True)
4192 lock_file = open(health_dir / "supervisor.lock", "w")
4193 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
4194 (health_dir / "supervisor.pid").write_text("12345")
4195 (health_dir / "callosum.sock").touch()
4196 monkeypatch.setattr(sys, "argv", ["supervisor"])
4197
4198 start_mock = MagicMock()
4199 health_mock = MagicMock(return_value=0)
4200 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4201 monkeypatch.setattr("solstone.think.health_cli.health_check", health_mock)
4202
4203 try:
4204 with pytest.raises(SystemExit) as exc:
4205 mod.main()
4206 finally:
4207 lock_file.close()
4208
4209 assert exc.value.code == 1
4210 output = capsys.readouterr().out
4211 assert "Supervisor already running" in output
4212 assert "PID 12345" in output
4213 health_mock.assert_called_once_with()
4214 start_mock.assert_not_called()
4215
4216
4217def test_supervisor_singleton_lock_blocked_with_health_under_systemd_skips_health_check(
4218 tmp_path, monkeypatch, capsys
4219):
4220 import fcntl
4221
4222 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4223
4224 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4225 monkeypatch.setenv("INVOCATION_ID", "test-invocation-uuid")
4226 health_dir = tmp_path / "health"
4227 health_dir.mkdir(parents=True, exist_ok=True)
4228 lock_file = open(health_dir / "supervisor.lock", "w")
4229 fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
4230 (health_dir / "supervisor.pid").write_text("12345")
4231 (health_dir / "callosum.sock").touch()
4232 monkeypatch.setattr(sys, "argv", ["supervisor"])
4233
4234 start_mock = MagicMock()
4235 health_mock = MagicMock(return_value=0)
4236 monkeypatch.setattr(mod, "start_callosum_in_process", start_mock)
4237 monkeypatch.setattr("solstone.think.health_cli.health_check", health_mock)
4238
4239 try:
4240 with pytest.raises(SystemExit) as exc:
4241 mod.main()
4242 finally:
4243 lock_file.close()
4244
4245 assert exc.value.code == 0
4246 output = capsys.readouterr().out
4247 assert (
4248 "Supervisor already running (PID 12345) - exiting cleanly under "
4249 "systemd activation"
4250 ) in output
4251 health_mock.assert_not_called()
4252 start_mock.assert_not_called()
4253
4254
4255def test_is_supervisor_up_without_pid_file(tmp_path, monkeypatch):
4256 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4257
4258 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4259 (tmp_path / "health").mkdir(parents=True, exist_ok=True)
4260
4261 assert mod.is_supervisor_up() is False
4262
4263
4264def test_is_supervisor_up_with_dead_pid(tmp_path, monkeypatch):
4265 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4266
4267 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4268 health_dir = tmp_path / "health"
4269 health_dir.mkdir(parents=True, exist_ok=True)
4270
4271 proc = subprocess.Popen(["true"])
4272 proc.wait()
4273 (health_dir / "supervisor.pid").write_text(str(proc.pid))
4274
4275 assert mod.is_supervisor_up() is False
4276
4277
4278def test_is_supervisor_up_with_live_pid_missing_start_time(tmp_path, monkeypatch):
4279 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4280
4281 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4282 health_dir = tmp_path / "health"
4283 health_dir.mkdir(parents=True, exist_ok=True)
4284 (health_dir / "supervisor.pid").write_text(str(os.getpid()))
4285
4286 assert mod.is_supervisor_up() is False
4287
4288
4289def test_is_supervisor_up_with_live_pid_mismatched_start_time(tmp_path, monkeypatch):
4290 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4291
4292 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4293 health_dir = tmp_path / "health"
4294 health_dir.mkdir(parents=True, exist_ok=True)
4295 (health_dir / "supervisor.pid").write_text(str(os.getpid()))
4296 create_time = psutil.Process(os.getpid()).create_time()
4297 (health_dir / "supervisor.start_time").write_text(str(create_time + 60))
4298
4299 assert mod.is_supervisor_up() is False
4300
4301
4302def test_is_supervisor_up_with_matching_process_identity(tmp_path, monkeypatch):
4303 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4304
4305 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4306 health_dir = tmp_path / "health"
4307 health_dir.mkdir(parents=True, exist_ok=True)
4308 (health_dir / "supervisor.pid").write_text(str(os.getpid()))
4309 (health_dir / "supervisor.start_time").write_text(
4310 str(psutil.Process(os.getpid()).create_time())
4311 )
4312
4313 assert mod.is_supervisor_up() is True
4314
4315
4316def test_is_supervisor_up_boundary_at_shared_tolerance(tmp_path, monkeypatch):
4317 mod = importlib.reload(importlib.import_module("solstone.think.supervisor"))
4318
4319 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
4320 health_dir = tmp_path / "health"
4321 health_dir.mkdir(parents=True, exist_ok=True)
4322 (health_dir / "supervisor.pid").write_text(str(os.getpid()))
4323 create_time = psutil.Process(os.getpid()).create_time()
4324 within = create_time + mod.START_TIME_TOLERANCE_S - 0.1
4325 beyond = create_time + mod.START_TIME_TOLERANCE_S + 0.1
4326
4327 (health_dir / "supervisor.start_time").write_text(str(within))
4328 assert mod.is_supervisor_up() is True
4329
4330 (health_dir / "supervisor.start_time").write_text(str(beyond))
4331 assert mod.is_supervisor_up() is False