Skip to content

Commit d71cf3f

Browse files
committed
test: add no-network release integration suite
1 parent 85a33f0 commit d71cf3f

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

minicode/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,25 @@ def _prompt(request: dict) -> dict:
9090
return _prompt
9191

9292

93+
def _configure_stdio_for_unicode() -> None:
94+
for stream in (sys.stdout, sys.stderr):
95+
reconfigure = getattr(stream, "reconfigure", None)
96+
if reconfigure is not None:
97+
try:
98+
reconfigure(encoding="utf-8", errors="replace")
99+
except Exception:
100+
pass
101+
102+
93103
def _save_transcript_file(cwd: str, permissions, transcript: list[TranscriptEntry], output_path: str) -> str:
94104
target = resolve_tool_path(ToolContext(cwd=cwd, permissions=permissions), output_path, "write")
95105
target.parent.mkdir(parents=True, exist_ok=True)
96106
target.write_text(format_transcript_text(transcript), encoding="utf-8")
97107
return str(target)
98108

99109
def main() -> None:
110+
_configure_stdio_for_unicode()
111+
100112
parser = argparse.ArgumentParser(
101113
description="MiniCode Python - A lightweight terminal coding assistant",
102114
add_help=True,

tests/test_release_integration.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import sys
6+
import threading
7+
from pathlib import Path
8+
9+
from minicode.agent_loop import run_agent_turn
10+
from minicode.memory import MemoryManager
11+
from minicode.permissions import PermissionManager
12+
from minicode.prompt import build_system_prompt
13+
from minicode.tooling import ToolRegistry
14+
from minicode.tools import create_default_tool_registry
15+
from minicode.tui.event_flow import _handle_event
16+
from minicode.tui.input_handler import _handle_input
17+
from minicode.tui.input_parser import KeyEvent
18+
from minicode.tui.state import ScreenState, TtyAppArgs
19+
from minicode.types import AgentStep
20+
21+
22+
REPO_ROOT = Path(__file__).resolve().parent.parent
23+
24+
25+
def _release_env(tmp_path: Path) -> dict[str, str]:
26+
home = tmp_path / "home"
27+
home.mkdir(exist_ok=True)
28+
env = os.environ.copy()
29+
env.update(
30+
{
31+
"PYTHONPATH": str(REPO_ROOT)
32+
+ os.pathsep
33+
+ env.get("PYTHONPATH", ""),
34+
"HOME": str(home),
35+
"USERPROFILE": str(home),
36+
"MINI_CODE_MODEL": "gpt-4o",
37+
"MINI_CODE_MODEL_MODE": "mock",
38+
"MINI_CODE_TOOL_PROFILE": "core",
39+
"MINI_CODE_SHOW_GUIDE": "0",
40+
"OPENAI_API_KEY": "test-openai-key",
41+
"ANTHROPIC_API_KEY": "",
42+
"ANTHROPIC_AUTH_TOKEN": "",
43+
"OPENROUTER_API_KEY": "",
44+
"CUSTOM_API_KEY": "",
45+
"CUSTOM_API_BASE_URL": "",
46+
}
47+
)
48+
return env
49+
50+
51+
def test_release_cli_valid_config_runs_as_black_box(tmp_path: Path) -> None:
52+
workspace = tmp_path / "workspace"
53+
workspace.mkdir()
54+
55+
completed = subprocess.run(
56+
[sys.executable, "-m", "minicode.main", "valid-config"],
57+
cwd=workspace,
58+
env=_release_env(tmp_path),
59+
text=True,
60+
encoding="utf-8",
61+
errors="replace",
62+
stdout=subprocess.PIPE,
63+
stderr=subprocess.PIPE,
64+
timeout=20,
65+
check=False,
66+
)
67+
68+
assert completed.returncode == 0, completed.stderr
69+
assert "Configuration Diagnostics" in completed.stdout
70+
assert "Status: OK" in completed.stdout
71+
assert "Provider: openai" in completed.stdout
72+
assert "Tool Profile: core" in completed.stdout
73+
assert "UnicodeEncodeError" not in completed.stderr
74+
75+
76+
def test_release_non_tty_main_handles_memory_and_local_commands(tmp_path: Path) -> None:
77+
workspace = tmp_path / "workspace"
78+
workspace.mkdir()
79+
80+
completed = subprocess.run(
81+
[sys.executable, "-m", "minicode.main"],
82+
cwd=workspace,
83+
env=_release_env(tmp_path),
84+
input="# Prefer pytest before release\n/memory\n/tools\n/exit\n",
85+
text=True,
86+
encoding="utf-8",
87+
errors="replace",
88+
stdout=subprocess.PIPE,
89+
stderr=subprocess.PIPE,
90+
timeout=30,
91+
check=False,
92+
)
93+
94+
assert completed.returncode == 0, completed.stderr
95+
assert "Saved memory (project): Prefer pytest before release" in completed.stdout
96+
assert "Memory System Status" in completed.stdout
97+
assert "read_file:" in completed.stdout
98+
assert "base64_encode:" not in completed.stdout
99+
100+
memory_file = workspace / ".mini-code-memory" / "MEMORY.md"
101+
assert memory_file.exists()
102+
assert "Prefer pytest before release" in memory_file.read_text(encoding="utf-8")
103+
104+
105+
class ReadFileReleaseModel:
106+
def __init__(self) -> None:
107+
self.calls = 0
108+
109+
def next(self, messages, on_stream_chunk=None):
110+
self.calls += 1
111+
if self.calls == 1:
112+
return AgentStep(
113+
type="tool_calls",
114+
calls=[
115+
{
116+
"id": "release-read",
117+
"toolName": "read_file",
118+
"input": {"path": "README.md"},
119+
}
120+
],
121+
)
122+
tool_result = next(
123+
message for message in reversed(messages) if message["role"] == "tool_result"
124+
)
125+
return AgentStep(
126+
type="assistant",
127+
content=f"release final saw: {tool_result['content'][:80]}",
128+
)
129+
130+
131+
def test_release_agent_loop_executes_real_tool_chain(tmp_path: Path) -> None:
132+
workspace = tmp_path / "workspace"
133+
workspace.mkdir()
134+
(workspace / "README.md").write_text("# Release Fixture\n", encoding="utf-8")
135+
136+
tools = create_default_tool_registry(str(workspace), runtime={"toolProfile": "core"})
137+
permissions = PermissionManager(
138+
str(workspace),
139+
prompt=lambda _request: {"decision": "allow_once"},
140+
)
141+
142+
messages = run_agent_turn(
143+
model=ReadFileReleaseModel(),
144+
tools=tools,
145+
messages=[
146+
{"role": "system", "content": "release integration system"},
147+
{"role": "user", "content": "read the release fixture"},
148+
],
149+
cwd=str(workspace),
150+
permissions=permissions,
151+
max_steps=5,
152+
)
153+
154+
assert any(message["role"] == "assistant_tool_call" for message in messages)
155+
assert any(
156+
message["role"] == "tool_result" and "# Release Fixture" in message["content"]
157+
for message in messages
158+
)
159+
assert messages[-1]["role"] == "assistant"
160+
assert "Release Fixtur" in messages[-1]["content"]
161+
162+
163+
class PromptCapturingModel:
164+
def __init__(self) -> None:
165+
self.system_prompt = ""
166+
167+
def next(self, messages, on_stream_chunk=None):
168+
self.system_prompt = messages[0]["content"]
169+
return AgentStep(type="assistant", content="ok")
170+
171+
172+
def test_release_memory_is_injected_into_next_agent_turn(tmp_path: Path) -> None:
173+
workspace = tmp_path / "workspace"
174+
workspace.mkdir()
175+
memory = MemoryManager(workspace)
176+
assert memory.handle_user_memory_input("# Prefer pytest before release")
177+
178+
model = PromptCapturingModel()
179+
messages = [
180+
{
181+
"role": "system",
182+
"content": build_system_prompt(
183+
str(workspace),
184+
[],
185+
{
186+
"skills": [],
187+
"mcpServers": [],
188+
"memory_context": memory.get_relevant_context(query="release tests"),
189+
},
190+
),
191+
},
192+
{"role": "user", "content": "How should I verify release tests?"},
193+
]
194+
195+
run_agent_turn(
196+
model=model,
197+
tools=ToolRegistry([]),
198+
messages=messages,
199+
cwd=str(workspace),
200+
max_steps=1,
201+
)
202+
203+
assert "Project Memory & Context" in model.system_prompt
204+
assert "Prefer pytest before release" in model.system_prompt
205+
206+
207+
def test_release_tty_return_routes_memory_without_agent_turn(tmp_path: Path) -> None:
208+
workspace = tmp_path / "workspace"
209+
workspace.mkdir()
210+
memory = MemoryManager(workspace)
211+
state = ScreenState(input="# Prefer pytest before release", cursor_offset=30)
212+
args = TtyAppArgs(
213+
runtime=None,
214+
tools=ToolRegistry([]),
215+
model=None,
216+
messages=[{"role": "system", "content": "sys"}],
217+
cwd=str(workspace),
218+
permissions=PermissionManager(str(workspace)),
219+
memory_manager=memory,
220+
)
221+
renders: list[bool] = []
222+
223+
_handle_event(
224+
args,
225+
state,
226+
KeyEvent(name="return", ctrl=False, meta=False),
227+
lambda: renders.append(True),
228+
threading.Event(),
229+
{},
230+
_handle_input,
231+
)
232+
233+
assert renders
234+
assert state.is_busy is False
235+
assert len(state.transcript) == 2
236+
assert state.transcript[0].kind == "user"
237+
assert state.transcript[1].kind == "assistant"
238+
assert "Saved memory" in state.transcript[1].body
239+
assert any("pytest" in entry.content for entry in memory.search("pytest"))

0 commit comments

Comments
 (0)