from http.server import BaseHTTPRequestHandler, HTTPServer import json from pathlib import Path import tempfile import threading import unittest import urllib.error import urllib.request from console.artifacts import Collector from console.job_main import BudgetRelay from console.settings import Settings class OutputTests(unittest.TestCase): def test_final_file_log_drain_includes_fast_script_tail_larger_than_one_chunk(self): with tempfile.TemporaryDirectory() as temp: root = Path(temp) logs, run = root / "logs", root / "run" logs.mkdir() run.mkdir() collector = Collector(Settings(root, []), run, [str(logs)]) payload = b"file log\n" * 10000 (logs / "new.log").write_bytes(payload) parts = [] collector.finish_follow(parts.append) self.assertEqual(b"".join(parts), "\n[文件日志 new.log]\n".encode() + payload) self.assertEqual(collector.follow(parts.append), 0) def test_proxy_does_not_redirect_credentials_or_add_requests(self): seen = [] class Redirect(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): seen.append(self.path) self.send_response(302) self.send_header("Location", "/unexpected") self.end_headers() upstream = HTTPServer(("127.0.0.1", 0), Redirect) relay = BudgetRelay(("127.0.0.1", 0), f"http://127.0.0.1:{upstream.server_port}", "test-only-credential") threads = [threading.Thread(target=s.serve_forever, daemon=True) for s in (upstream, relay)] for thread in threads: thread.start() try: with self.assertRaises(urllib.error.HTTPError) as rejected: urllib.request.urlopen(f"http://127.0.0.1:{relay.server_port}/v1/models", timeout=5) self.assertEqual(rejected.exception.code, 302) self.assertEqual(seen, ["/v1/models"]) self.assertEqual(relay.gets, 1) finally: for server in (relay, upstream): server.shutdown() server.server_close() for thread in threads: thread.join() def test_only_changed_external_outputs_are_archived_and_originals_preserved(self): with tempfile.TemporaryDirectory() as temp: root = Path(temp) output, run = root / "original_logs", root / "run" output.mkdir() run.mkdir() (run / "work").mkdir() old = output / "old.json" old.write_text('{"old":true}') log = output / "current.log" log.write_bytes(b"previous\n") collector = Collector(Settings(root, []), run, [str(output)]) with log.open("ab") as handle: handle.write("新日志\n".encode()) messages = [] collector.follow(messages.append) collector.follow(messages.append) self.assertEqual(len(messages), 1) self.assertIn("新日志", messages[0].decode()) self.assertNotIn("previous", messages[0].decode()) (run / "work/result.json").write_text('{"ok":true}') items = collector.collect() self.assertEqual({i["name"] for i in items}, {"current.log", "result.json"}) self.assertEqual(old.read_text(), '{"old":true}') self.assertTrue(log.exists()) log.write_text("overwritten later") archived = next(i for i in items if i["name"] == "current.log") self.assertIn("新日志", (run / "artifacts" / archived["id"]).read_text(encoding="utf-8")) def test_model_proxy_budget_and_auth_are_per_run_without_retry(self): requests = [] class Upstream(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): requests.append((self.path, self.headers.get("Authorization"))) self.send_response(200) self.end_headers() self.wfile.write(b'{"data":[]}') def do_POST(self): self.rfile.read(int(self.headers["Content-Length"])) requests.append((self.path, self.headers.get("Authorization"))) self.send_response(503) self.end_headers() upstream = HTTPServer(("127.0.0.1", 0), Upstream) relay = BudgetRelay(("127.0.0.1", 0), f"http://127.0.0.1:{upstream.server_port}", "test-only-credential") threads = [threading.Thread(target=s.serve_forever, daemon=True) for s in (upstream, relay)] for t in threads: t.start() url = f"http://127.0.0.1:{relay.server_port}" try: with urllib.request.urlopen(url + "/v1/models", timeout=5) as response: self.assertEqual(response.status, 200) with self.assertRaises(urllib.error.HTTPError) as rejected: urllib.request.urlopen(url + "/v1/models", timeout=5) self.assertEqual(rejected.exception.code, 403) data = json.dumps({"model": "deepseek-v4.1-flash", "max_tokens": 512}).encode() for i in range(4): with self.assertRaises(urllib.error.HTTPError) as rejected: urllib.request.urlopen(url + "/v1/chat/completions", data, timeout=5) self.assertEqual(rejected.exception.code, 503 if i < 3 else 403) self.assertEqual(len(requests), 4) self.assertTrue(all(auth == "Bearer test-only-credential" for _, auth in requests)) self.assertNotIn("test-only-credential", json.dumps(relay.records)) finally: for server in (relay, upstream): server.shutdown() server.server_close() for thread in threads: thread.join()