File size: 6,598 Bytes
69f9cd1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f30086
 
69f9cd1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Stability of autonomous operation: bounded status reads, restart backoff,
and a single configurable local LLM path for Naldo_Bot.

Offline-safe: pure stdlib plus text-level assertions on the deployment files.
"""
from __future__ import annotations

import re
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest import mock

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from voyager_stack import restart_backoff_after, tail_lines  # noqa: E402
import gemini_bot_stack as gemini_module  # noqa: E402
import minecraft_stack as mc_stack  # noqa: E402
from minecraft_stack import _tail_lines as mc_tail_lines  # noqa: E402


def test_tail_lines_matches_full_read_on_small_logs(tmp_path):
    path = tmp_path / "voyager.log"
    expected = [f"line {i} [llm_metric] payload" for i in range(500)]
    path.write_text("\n".join(expected) + "\n", encoding="utf-8")
    assert tail_lines(path) == expected
    assert mc_tail_lines(path, 500) == expected


def test_tail_lines_is_bounded_on_long_logs(tmp_path):
    path = tmp_path / "voyager.log"
    total = 60000
    with path.open("w", encoding="utf-8") as fh:
        for i in range(total):
            fh.write(f"line {i}\n")
    window = tail_lines(path, 20000)
    assert len(window) == 20000
    assert window[0] == f"line {total - 20000}"
    assert window[-1] == f"line {total - 1}"
    assert tail_lines(tmp_path / "missing.log") == []
    assert mc_tail_lines(tmp_path / "missing.log") == []


def test_restart_backoff_doubles_on_quick_crash_and_resets_when_healthy():
    assert restart_backoff_after(5, 12.0) == 10
    assert restart_backoff_after(60, 30.0) == 120
    assert restart_backoff_after(120, 1.0) == 120
    assert restart_backoff_after(120, 3600.0) == 5
    assert restart_backoff_after(120, 600.0) == 5
    assert restart_backoff_after(10, 599.9) == 20


def test_both_bots_share_the_same_backoff_policy():
    assert gemini_module.restart_backoff_after is restart_backoff_after
    assert gemini_module.tail_lines is tail_lines


def test_naldo_uses_only_the_configurable_local_endpoint():
    stack = (ROOT / "voyager_stack.py").read_text(encoding="utf-8")
    # Naldo generative brain is loopback OpenCode; embeddings live in the separate
    # CPU model Space because this Space intentionally runs server-only.
    assert "NALDO_BRIDGE_BASE" in stack
    assert "NALDO_BRAIN_PROFILE" in stack
    assert "NALDO_PRIMARY_MODEL" in stack
    assert "naldo-opencode" in stack or "NALDO_BRAIN_PROFILE" in stack
    assert "granite-4.0-h-tiny" not in stack
    runner = (ROOT / "voyager_runner.py").read_text(encoding="utf-8")
    assert "http://127.0.0.1:7864/v1" in runner
    assert "NALDO_BRAIN_PROFILE" in runner
    assert "from naldo_opencode import" in runner
    assert "naldo-granite" not in runner
    assert "voyager.learn(reset_env=False)" in runner


def test_no_new_direct_llm_sdk_dependency():
    requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8").lower()
    assert "google-generativeai" not in requirements
    assert "google-genai" not in requirements
    assert "anthropic" not in requirements
    assert "openai==0.28.1" in requirements
    assert "llama-cpp-python" in requirements


def test_local_endpoint_stays_cpu_only():
    app = (ROOT / "app.py").read_text(encoding="utf-8")
    assert "n_gpu_layers=0" in app
    assert 'os.environ["CUDA_VISIBLE_DEVICES"] = ""' in app


def test_acceptance_script_defaults_match_this_deployment():
    agent = (ROOT / "tests" / "test_agent.py").read_text(encoding="utf-8")
    app = (ROOT / "app.py").read_text(encoding="utf-8")
    readme = (ROOT / "README.md").read_text(encoding="utf-8")
    base = re.search(r'BASE = os.environ.get\("BASE", "(.*?)"\)', agent).group(1)
    model = re.search(r'MODEL = os.environ.get\("MODEL", "(.*?)"\)', agent).group(1)
    assert base in readme, "acceptance BASE default must be the documented Space URL"
    assert base.endswith("/v1")
    assert model == "granite-4.0-h-tiny"
    assert f'"{model}"' in app or f"'{model}'" in app or model in app


def test_boot_counters_survive_tail_window_and_reset_on_new_boot(tmp_path):
    import inspect

    # Wiring: every new minecraft boot must reset the counters.
    assert "_reset_boot_counters" in inspect.getsource(
        mc_stack.MinecraftStack._start_fabric
    )
    stack = mc_stack.MinecraftStack()
    empty_log = tmp_path / "stack.log"
    empty_log.write_text("log line without markers\n", encoding="utf-8")
    with mock.patch.object(mc_stack, "_log", lambda *args, **kwargs: None), \
         mock.patch.object(mc_stack, "LOG_PATH", empty_log):
        stack._pump("minecraft", SimpleNamespace(stdout=[
            "Starting minecraft server version 1.19\n",
            "Failed to save player data for Naldo_Bot\n",
            'java.lang.NullPointerException: Cannot read field "respawnDimension" because "player" is null\n',
            "Failed to save player data for IffyBinkie6134\n",
            'Done (1.234s)! For help, type "help"\n',
        ]))
        assert stack.failed_saves_since_boot == 2
        assert stack.respawn_npe_since_boot == 1

        stack._pump("viaproxy", SimpleNamespace(stdout=[
            "Failed to save player data for x\n",
        ]))
        assert stack.failed_saves_since_boot == 2
        assert stack.respawn_npe_since_boot == 1

        # The tail window holds none of those lines, yet the counters behind
        # the *_since_boot contract must still report them.
        report = stack.status()
        assert report["player_save"]["failed_saves_since_boot"] == 2
        assert report["player_save"]["respawn_npe_since_boot"] == 1

        stack._reset_boot_counters()
        report = stack.status()
        assert report["player_save"]["failed_saves_since_boot"] == 0
        assert report["player_save"]["respawn_npe_since_boot"] == 0


if __name__ == "__main__":
    import tempfile

    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)
        test_tail_lines_matches_full_read_on_small_logs(tmp_path)
        test_tail_lines_is_bounded_on_long_logs(tmp_path)
        test_boot_counters_survive_tail_window_and_reset_on_new_boot(tmp_path)
    test_restart_backoff_doubles_on_quick_crash_and_resets_when_healthy()
    test_both_bots_share_the_same_backoff_policy()
    test_naldo_uses_only_the_configurable_local_endpoint()
    test_no_new_direct_llm_sdk_dependency()
    test_local_endpoint_stays_cpu_only()
    test_acceptance_script_defaults_match_this_deployment()
    print("PASS autonomy stability")