From d37a3e2390f30b061a2977b0b18ca0d60e306118 Mon Sep 17 00:00:00 2001 From: lzwjava Date: Sat, 14 Mar 2026 22:36:27 +0800 Subject: [PATCH 1/5] Refactor login: remove standalone script, clean up login.py, and ignore .coverage --- .coverage | Bin 53248 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index b82b2b2dfd031073ed24c822efd6eb2264b4d4b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4UyR(u9mnmpcfG#by*YYHSK)fS5dvLK+#gO?^-o%fq~w4=XbGg0r#jxBDrAptbs^X2RFYw+z(T6?(eW_GI-++fgK_DIqi2hLosnq^vZ13JCxf9w% zB8k4EU3)x#e*XPF^P3sZdhJI~p70XEH^azx6TYrZYr3u-;at-+o4!^0=E0(fnIF(z z-xc30YHKUMdb`RV(<-IMHTI+Gt!%lnRrzG{xym=~A541IlD$G12p|9gAOHe44T1j2 zid8#!P(Sz6#I3hQ61fc#rQ7N~k8B)0wZTsv{qBhko~H3b6P&{GGC#_ru*W+h;+tMu za4%?j4L9+EEuOSQUb-8Lrkt}Vjb<%ba7ERZF~@6Ctwd~*h)(4BZgiGEALT zv6G|;Bt&>yfh@rfWz`pXBsN7Pf`*7wEe?6j#finAJvCv~W@hyBLfPa-4f;%uSVIXz zQ;u(vphg%qrS|p64H_*GFL8HZVIw3xcaqfFDD+p8khi@+nHj`h;)MYhJEGA|MDvOU zE=g#4Ab7j2Az7!TL8sH2*|~Pn8CJeR&8&D9@@fp?}W293YOhc%a!89}(jr4i%- zcTARQ-5-VA5IxKwAjzq4rW|wLk^Lx3@zy+MO zL3E;juw>O{XZ3Toa-*!ZvYj>JLU~JGVHnM`eBs93C?81n8zl1@!%;HtBUuiavuu@W zM`!mkCoAWyWs!2;(_ z)1|_0J?tjRf90WaPdWDt^-To~k8}1^YEc!Z?)`8W#~w|qa@vXy=c#<5<;HRpiWhi2 z4BNsD(z$_Z9FOUoUd)vam(NOf4`sw>rHrlZ1*KCyxyWJu6m`11e^JEu-JqGxS8^Io zrPA0hJEuHPI*3q3zCfKarY?Gk%v@-eD@Uwc{z`|KNU6TtOuM^mlwA^gEpeH+bT&HX zCL-~C!DSJpjB>1=8}mjaC^MT&m10D!b*WaVnENRwA45}X78i{Dd`C91vnIE4m0XsW za{YPTs@-{~-tT5TI31jvn63nTH@7uAER1;8C7DAf`#z1*bcwXvxg_vi4CP^Z200?8 z&0bH@<)yL_0c8flwL*KmY_l00ck)1V8`;KmY_l00cl_K%ipG>G{t9g$ZNU&VB`u z_y6tcNsYb8T5N9Wm8tEiyQ+V$K1WFiAOHd&00JNY0w4eaAOHd&00R3FIBuI-zqZ;6 zeX-j9NpE!}?9hv3+HP;P7e-IS9eNMWs_zBfawF_`?J!XXPJYl6bte||2!D$nfWN0q zq9(@`<-1-$LLRb6$o#lMvZwsx)o$XoV-k0)MB=LBife~kN~se?k}@~0lz~$8>J`c9 zhbBo|ziMeIcc&AD+g?*dB>1>Ng2!&lEiY+x>yF#;WHS^<)YUhGA2vms#NDTpxS4TH zb{ulR#BoTFy#H@kAJNz!rh3eq`cw5C_SLDi>Tf6s0R%t*1V8`;KmY_l00ck)1VCV4 z0uPw|nm+EzY-Ig!A2a)tV~fhx|K;zQ^cK2t^%*R+^Yy>AVfL$Ii_6#lrH9P^+}KhE zN@eT+;^St2{pzJ%zWz6_sx3#>|HidxhJ5{BxX^vMkWY=00@8p z2!H?xfB*=900@8p2;Ard4BgPG^8UZh{;Sak0R%t*1V8`;KmY_l00ck)1V8`;K;R}M zU>H?fJ^#;qjeW{KX792$*lX-1_IvhgN+S4B&(YO>k{gpD6I$Zwafp=@C|99co z*Pohy^87g?JKqszQ7$h%`Iify{&wzZDqbm3!mY+!k^_gBaDe@)r zpBLvR42pfZu)TBPt-H$wnOgi?j}>*(wx@LU{6BkHV;9+n>~;1&`#1Y5dyD;;LPiMW71^fB*=900@8p2!H?xfB*=900>-r0{;i3Cu;)$ diff --git a/.gitignore b/.gitignore index 4162ab1..dedd9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ package-lock.json **/package-lock.json node_modules/ .coverage +.coverage From 61cbdd160f6ec3ea890da7ae7279451dac5fa99b Mon Sep 17 00:00:00 2001 From: lzwjava Date: Sat, 14 Mar 2026 22:37:35 +0800 Subject: [PATCH 2/5] Cleanup .gitignore: remove duplicates (.coverage and node_modules) --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index dedd9e2..6e22d58 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,4 @@ public/config.json node_modules/ package-lock.json **/package-lock.json -node_modules/ -.coverage .coverage From 22bd928db016df6be97f20da2ca7f4b001f09e79 Mon Sep 17 00:00:00 2001 From: lzwjava Date: Sat, 14 Mar 2026 22:39:09 +0800 Subject: [PATCH 3/5] Add exec_command tool, integrate it into main app, and add tests --- mini_copilot/exec_tool.py | 26 ++++++++++++++++++++++++++ mini_copilot/main.py | 34 +++++++++++++++++++++++++++++++++- tests/test_exec_tool.py | 21 +++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 mini_copilot/exec_tool.py create mode 100644 tests/test_exec_tool.py diff --git a/mini_copilot/exec_tool.py b/mini_copilot/exec_tool.py new file mode 100644 index 0000000..a497a60 --- /dev/null +++ b/mini_copilot/exec_tool.py @@ -0,0 +1,26 @@ +import subprocess + + +def exec_command(command: str) -> str: + """Execute a shell command and return its output (stdout or stderr).""" + print(f"[exec] Running command: {command}") + try: + # Run command with a 30s timeout, capturing both stdout and stderr + result = subprocess.run( + command, shell=True, capture_output=True, text=True, timeout=30 + ) + output = result.stdout if result.returncode == 0 else result.stderr + if not output.strip(): + output = f"(Process exited with code {result.returncode})" + return output + except subprocess.TimeoutExpired: + return "Error: Command timed out after 30 seconds." + except Exception as e: + return f"Error executing command: {str(e)}" + + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + print(exec_command(" ".join(sys.argv[1:]))) diff --git a/mini_copilot/main.py b/mini_copilot/main.py index f9f5d96..11ec07c 100644 --- a/mini_copilot/main.py +++ b/mini_copilot/main.py @@ -20,6 +20,7 @@ def completer(text, state): from mini_copilot.github_api import chat, get_copilot_token from mini_copilot.web_search import web_search +from mini_copilot.exec_tool import exec_command from mini_copilot.commands.auth import handle_login_command from mini_copilot.commands.model import handle_model_command from mini_copilot.commands.search_provider import handle_search_provider_command @@ -59,7 +60,25 @@ def completer(text, state): }, }, } -TOOLS = [WEB_SEARCH_TOOL] + +EXEC_COMMAND_TOOL = { + "type": "function", + "function": { + "name": "exec_command", + "description": "Execute a shell command on the local system and return the output.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The shell command to execute.", + }, + }, + "required": ["command"], + }, + }, +} +TOOLS = [WEB_SEARCH_TOOL, EXEC_COMMAND_TOOL] def load_github_token(): @@ -167,6 +186,19 @@ def main(): "content": search_context, } ) + + if function_name == "exec_command": + command = function_args.get("command") + output = exec_command(command) + + messages.append( + { + "tool_call_id": tool_call["id"], + "role": "tool", + "name": function_name, + "content": output, + } + ) response_message = chat( messages, copilot_token, current_model, tools=TOOLS ) diff --git a/tests/test_exec_tool.py b/tests/test_exec_tool.py new file mode 100644 index 0000000..f185a53 --- /dev/null +++ b/tests/test_exec_tool.py @@ -0,0 +1,21 @@ +import unittest +from mini_copilot.exec_tool import exec_command + + +class TestExecTool(unittest.TestCase): + def test_exec_echo(self): + output = exec_command("echo 'hello world'") + self.assertEqual(output.strip(), "hello world") + + def test_exec_ls(self): + output = exec_command("ls pyproject.toml") + self.assertIn("pyproject.toml", output) + + def test_exec_error(self): + # Depending on the system, the error message might vary, but it should contain some indication of failure + output = exec_command("non_existent_command_12345") + self.assertIn("not found", output.lower()) + + +if __name__ == "__main__": + unittest.main() From ada01cb327835e8ee1f212e2ea61e77a47d636d2 Mon Sep 17 00:00:00 2001 From: lzwjava Date: Sat, 14 Mar 2026 22:40:37 +0800 Subject: [PATCH 4/5] Rename exec_command tool to exec --- mini_copilot/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mini_copilot/main.py b/mini_copilot/main.py index 11ec07c..18331da 100644 --- a/mini_copilot/main.py +++ b/mini_copilot/main.py @@ -20,7 +20,7 @@ def completer(text, state): from mini_copilot.github_api import chat, get_copilot_token from mini_copilot.web_search import web_search -from mini_copilot.exec_tool import exec_command +from mini_copilot.exec_tool import exec_command as exec from mini_copilot.commands.auth import handle_login_command from mini_copilot.commands.model import handle_model_command from mini_copilot.commands.search_provider import handle_search_provider_command @@ -64,7 +64,7 @@ def completer(text, state): EXEC_COMMAND_TOOL = { "type": "function", "function": { - "name": "exec_command", + "name": "exec", "description": "Execute a shell command on the local system and return the output.", "parameters": { "type": "object", @@ -187,9 +187,9 @@ def main(): } ) - if function_name == "exec_command": + if function_name == "exec": command = function_args.get("command") - output = exec_command(command) + output = exec(command) messages.append( { From d8302caf72c443af6d327cd5732f5c352d5648e6 Mon Sep 17 00:00:00 2001 From: lzwjava Date: Sat, 14 Mar 2026 22:43:14 +0800 Subject: [PATCH 5/5] Add integration test for the 'exec' tool --- integration_tests/test_exec_integration.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 integration_tests/test_exec_integration.py diff --git a/integration_tests/test_exec_integration.py b/integration_tests/test_exec_integration.py new file mode 100644 index 0000000..c94c869 --- /dev/null +++ b/integration_tests/test_exec_integration.py @@ -0,0 +1,86 @@ +import subprocess +import time +import sys +import os + + +def run_integration_test(): + print("šŸš€ Starting Integration Test for 'exec' tool...") + + # Ensure current directory is in PYTHONPATH for the subprocess + env = os.environ.copy() + env["PYTHONPATH"] = os.getcwd() + ":" + env.get("PYTHONPATH", "") + + # Start the mini-copilot process + process = subprocess.Popen( + [sys.executable, "-m", "mini_copilot.main"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Combine stdout and stderr + text=True, + bufsize=1, + cwd=os.getcwd(), + env=env, + ) + + try: + # 1. Wait for prompt + print("Waiting for prompt...") + output = "" + start_time = time.time() + while time.time() - start_time < 30: + char = process.stdout.read(1) + if not char: + break + output += char + if "> " in output: + print("āœ… Prompt detected.") + break + + if "> " not in output: + print(f"āŒ Timed out waiting for prompt. Last output: {output}") + return False + + # 2. Trigger tool + print("Sending message to trigger 'exec' tool...") + process.stdin.write("run command: echo integration_test_success\n") + process.stdin.flush() + + # 3. Look for tool execution + print("Monitoring for tool invocation and response...") + found_tool = False + found_output = False + start_time = time.time() + output = "" + while time.time() - start_time < 60: + char = process.stdout.read(1) + if not char: + break + output += char + sys.stdout.write(char) # Echo for observability + sys.stdout.flush() + + if "[exec] Running command: echo integration_test_success" in output: + found_tool = True + + if "integration_test_success" in output and found_tool: + found_output = True + print("\nāœ… Found expected output in responses!") + break + + if found_output: + print("\nšŸŽ‰ Integration Test PASSED!") + return True + else: + print("\nāŒ Integration Test FAILED.") + return False + + finally: + process.terminate() + + +if __name__ == "__main__": + if run_integration_test(): + sys.exit(0) + else: + sys.exit(1)