feat: initialize XAS test console with native execution

This commit is contained in:
AdamWen95
2026-10-14 09:38:58 +08:00
commit 438844b01a
83 changed files with 9334 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
"""Console contracts: immutable uploads, isolation and durable queue semantics."""
import json
from pathlib import Path
import tempfile
import unittest
from fastapi.testclient import TestClient
from console.app import create_app
from console.settings import Settings
from console.store import Store
class ConsoleTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.source = self.root / "original" / "scripts"
self.source.mkdir(parents=True)
self.settings = Settings(self.root / "runtime", [
{"id": "trial", "path": str(self.source)}], allowed_hosts=["testserver"],
allowed_networks=["127.0.0.0/8"], test_mode=True)
self.settings.test_definitions = [{"key": name, "source_group": "trial", "entry": name,
"name": name, "kind": "test"} for name in ("sample.py", "test.py", "stop.sh")]
self.app = create_app(self.settings)
self.client = TestClient(self.app, client=("127.0.0.1", 5555))
self.headers = {"X-XAS-Console": "1"}
def tearDown(self):
self.client.close()
self.temp.cleanup()
def upload(self, content=b"print('hello')\n", name="test.py"):
r = self.client.post("/api/scripts/upload", files={"file": (name, content)},
data={"name": "Sample"}, headers=self.headers)
self.assertEqual(r.status_code, 200, r.text)
return r.json()
def queue(self, script, key="9ad0745c-0d5e-4d1e-88e8-000000000001"):
return self.client.post(f"/api/scripts/{script['id']}/runs",
json={"request_id": key}, headers=self.headers)
def test_upload_versions_are_immutable_and_runs_pin_version(self):
script = self.upload()
run = self.queue(script).json()
new = self.client.post(f"/api/scripts/{script['id']}/versions",
files={"file": ("test.py", b"print('v2')\n")}, headers=self.headers)
self.assertEqual(new.status_code, 200, new.text)
old = self.client.get(f"/api/scripts/{script['id']}/code?version=1").json()
self.assertEqual(old["code"], "print('hello')\n")
self.assertEqual(self.client.get(f"/api/runs/{run['id']}").json()["version"], 1)
self.assertEqual(new.json()["version"], 2)
def test_code_cannot_be_patched_and_path_cannot_be_replaced(self):
s = self.upload()
for patch in ({"code": "bad"}, {"source_path": "/etc/passwd"},
{"config": {"profile": "legacy_smoke"}}):
r = self.client.patch(f"/api/scripts/{s['id']}", json=patch, headers=self.headers)
self.assertEqual(r.status_code, 422, r.text)
def test_upload_rejects_traversal_secrets_and_oversize(self):
for name, content in [("../x.py", b"pass"), ("x.env", b"pass"),
("x.py", b"API_KEY = 'abc-secret-key-value-01234'"),
("x.py", b"x" * (1024 * 1024 + 1))]:
r = self.client.post("/api/scripts/upload", files={"file": (name, content)},
headers=self.headers)
self.assertIn(r.status_code, (400, 413, 422), r.text)
def test_cross_site_mutations_and_host_spoofing_rejected(self):
r = self.client.post("/api/discover", headers={**self.headers, "Origin": "https://evil.test"})
self.assertEqual(r.status_code, 403)
self.assertEqual(self.client.post("/api/discover").status_code, 403)
self.assertEqual(self.client.get("/api/summary", headers={"Host": "evil.test"}).status_code, 403)
def test_duplicate_submission_and_single_claim(self):
s = self.upload()
a, b = self.queue(s), self.queue(s)
self.assertEqual(a.json()["id"], b.json()["id"])
store = Store(self.settings)
self.assertEqual(store.claim()["id"], a.json()["id"])
self.assertIsNone(store.claim())
self.queue(s, "9ad0745c-0d5e-4d1e-88e8-000000000002")
self.assertIsNone(store.claim())
def test_pending_cancellation_does_not_execute(self):
run = self.queue(self.upload()).json()
r = self.client.post(f"/api/runs/{run['id']}/cancel", headers=self.headers)
self.assertEqual(r.json()["state"], "cancelled")
self.assertIsNone(Store(self.settings).claim())
def test_external_inventory_readonly_and_missing_detection(self):
p = self.source / "sample.py"
p.write_text("print('original')", encoding="utf-8")
self.client.post("/api/discover", headers=self.headers)
s = self.client.get("/api/scripts").json()["items"][0]
self.assertEqual(s["kind"], "external")
self.assertFalse(s["enabled"])
self.assertEqual(p.read_text(), "print('original')")
p.unlink()
self.client.post("/api/discover", headers=self.headers)
self.assertEqual(self.client.get(f"/api/scripts/{s['id']}").json()["status"], "missing")
def test_service_management_script_cannot_be_enabled(self):
(self.source / "stop.sh").write_text("docker stop other-service\n")
self.client.post("/api/discover", headers=self.headers)
s = self.client.get("/api/scripts").json()["items"][0]
r = self.client.patch(f"/api/scripts/{s['id']}", json={"enabled": True}, headers=self.headers)
self.assertEqual(r.status_code, 400, r.text)
def test_external_output_path_cannot_escape_source_family(self):
(self.source / "test.py").write_text("print(1)\n")
self.client.post("/api/discover", headers=self.headers)
s = self.client.get("/api/scripts").json()["items"][0]
for path in ["/etc", str(self.source), str(self.source.parent / "configs")]:
r = self.client.patch(f"/api/scripts/{s['id']}",
json={"config": {"output_paths": [path]}}, headers=self.headers)
self.assertEqual(r.status_code, 400, r.text)
def test_log_offset_unicode_and_artifact_path_boundary(self):
run = self.queue(self.upload()).json()
d = self.settings.runs / run["id"]
d.mkdir(exist_ok=True)
(d / "console.log").write_bytes("你好\n".encode("utf-8"))
a = self.client.get(f"/api/runs/{run['id']}/log?offset=0").json()
self.assertEqual(a["text"], "你好\n")
b = self.client.get(f"/api/runs/{run['id']}/log?offset={a['next_offset']}").json()
self.assertEqual(b["text"], "")
self.assertEqual(self.client.get(f"/api/runs/{run['id']}/artifacts/nope").status_code, 404)
def test_finished_log_utf8_character_across_chunk_boundary(self):
run = self.queue(self.upload()).json()
directory = self.settings.runs / run["id"]
directory.mkdir()
content = "x" * 65535 + "你" + "tail"
(directory / "console.log").write_bytes(content.encode("utf-8"))
Store(self.settings).finish(run["id"], "succeeded")
first = self.client.get(f"/api/runs/{run['id']}/log").json()
second = self.client.get(f"/api/runs/{run['id']}/log?offset={first['next_offset']}").json()
self.assertEqual(first["text"] + second["text"], content)
def test_runtime_parent_link_rejected_before_writes(self):
from deploy.dedicated import validate_runtime
outside = self.root / "outside"
outside.mkdir()
project = self.root / "project"
project.mkdir()
try:
(project / "runtime").symlink_to(outside, target_is_directory=True)
except OSError:
self.skipTest("OS account cannot create directory symlinks")
with self.assertRaises(ValueError):
validate_runtime(project)
with self.assertRaises(ValueError):
Settings(project / "runtime", []).prepare()
self.assertEqual(list(outside.iterdir()), [])
if __name__ == "__main__":
unittest.main()
+126
View File
@@ -0,0 +1,126 @@
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()
+48
View File
@@ -0,0 +1,48 @@
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from console.catalog import upload
from console.settings import Settings
from console.store import Store
@unittest.skipUnless(sys.platform == "linux", "worker dispatcher runs on Linux")
class WorkerFinalizationTests(unittest.TestCase):
def test_unmount_failure_changes_both_record_and_archived_summary(self):
from console.worker import Worker
with tempfile.TemporaryDirectory() as temp:
settings = Settings(Path(temp), [])
store = Store(settings)
script = upload(store, b"print('test')\n", "test.py")
run = store.queue(script, "test-request", "127.0.0.1")
store.claim()
worker = Worker(settings, {})
directory = settings.runs / run["id"]
real_popen = subprocess.Popen
def command(value, target):
(target / "work").mkdir()
(target / "work/result.txt").write_text("test output")
return ["create"], {}
def fake_docker(*args, **kwargs):
if args[0] == "inspect":
return json.dumps([{"Config": {"Labels": {"xas.console": "dedicated"}},
"State": {"ExitCode": 0, "Running": False}}])
return "owned-test-container"
def process(*args, **kwargs):
return real_popen([sys.executable, "-c", "import sys;sys.stdin.readline();print('output')"], **kwargs)
with patch.object(worker, "command", command), patch("console.worker.docker", fake_docker), \
patch("console.worker.subprocess.Popen", process), \
patch("console.worker.workspace.mounts", return_value=[directory / "work"]), \
patch("console.worker.workspace.remove", side_effect=OSError("simulated unmount failure")):
worker.execute(run)
result = store.run(run["id"])
self.assertEqual(result["state"], "interrupted")
summary = next(a for a in result["artifacts"] if a["name"] == "summary.json")
archived = json.loads((directory / "artifacts" / summary["id"]).read_text())
self.assertEqual(archived["state"], result["state"])
self.assertEqual(archived["message"], result["message"])
self.assertTrue(worker.stopping)
+82
View File
@@ -0,0 +1,82 @@
import importlib.util
from pathlib import Path
import tempfile
import unittest
from unittest import mock
import contextlib
import io
import json
import sys
from types import SimpleNamespace
MODULE = Path(__file__).resolve().parents[1] / "deploy/start.py"
spec = importlib.util.spec_from_file_location("start_console", MODULE)
start = importlib.util.module_from_spec(spec)
spec.loader.exec_module(start)
class DeploymentPathsTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.base = Path(self.temp.name)
self.project = self.base / "project"
self.project.mkdir()
self.outside = self.base / "other-owner"
self.outside.mkdir()
def validator(self):
self.assertTrue(hasattr(start, "validate_runtime"), "runtime boundary validation is missing")
return start.validate_runtime
def link(self, target, destination):
try:
destination.symlink_to(target, target_is_directory=True)
except OSError:
self.skipTest("OS account cannot create a directory symlink")
def test_runtime_parent_link_cannot_escape_project(self):
validate = self.validator()
self.link(self.outside, self.project / "runtime")
before = self.outside.stat().st_mode
with self.assertRaises(ValueError):
validate(self.project)
self.assertEqual(self.outside.stat().st_mode, before)
self.assertEqual(list(self.outside.iterdir()), [])
def test_runtime_child_link_cannot_escape_project(self):
validate = self.validator()
(self.project / "runtime").mkdir()
self.link(self.outside, self.project / "runtime/data")
with self.assertRaises(ValueError):
validate(self.project)
self.assertEqual(list(self.outside.iterdir()), [])
def test_normal_runtime_is_resolved_without_writing(self):
validate = self.validator()
self.assertEqual(validate(self.project), self.project.resolve() / "runtime")
self.assertEqual(list(self.project.iterdir()), [])
def test_backend_is_only_on_proxy_loopback_namespace(self):
# Regress the reviewed direct-container-IP role-header bypass at the Docker boundary.
deploy = self.project / 'deploy'
deploy.mkdir()
(deploy/'lan-settings.json').write_text(json.dumps({
'address':'10.88.118.188','allowed_cidrs':['10.88.0.0/17','10.20.0.0/21'],'port':14440}))
calls = []
def docker(args, **kwargs):
calls.append(args)
absent = args[:3] in (['docker','container','inspect'], ['docker','network','inspect'])
return SimpleNamespace(returncode=1 if absent else 0, stdout='')
with mock.patch.object(sys, 'argv', ['start.py','--project-root',str(self.project)]), \
mock.patch.object(sys, 'path', [str(MODULE.parent)] + sys.path), \
mock.patch.object(start.subprocess, 'run', side_effect=docker), \
mock.patch.object(start.os, 'chown', create=True), \
mock.patch.object(start.socket, 'socket'), contextlib.redirect_stdout(io.StringIO()):
start.main()
backend = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console')
proxy = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console-proxy')
self.assertIn('RUNDECK_SERVER_ADDRESS=127.0.0.1', backend)
self.assertEqual(backend[backend.index('--network')+1], 'container:adamwen-test-console-proxy')
self.assertNotIn('--publish', backend)
self.assertLess(calls.index(proxy), calls.index(backend))
+36
View File
@@ -0,0 +1,36 @@
import importlib.util
from pathlib import Path
import unittest
MODULE = Path(__file__).resolve().parents[1] / 'deploy/lan_config.py'
class LanConfigurationTests(unittest.TestCase):
def validator(self):
self.assertTrue(MODULE.is_file(), 'LAN boundary configuration is not implemented')
spec = importlib.util.spec_from_file_location('lan_config', MODULE)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.validate_settings
def test_explicit_private_interface_and_matching_subnet_are_accepted(self):
result = self.validator()({'address':'10.88.118.188','allowed_cidrs':['10.88.0.0/17','10.20.0.0/21'],'port':14440})
self.assertEqual(result['address'], '10.88.118.188')
self.assertEqual(result['allowed_cidrs'], ['10.88.0.0/17','10.20.0.0/21'])
def test_wildcard_public_loopback_and_nonmatching_network_are_rejected(self):
validate = self.validator()
for address, cidr in [('0.0.0.0','0.0.0.0/0'), ('8.8.8.8','8.8.8.0/24'),
('127.0.0.1','127.0.0.0/8'), ('10.88.118.188','10.89.0.0/17'),
('10.88.118.188','0.0.0.0/0'), ('10.88.118.188; admin','10.88.0.0/17')]:
with self.subTest(address=address,cidr=cidr), self.assertRaises(ValueError):
validate({'address':address,'allowed_cidrs':[cidr],'port':14440})
def test_invalid_port_and_extra_fields_are_rejected(self):
validate = self.validator()
base = {'address':'10.88.118.188','allowed_cidrs':['10.88.0.0/17','10.20.0.0/21'],'port':14440}
for port in (0, 80, 65536, True, '14440'):
with self.subTest(port=port), self.assertRaises(ValueError):
validate(dict(base, port=port))
with self.assertRaises(ValueError):
validate(dict(base, public=True))
+89
View File
@@ -0,0 +1,89 @@
import json
from pathlib import Path
from types import SimpleNamespace
import tempfile
import os
import unittest
from unittest.mock import patch
from console.native_executor import inspect, NativeExecutor
from console.native_io import SecretFilter
from console.artifacts import Collector
from console.settings import Settings
from console.store import Store
class LifecycleTests(unittest.TestCase):
def test_many_old_outputs_do_not_hide_new_native_log(self):
with tempfile.TemporaryDirectory() as temp:
root=Path(temp);logs=root/'logs';run=root/'run'
logs.mkdir();run.mkdir();(run/'work').mkdir()
for number in range(300):(logs/f'{number:04}.log').write_text('old history')
collector=Collector(Settings(root,[]),run,lambda:sorted(logs.iterdir()),file_limit=4096)
(logs/'new.log').write_text('current progress')
chunks=[];collector.finish_follow(chunks.append)
self.assertIn(b'current progress',b''.join(chunks))
self.assertNotIn(b'old history',b''.join(chunks))
self.assertEqual([a['name'] for a in collector.collect()],['new.log'])
@unittest.skipIf(os.name == "nt", "Linux controlling terminal")
def test_menu_has_controlling_terminal(self):
import pty
import subprocess
import sys
from console.native_job import acquire_terminal
master, slave = pty.openpty()
try:
result = subprocess.run([sys.executable, "-c", "import os; t=os.open('/dev/tty',os.O_RDWR); os.close(t)"],
stdin=slave, stdout=slave, stderr=slave, start_new_session=True, preexec_fn=acquire_terminal, timeout=5)
self.assertEqual(result.returncode, 0)
finally:
os.close(master);os.close(slave)
def test_systemd_probe_error_is_not_missing_unit(self):
result = SimpleNamespace(returncode=1, stdout="", stderr="D-Bus unavailable")
with patch("console.native_executor.subprocess.run", return_value=result):
with self.assertRaises(OSError):inspect("a" * 32)
result.stdout = "LoadState=not-found\n"
with patch("console.native_executor.subprocess.run", return_value=result):
self.assertIsNone(inspect("a" * 32))
def test_file_log_secret_split_at_chunk_boundary_is_filtered(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
logs, run = root / "logs", root / "run"
logs.mkdir();run.mkdir();(run / "work").mkdir()
secret = "dummy-fixture-credential-123456"
collector = Collector(Settings(root, []), run, [logs], stream_filter_factory=lambda: SecretFilter([secret]))
(logs / "x.log").write_bytes(b"x" * 32760 + secret.encode() + b"\n")
chunks=[];collector.finish_follow(chunks.append)
result=b"".join(chunks)
self.assertNotIn(secret.encode(),result)
self.assertIn(b"[REDACTED]",result)
def test_terminal_state_waits_for_confirmed_cleanup(self):
with tempfile.TemporaryDirectory() as temp:
settings=Settings(Path(temp),[]);store=Store(settings)
script={"id":"b"*32,"name":"fixture","version":1,"sha256":"x","config":{},"source_path":"fixture.py",
"kind":"external","source_group":"fixture","language":"python"}
run=store.queue(script,"fixture-request","127.0.0.1",execution={"mode":"native"})
store.claim()
directory=settings.runs/run['id'];directory.mkdir()
(directory/'native-result.json').write_text(json.dumps({'exit_code':0,'artifacts':[]}))
executor=NativeExecutor(SimpleNamespace(settings=settings,store=store,stopping=False))
with patch.object(executor,'wait_stopped',return_value=False):
executor.finish(run,{'Result':'success'})
self.assertEqual(store.run(run['id'])['state'],'running')
self.assertIsNone(store.claim())
def test_cancelled_before_dispatch_does_not_create_unit(self):
with tempfile.TemporaryDirectory() as temp:
settings=Settings(Path(temp),[]);store=Store(settings)
script={"id":"b"*32,"name":"fixture","version":1,"sha256":"x","config":{},"source_path":"fixture.py",
"kind":"external","source_group":"fixture","language":"python"}
run=store.queue(script,"fixture-request","127.0.0.1",execution={"mode":"native"})
store.claim();store.cancel(run['id'])
executor=NativeExecutor(SimpleNamespace(settings=settings,store=store,stopping=False))
with patch('console.native_executor.subprocess.run') as launch:
executor.execute(run)
launch.assert_not_called()
self.assertEqual(store.run(run['id'])['state'],'cancelled')
+89
View File
@@ -0,0 +1,89 @@
import hashlib
import json
from pathlib import Path
import sys
import tempfile
import unittest
from console.settings import Settings
from console.native_plans import build_plan, profile_for, validate_options, public_execution
from console.native_io import SecretFilter
class NativePlanTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
source = self.root / "original"
source.mkdir()
(source / "logs").mkdir()
self.entry = source / "test.py"
self.entry.write_text("print('original')\n")
self.settings = Settings(self.root / "runtime", [{"id": "original", "path": str(source)}])
self.profile = {"id": "test", "source_group": "original", "entry": "test.py", "cwd": str(source),
"interpreter": sys.executable, "user": "root", "arguments": ["{concurrency}", "{output_file}"],
"parameters": [{"key": "concurrency", "label": "并发", "type": "integer", "default": 2, "min": 1, "max": 8},
{"key": "configuration", "type": "select", "default": "known", "options": [{"value": "known", "label": "已登记配置"}]}],
"impact": {"level": "service", "title": "切换服务", "details": ["可能停止旧服务"], "requires_confirmation": True},
"environment": {}, "credential_files": [], "interactive": False, "output_paths": [str(source / "logs" / "console-{run_id}*")]}
self.settings.native_catalog = {"profiles": [self.profile], "targets": []}
self.script = {"id": "a" * 32, "kind": "external", "source_group": "original", "source_path": str(self.entry),
"sha256": hashlib.sha256(self.entry.read_bytes()).hexdigest(), "config": {"native_options": {}, "timeout_seconds": 300}, "language": "python"}
def tearDown(self):
self.temp.cleanup()
def test_plan_uses_original_file_and_typed_argv_without_shell_join(self):
before = self.entry.read_bytes()
plan = build_plan(self.settings, self.script, run_id="b" * 32, check_host=False)
self.assertEqual(plan["argv"][:2], [sys.executable, str(self.entry)])
self.assertEqual(plan["argv"][2], "2")
self.assertIn("console-" + "b" * 32, plan["argv"][3])
self.assertEqual(plan["cwd"], str(self.entry.parent))
self.assertEqual(self.entry.read_bytes(), before)
def test_unregistered_uploaded_and_replaced_entries_cannot_run_natively(self):
for change in ({"kind": "managed"}, {"source_path": str(self.root / "unknown.py")}, {"source_group": "other"}):
self.assertIsNone(profile_for(self.settings, {**self.script, **change}))
def test_parameters_reject_injection_unknown_keys_bool_and_out_of_range(self):
for value in ({"configuration": "../../etc/profile"}, {"configuration": "known;id"},
{"concurrency": True}, {"concurrency": 9}, {"command": "id"}):
with self.assertRaises(ValueError):
validate_options(self.profile, value)
def test_confirmation_changes_with_options_source_or_recipe(self):
first = build_plan(self.settings, self.script, check_host=False)["plan_token"]
self.script["config"]["native_options"] = {"concurrency": 3}
second = build_plan(self.settings, self.script, check_host=False)["plan_token"]
self.assertNotEqual(first, second)
self.profile["impact"]["details"].append("使用全部 GPU")
self.assertNotEqual(second, build_plan(self.settings, self.script, check_host=False)["plan_token"])
self.entry.write_text("print('changed')\n")
with self.assertRaises(ValueError):
build_plan(self.settings, self.script, check_host=False)
def test_credentials_not_in_public_execution(self):
self.profile["credential_files"] = [str(self.root / "private.env")]
visible = public_execution(self.settings, self.script)
self.assertEqual(visible["mode"], "native")
self.assertNotIn("credential_files", visible)
self.assertNotIn("private.env", json.dumps(visible))
def test_stream_filter_redacts_secret_split_across_reads(self):
secret = "dummy-credential-abcdefghijk"
filt = SecretFilter([secret])
output = filt.feed(b"hello\nkey=dummy-cred")
output += filt.feed(b"ential-abcdefghijk\nworld\n")
output += filt.finish()
self.assertNotIn(secret, output.decode())
self.assertIn("[REDACTED]", output.decode())
self.assertIn("world", output.decode())
def test_ansi_split_inside_secret_cannot_reconstruct_plaintext(self):
secret = "dummy-credential-abcdefghijk"
filt = SecretFilter([secret])
result = filt.feed(b"dummy-cred\x1b[")
result += filt.feed(b"31mential-abcdefghijk\x1b[0m\n")
result += filt.finish()
self.assertNotIn(secret.encode(), result)
self.assertIn(b"[REDACTED]", result)
+68
View File
@@ -0,0 +1,68 @@
import hashlib
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from console.settings import Settings
from console.store import Store
from console.native_plans import build_plan
from console.native_rpc import handle
class NativeRPCTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
source = root / "source"
source.mkdir()
entry = source / "test.py"
entry.write_text("print(1)\n")
self.settings = Settings(root / "runtime", [{"id": "original", "path": str(source)}])
self.settings.test_definitions = [{"key": "native", "source_group": "original", "entry": "test.py", "name": "原环境样例"}]
self.settings.native_catalog = {"targets": [], "profiles": [{"id": "native", "source_group": "original", "entry": "test.py",
"cwd": str(source), "user": "root", "interpreter": "/usr/bin/python3", "parameters": [], "arguments": [],
"impact": {"level": "service", "title": "服务切换", "details": ["会停止指定服务"], "requires_confirmation": True}}]}
self.script = {"id": "a" * 32, "name": "test.py", "description": "", "kind": "external", "source_group": "original",
"source_path": str(entry), "sha256": hashlib.sha256(entry.read_bytes()).hexdigest(), "language": "python", "version": 1,
"enabled": True, "status": "ready", "config": {"native_options": {}, "timeout_seconds": 300}}
self.store = Store(self.settings)
self.store.save_script(self.script)
self.plan = build_plan(self.settings, self.script, check_host=False)
self.request = {"op": "queue", "script_id": self.script["id"], "request_id": "5b0202b7-851d-48ad-a7b9-dce22f000001",
"plan_token": self.plan["plan_token"], "source_ip": "127.0.0.1"}
def tearDown(self):
self.temp.cleanup()
def test_server_requires_explicit_current_confirmation(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
with self.assertRaises(ValueError):handle(self.settings, self.store, self.request)
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True, "plan_token": "old"})
self.assertEqual(self.store.runs(), [])
run = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
self.assertTrue(run["execution"]["impact_acknowledged"])
self.assertEqual(run["execution"]["mode"], "native")
def test_retry_returns_original_even_if_plan_later_changed(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
first = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with patch("console.native_rpc.native_plans.build_plan", side_effect=ValueError("changed")):
again = handle(self.settings, self.store, self.request)
self.assertEqual(first["id"], again["id"])
self.assertEqual(len(self.store.runs()), 1)
def test_failed_preflight_and_unknown_operation_never_queue(self):
with patch("console.native_rpc.native_plans.build_plan", return_value={**self.plan, "ready": False}):
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "exec", "command": "id"})
self.assertEqual(self.store.runs(), [])
def test_input_rejected_for_queued_or_noninteractive_run(self):
run = self.store.queue(self.script, "sample", "127.0.0.1", execution={"mode": "native", "interactive": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": run["id"], "text": "1"})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": "../../bad", "text": "1"})
def test_input_channel_rejects_tmux_keys_and_shell_commands(self):
for text in ("\x02c", "\x1b", "bash", "1\nid", "echo test", ":new-window"):
with self.assertRaisesRegex(ValueError, "控制键"):
handle(self.settings, self.store, {"op": "input", "run_id": "a"*32, "text": text})
+102
View File
@@ -0,0 +1,102 @@
"""Behavioral acceptance; Linux process tests also run on the target host."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import unittest
ROOT = Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "run_smoke.py"
class RunnerTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.results = Path(self.temp.name)
def command(self, mode="success", run_id="test-1", timeout="3"):
self.assertTrue(RUNNER.is_file(), "run_smoke.py is not implemented")
return [sys.executable, "-u", str(RUNNER), "--mode", mode,
"--run-id", run_id, "--timeout", timeout,
"--results-root", str(self.results)]
def run_task(self, **kwargs):
return subprocess.run(self.command(**kwargs), capture_output=True,
text=True, timeout=12)
def summary(self, run_id="test-1"):
return json.loads((self.results / run_id / "summary.json").read_text())
def test_success_streams_progress_and_records_synthetic_result(self):
result = self.run_task()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
summary = self.summary()
self.assertEqual(summary["status"], "succeeded")
self.assertEqual(summary["run_id"], "test-1")
self.assertTrue(summary["synthetic"])
self.assertEqual(summary["result"]["checks_passed"], 3)
self.assertIn("progress=1/3", result.stdout)
self.assertIn("progress=3/3", result.stdout)
self.assertEqual(len(summary["source_sha256"]["smoke_task.py"]), 64)
def test_nonzero_exit_is_recorded_as_failure(self):
result = self.run_task(mode="failure")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.summary()["status"], "failed")
self.assertEqual(self.summary()["exit_code"], 23)
def test_zero_exit_with_invalid_report_is_failure(self):
result = self.run_task(mode="invalid-report")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.summary()["status"], "invalid_result")
def test_reused_run_id_preserves_previous_result(self):
self.assertEqual(self.run_task().returncode, 0)
before = (self.results / "test-1" / "summary.json").read_bytes()
self.assertNotEqual(self.run_task(mode="failure").returncode, 0)
self.assertEqual((self.results / "test-1" / "summary.json").read_bytes(), before)
def test_invalid_inputs_do_not_create_run_directory(self):
for kwargs in ({"run_id": "../escape"}, {"mode": "arbitrary"},
{"timeout": "0"}, {"timeout": "31"}, {"timeout": "nan"}):
with self.subTest(kwargs=kwargs):
self.assertNotEqual(self.run_task(**kwargs).returncode, 0)
self.assertEqual(list(self.results.iterdir()), [])
@unittest.skipUnless(sys.platform == "linux", "requires Linux process groups")
def test_timeout_terminates_child_and_records_timeout(self):
result = self.run_task(mode="wait", timeout="2")
self.assertEqual(result.returncode, 124, result.stdout + result.stderr)
summary = self.summary()
self.assertEqual(summary["status"], "timed_out")
self.assertFalse(Path(f'/proc/{summary["child_pid"]}').exists())
@unittest.skipUnless(sys.platform == "linux", "requires Linux process groups")
def test_cancel_terminates_parent_and_child(self):
proc = subprocess.Popen(self.command(mode="wait", timeout="15"),
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
deadline = time.monotonic() + 4
child_file = self.results / "test-1" / "child.pid"
while not child_file.exists() and time.monotonic() < deadline:
time.sleep(.05)
self.assertTrue(child_file.exists(), "worker did not start")
child_pid = int(child_file.read_text())
os.kill(proc.pid, signal.SIGTERM)
out, err = proc.communicate(timeout=4)
self.assertEqual(proc.returncode, 130, out + err)
self.assertEqual(self.summary()["status"], "cancelled")
self.assertFalse(Path(f"/proc/{child_pid}").exists())
finally:
if proc.poll() is None:
proc.kill()
proc.communicate()
if __name__ == "__main__":
unittest.main()
+103
View File
@@ -0,0 +1,103 @@
"""A test entry represents behavior, not every source file under a directory."""
from pathlib import Path
import tempfile
import unittest
from fastapi.testclient import TestClient
from console.app import create_app
from console.settings import Settings
from console.store import Store
class TestCatalogTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
self.source = root / "scripts"
self.source.mkdir()
(self.source / "run-suite.sh").write_text("python3 helper.py\n")
(self.source / "helper.py").write_text("print('helper')\n")
(self.source / "tests").mkdir()
(self.source / "tests/test_internal.py").write_text("assert True\n")
self.settings = Settings(root / "runtime", [{"id": "suite", "path": str(self.source)}],
allowed_hosts=["testserver"], allowed_networks=["127.0.0.0/8"])
self.settings.test_definitions = [{"key": "suite", "source_group": "suite", "entry": "run-suite.sh",
"name": "模型综合测试", "description": "一轮完整验证", "kind": "suite",
"members": [{"source_group": "suite", "pattern": "**/*"}]}]
self.client = TestClient(create_app(self.settings), client=("127.0.0.1", 5555))
self.headers = {"X-XAS-Console": "1"}
self.client.post("/api/discover", headers=self.headers)
self.store = Store(self.settings)
def tearDown(self):
self.client.close()
self.temp.cleanup()
def test_suite_is_one_item_with_readonly_members_not_three_cards(self):
items = self.client.get("/api/scripts").json()["items"]
self.assertEqual(len(items), 1)
self.assertEqual(items[0]["name"], "模型综合测试")
self.assertEqual(items[0]["test_kind"], "suite")
self.assertEqual(items[0]["file_count"], 3)
files = self.client.get(f"/api/scripts/{items[0]['id']}/files").json()["items"]
self.assertEqual(len(files), 3)
self.assertEqual(sum(f["role"] == "entry" for f in files), 1)
summary = self.client.get("/api/summary").json()
self.assertEqual(summary["scripts"], 1)
self.assertEqual(summary["source_files"], 3)
def test_refresh_does_not_promote_new_support_file_or_clear_configuration(self):
entry = next(s for s in self.store.scripts() if s["source_path"].endswith("run-suite.sh"))
self.client.patch(f"/api/scripts/{entry['id']}", json={"name": "我的综合测试"}, headers=self.headers)
(self.source / "new_module.py").write_text("pass\n")
self.client.post("/api/discover", headers=self.headers)
items = self.client.get("/api/scripts").json()["items"]
self.assertEqual(len(items), 1)
self.assertEqual(items[0]["name"], "我的综合测试")
self.assertEqual(items[0]["file_count"], 4)
def test_member_cannot_be_enabled_or_run_by_direct_api(self):
helper = next(s for s in self.store.scripts() if s["source_path"].endswith("helper.py"))
self.assertFalse(self.client.get(f"/api/scripts/{helper['id']}").json()["is_test"])
enabled = self.client.patch(f"/api/scripts/{helper['id']}", json={"enabled": True}, headers=self.headers)
self.assertEqual(enabled.status_code, 400)
# Even a pre-existing enabled inventory record cannot bypass the entry boundary.
helper.update(enabled=True, status="ready")
self.store.save_script(helper)
run = self.client.post(f"/api/scripts/{helper['id']}/runs",
json={"request_id": "caa3943d-6872-472c-86c2-500000000001"}, headers=self.headers)
self.assertEqual(run.status_code, 400)
self.assertEqual(self.store.runs(), [])
def test_upload_remains_one_test_and_examples_are_opt_in(self):
uploaded = self.client.post("/api/scripts/upload", headers=self.headers,
files={"file": ("test.py", b"print('custom')\n")}, data={"name": "用户测试"}).json()
example = self.client.post("/api/scripts/upload", headers=self.headers,
files={"file": ("demo.py", b"print('demo')\n")}, data={"name": "演示"}).json()
item = self.store.script(example["id"])
item["synthetic"] = True
self.store.save_script(item)
regular = self.client.get("/api/scripts").json()["items"]
self.assertEqual({s["name"] for s in regular}, {"模型综合测试", "用户测试"})
all_items = self.client.get("/api/scripts?include_examples=true").json()["items"]
self.assertEqual(len(all_items), 3)
self.assertTrue(next(s for s in all_items if s["id"] == uploaded["id"])["is_test"])
def test_historical_runs_keep_ids_and_source_contents_unchanged(self):
entry = next(s for s in self.store.scripts() if s["source_path"].endswith("run-suite.sh"))
historical = self.store.queue(entry, "past-request", "127.0.0.1")
before = (self.source / "run-suite.sh").read_bytes()
self.client.get("/api/scripts")
self.client.post("/api/discover", headers=self.headers)
self.assertEqual(self.store.run(historical["id"])["script_name"], "run-suite.sh")
self.assertEqual(self.store.run(historical["id"])["script_id"], entry["id"])
self.assertEqual(before, (self.source / "run-suite.sh").read_bytes())
def test_new_run_uses_one_entry_and_the_test_name(self):
entry = self.client.get("/api/scripts").json()["items"][0]
response = self.client.patch(f"/api/scripts/{entry['id']}", json={"enabled": True}, headers=self.headers)
self.assertEqual(response.status_code, 200)
run = self.client.post(f"/api/scripts/{entry['id']}/runs", headers=self.headers,
json={"request_id": "caa3943d-6872-472c-86c2-500000000002"}).json()
self.assertEqual(run["script_name"], "模型综合测试")
self.assertEqual(run["source_path"], str(self.source / "run-suite.sh"))
self.assertEqual(len(self.store.runs()), 1)