zcbot/tests/test_windows_node_source.py

554 lines
30 KiB
Python

from __future__ import annotations
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] / "windows-node"
PROJECT = ROOT / "Zcbot.WindowsNode"
class WindowsNodeSourceTests(unittest.TestCase):
def test_project_targets_net10_windows_forms_with_json_schema_validator(self) -> None:
tree = ET.parse(PROJECT / "Zcbot.WindowsNode.csproj")
root = tree.getroot()
self.assertEqual(root.findtext("./PropertyGroup/TargetFramework"), "net10.0-windows")
self.assertEqual(root.findtext("./PropertyGroup/UseWindowsForms"), "true")
self.assertEqual(root.findtext("./PropertyGroup/OutputType"), "WinExe")
packages = root.findall("./ItemGroup/PackageReference")
self.assertEqual([item.attrib["Include"] for item in packages], ["JsonSchema.Net"])
def test_node_protocol_and_secret_storage_markers_are_present(self) -> None:
source = "\n".join(path.read_text(encoding="utf-8") for path in PROJECT.glob("*.cs"))
for marker in (
"v1/software-nodes/enroll",
"v1/software-nodes/connect",
'SetRequestHeader("Authorization"',
'SetRequestHeader("X-Node-Id"',
"DataProtectionScope.LocalMachine",
"SetAccessRuleProtection(isProtected: true",
'"adapter.json"',
"NotifyIcon",
"ConfigurationForm",
"TrayIconFactory.Create",
'"--headless"',
):
self.assertIn(marker, source)
def test_workspace_storage_is_global_by_workspace_not_user_directory(self) -> None:
models = (PROJECT / "NodeModels.cs").read_text(encoding="utf-8")
store = (PROJECT / "WorkspaceStore.cs").read_text(encoding="utf-8")
runner = (PROJECT / "AdapterProcessRunner.cs").read_text(encoding="utf-8")
self.assertIn('Path.Combine(root, "workspaces")', models)
self.assertIn('Binding(job).WorkspaceId.ToString("D")', store)
self.assertNotIn("UserId", store)
self.assertIn('Path.Combine(root, "current")', store)
self.assertIn('Path.Combine(root, "rollback")', store)
self.assertIn("workspaceStore.Promote", runner)
self.assertIn("workspaceStore.Restore", runner)
def test_runtime_auto_reports_all_discovered_adapter_capabilities(self) -> None:
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
config_store = (PROJECT / "NodeConfigStore.cs").read_text(encoding="utf-8")
self.assertIn(
"var installedCapabilities = adapters.All.Select(item => item.Capability).ToArray()",
connection,
)
self.assertIn("capabilities = installedCapabilities", connection)
self.assertIn("installed_capabilities = installedCapabilities", connection)
self.assertNotIn("config.Capabilities.Contains", connection)
self.assertNotIn("InstalledCapabilities.Contains", config_store)
def test_node_does_not_expose_arbitrary_execution_primitives(self) -> None:
source = "\n".join(
path.read_text(encoding="utf-8")
for path in PROJECT.glob("*.cs")
if path.name not in {
"AdapterProcessRunner.cs",
"ConfigurationForm.cs",
"SoftwareRuntimeManager.cs",
}
)
for forbidden in ("Process.Start", "cmd.exe", "powershell.exe", "LabTalk"):
self.assertNotIn(forbidden, source)
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
self.assertIn('"explorer.exe"', form)
self.assertIn("startInfo.ArgumentList.Add(dataRoot.Text)", form)
self.assertNotIn("UseShellExecute = true", form)
def test_data_root_is_configurable_and_migrated_before_switching(self) -> None:
settings = (PROJECT / "NodeDataRoot.cs").read_text(encoding="utf-8")
models = (PROJECT / "NodeModels.cs").read_text(encoding="utf-8")
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
tray = (PROJECT / "TrayApplicationContext.cs").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
self.assertIn('"ZCBOT_WINDOWS_NODE_DATA_DIR"', settings)
self.assertIn("Environment.SpecialFolder.LocalApplicationData", settings)
self.assertIn("Environment.SpecialFolder.CommonApplicationData", settings)
self.assertIn('Directory.Exists(legacy)', settings)
self.assertLess(
settings.index("Environment.GetEnvironmentVariable"),
settings.index("Registry.CurrentUser.OpenSubKey"),
)
self.assertIn("NodeDataRootSettings.Resolve().RootDirectory", models)
self.assertIn("SHA256.HashData(stream)", settings)
self.assertIn("Directory.Move(staging, target)", settings)
self.assertLess(
settings.index("VerifyTree(source, staging"),
settings.index("Directory.Move(staging, target)"),
)
self.assertIn("DataRootMigrationRequested", form)
self.assertIn("jobInbox.HasPendingJobs", form)
self.assertIn(
"NodeDataRootSettings.SaveUserRoot(result.TargetDirectory)", tray
)
self.assertIn("jobPipelines.Values.Concat(exportPipelines.Values)", connection)
self.assertIn(
"Path.GetPathRoot(NodePaths.ForCurrentMachine().RootDirectory)", connection
)
def test_config_field_names_do_not_serialize_plain_node_token(self) -> None:
models = (PROJECT / "NodeModels.cs").read_text(encoding="utf-8")
stored_record = models.split("internal sealed record StoredNodeConfig", 1)[1].split(");", 1)[0]
self.assertIn("ProtectedNodeToken", stored_record)
self.assertNotIn("string NodeToken", stored_record)
def test_configuration_ui_never_displays_or_copies_token(self) -> None:
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
self.assertIn("CreateTextBox(usePassword: true)", form)
self.assertIn("UseSystemPasswordChar = usePassword", form)
self.assertNotIn("NodeToken", form)
self.assertIn("Clipboard.SetText(BuildDiagnosticText(currentConfig))", form)
diagnostics = form.split("private string BuildDiagnosticText", 1)[1].split(
"private async Task RunAnsysAcceptanceAsync", 1
)[0]
self.assertNotIn("NodeToken", diagnostics)
self.assertIn("内容不包含 Node Token", form)
self.assertIn("清除本机身份", form)
self.assertIn("管理后台删除或禁用云端旧节点", form)
def test_ansys_acceptance_ui_runs_only_fixed_bundled_suite_before_gate(self) -> None:
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
adapters = (PROJECT / "NodeAdapters.cs").read_text(encoding="utf-8")
runner = (PROJECT / "AdapterProcessRunner.cs").read_text(encoding="utf-8")
self.assertIn("ANSYS Mechanical 真机验收", form)
self.assertIn("运行内置基准验收", form)
self.assertIn("AcceptanceReportPassed", form)
self.assertIn('"ZCBOT_ANSYS_242_VALIDATED", "1"', form)
self.assertIn("EnvironmentVariableTarget.Machine", form)
self.assertIn("SupportsLocalAcceptance", adapters)
self.assertIn('Path.Combine(descriptor.DirectoryPath, "acceptance.py")', runner)
self.assertIn(
'Path.Combine(descriptor.DirectoryPath, "acceptance-coupon.step")', runner
)
self.assertIn('["--work-root", root, "--repeat", "20"', runner)
self.assertIn("Adapter acceptance cannot run while a dispatched job is active", runner)
acceptance = form.split("private async Task RunAnsysAcceptanceAsync", 1)[1].split(
"private void EnableAnsysGate", 1
)[0]
self.assertNotIn("new OpenFileDialog", acceptance)
self.assertNotIn("Process.Start", acceptance)
def test_professional_software_ui_uses_fixed_per_runtime_installers(self) -> None:
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
manager = (PROJECT / "SoftwareRuntimeManager.cs").read_text(encoding="utf-8")
runner = (PROJECT / "AdapterProcessRunner.cs").read_text(encoding="utf-8")
self.assertIn('CreateSectionTitle("专业软件")', form)
self.assertIn('CreateButton("自动检测", 92)', form)
self.assertIn('CreateButton("选择位置", 92)', form)
self.assertIn('CreateButton("安装 / 更新环境", 150', form)
self.assertIn("panel.SetColumnSpan(path, 2)", form)
self.assertIn("path.ReadOnly = true", form)
software_panel = form.split("private Control CreateSoftwarePanel", 1)[1].split(
"private void RefreshSoftwareCards", 1
)[0]
self.assertIn("panel.SetColumnSpan(status, 2)", software_panel)
self.assertIn("panel.SetColumnSpan(actions, 2)", software_panel)
self.assertIn("SoftwareRuntimeManager.InstallRuntimeAsync", form)
self.assertIn("item.HasActiveJobs", form)
for runtime_id in ('"origin"', '"ansys"', '"blender"'):
self.assertIn(runtime_id, manager)
self.assertIn('@"Software\\Zcbot\\WindowsNode\\Software"', manager)
self.assertIn("RegistryHive.LocalMachine", manager)
self.assertIn("RegistryHive.CurrentUser", manager)
self.assertIn('"ZCBOT_BLENDER_EXE"', manager)
self.assertIn('"AWP_ROOT242"', manager)
self.assertIn('"ZCBOT_ORIGIN_EXE"', manager)
self.assertIn('"Origin*.exe"', manager)
self.assertIn("OriginExecutables", manager)
self.assertIn('Path.Combine(runtimes, definition.RuntimeId)', manager)
self.assertIn("ArgumentList.Add(argument)", manager)
self.assertNotIn('"cmd.exe"', manager.lower())
self.assertNotIn('"powershell.exe"', manager.lower())
self.assertIn("SoftwareRuntimeManager.ApplyProcessEnvironment", runner)
def test_configuration_window_is_resizable_and_dpi_safe(self) -> None:
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
self.assertIn("ClientSize = new Size(1080, 720)", form)
self.assertIn("MinimumSize = new Size(760, 560)", form)
self.assertIn("FormBorderStyle.Sizable", form)
self.assertIn("AutoScaleMode.Dpi", form)
self.assertIn("AutoScroll = true", form)
self.assertNotIn("MaximumSize = new Size(410", form)
self.assertIn("注册码默认 10 分钟有效", form)
self.assertIn("成功注册一次后立即失效", form)
self.assertIn("CreateSection", form)
self.assertIn("注册并连接", form)
self.assertNotIn("ContentWidth", form)
self.assertIn("new Padding(24, 20, 24, 20)", form)
self.assertIn("new ColumnStyle(SizeType.Percent, 100)", form)
self.assertIn('AddTab(tabs, "节点概览")', form)
self.assertIn('AddTab(tabs, "专业软件")', form)
self.assertIn('AddTab(tabs, "本机任务")', form)
self.assertIn('AddTab(tabs, "运行设置")', form)
self.assertIn("SizeMode = TabSizeMode.FillToRight", form)
self.assertIn("new NavigationTabControl", form)
self.assertIn("DrawMode = TabDrawMode.OwnerDrawFixed", form)
self.assertIn("Appearance = TabAppearance.FlatButtons", form)
self.assertNotIn("protected override void OnResize", form)
self.assertIn("tab.Controls.Add(content)", form)
self.assertIn("CreateSubsectionTitle(\"能力概览\")", form)
self.assertIn("panel.SetColumnSpan(path, 2)", form)
self.assertIn("panel.SetColumnSpan(status, 2)", form)
self.assertIn("panel.SetColumnSpan(actions, 2)", form)
self.assertIn("BorderedTableLayoutPanel", form)
self.assertIn(
'runtime.Software == "OriginPro" ? "Origin" : runtime.Software', form
)
self.assertIn("FormatCapabilitySummary(adapters.All)", form)
self.assertIn("GroupBy(entry => entry.SoftwareName", form)
self.assertIn("GroupBy(entry => entry.CapabilityName", form)
self.assertIn("ShortCapabilityName(adapter.DisplayName, softwareName)", form)
self.assertIn('CreateButton("立即重连", 112, primary: true)', form)
self.assertIn("ReconnectRequested?.Invoke()", form)
self.assertIn("registrationCard.Visible = !registered", form)
self.assertIn("reconnect.Visible = registered", form)
self.assertIn("resetIdentity.Visible = registered", form)
self.assertIn('CreateSectionTitle("本机任务")', form)
self.assertIn("DataGridView", form)
self.assertIn("jobInbox.ReadJobSnapshots()", form)
self.assertIn('"software_running" => "软件执行中"', form)
self.assertNotIn("HttpClient", form)
tray = (PROJECT / "TrayApplicationContext.cs").read_text(encoding="utf-8")
self.assertIn("form.ReconnectRequested += RestartConnection", tray)
def test_tray_exit_waits_for_or_cancels_active_jobs(self) -> None:
tray = (PROJECT / "TrayApplicationContext.cs").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
self.assertIn("private async Task ExitNodeAsync()", tray)
self.assertIn("if (exiting || config is null", tray)
self.assertIn("ReadJobSnapshots()", tray)
self.assertIn("MessageBoxButtons.YesNoCancel", tray)
self.assertIn("connectionLoop?.CancelActiveJobsForExit()", tray)
self.assertIn("tray.ContextMenuStrip.Enabled = false", tray)
self.assertLess(tray.index("await connectionTask"), tray.index("ExitThread();"))
self.assertIn("internal void CancelActiveJobsForExit()", connection)
self.assertIn("adapters.Find(job.Capability)?.Cancel(job.JobId)", connection)
self.assertIn("inputDownloader.DownloadAsync(job, forcedStop.Token)", connection)
self.assertIn('"NODE_EXIT_CANCELLED"', connection)
self.assertIn("catch (OperationCanceledException) when (forcedStop.IsCancellationRequested)", connection)
def test_local_job_monitor_is_persisted_and_software_neutral(self) -> None:
inbox = (PROJECT / "JobInboxStore.cs").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
models = (PROJECT / "JobMonitorModels.cs").read_text(encoding="utf-8")
self.assertIn('"state.json"', inbox)
self.assertIn("ReadJobSnapshots", inbox)
self.assertIn("AtomicWrite(path, content, overwrite: true)", inbox)
for stage in (
"accepted",
"downloading_inputs",
"ready_to_run",
"software_running",
"uploading_outputs",
"succeeded",
"failed",
"cancelled",
):
self.assertIn(f'"{stage}"', inbox + connection)
self.assertIn("JobDisplaySnapshot", models)
self.assertNotIn("origin_running", inbox + connection + models)
def test_startup_and_runtime_installation_are_owned_by_the_ui(self) -> None:
startup = (PROJECT / "StartupRegistration.cs").read_text(encoding="utf-8")
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
manager = (PROJECT / "SoftwareRuntimeManager.cs").read_text(encoding="utf-8")
self.assertFalse((ROOT / "install-windows-node.bat").exists())
self.assertIn('Text = "登录 Windows 后自动启动节点"', form)
self.assertIn("StartupRegistration.SetEnabled", form)
self.assertIn('@"Software\\Microsoft\\Windows\\CurrentVersion\\Run"', startup)
self.assertIn("Environment.ProcessPath", startup)
self.assertIn("FindPython312Async", manager)
self.assertIn('new BootstrapPython("py.exe", ["-3.12"])', manager)
self.assertIn('new BootstrapPython("python.exe", [])', manager)
self.assertIn('"ZCBOT_PIP_INDEX_URL"', manager)
self.assertIn(
'"https://pypi.tuna.tsinghua.edu.cn/simple/"', manager
)
def test_publish_output_contains_the_complete_node_payload(self) -> None:
project = (PROJECT / "Zcbot.WindowsNode.csproj").read_text(encoding="utf-8")
self.assertNotIn("install-windows-node.bat", project)
self.assertIn("..\\adapters\\**\\*", project)
self.assertIn("adapters\\%(RecursiveDir)%(Filename)%(Extension)", project)
self.assertIn("..\\..\\software-contracts\\origin.plot.v2.json", project)
self.assertIn("adapters\\origin.plot@v2\\origin.plot.v2.json", project)
self.assertIn(
"..\\..\\software-contracts\\ansys.geometry.inspect.v1.json",
project,
)
self.assertIn(
"adapters\\ansys.geometry.inspect@v1\\ansys.geometry.inspect.v1.json",
project,
)
self.assertIn(
"..\\..\\software-contracts\\ansys.mechanical.static_structural.v2.json",
project,
)
self.assertIn(
"adapters\\ansys.mechanical.static_structural@v2\\"
"ansys.mechanical.static_structural.v2.json",
project,
)
self.assertIn(
"..\\..\\software-contracts\\blender.scene.author.v2.json",
project,
)
self.assertIn(
"adapters\\blender.scene.author@v2\\blender.scene.author.v2.json",
project,
)
self.assertNotIn("software-contracts\\%(Filename)%(Extension)", project)
self.assertFalse((ROOT / "install-windows-node.ps1").exists())
self.assertFalse((ROOT / "install-origin-runtime.ps1").exists())
self.assertFalse((ROOT / "install-startup.ps1").exists())
def test_packager_defaults_to_framework_dependent_verified_zip(self) -> None:
script = (ROOT / "package-windows-node.bat").read_text(encoding="utf-8")
self.assertNotIn("powershell", script.lower())
self.assertIn('set "SELF_CONTAINED=false"', script)
self.assertIn('if /I "%~1"=="--self-contained"', script)
self.assertIn("dotnet publish", script)
self.assertNotIn('"install-windows-node.bat"', script)
self.assertIn("start Zcbot.WindowsNode.exe", script)
self.assertIn('for /r "%PUBLISH_DIR%\\adapters" %%F in (adapter.json)', script)
self.assertIn('do if exist "%%~fF"', script)
self.assertIn("ADAPTER_CONTRACT_COUNT", script)
self.assertIn("Published adapter has no colocated contract", script)
self.assertNotIn('"!PUBLISH_DIR!\\software-contracts\\*.json"', script)
self.assertIn("tar.exe -a -c -f", script)
self.assertIn("certutil.exe -hashfile", script)
self.assertFalse((ROOT / "package-windows-node.ps1").exists())
def test_auth_rejection_is_distinct_from_http_websocket_handshake_failure(self) -> None:
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
self.assertIn('socket.HttpStatusCode is HttpStatusCode.Unauthorized', connection)
self.assertIn('or HttpStatusCode.Forbidden', connection)
self.assertIn("WebSocket 握手被拒绝,请检查服务端或反向代理", connection)
self.assertIn("catch (NodeEndpointException exception)", connection)
self.assertIn("throw new NodeEndpointException", connection)
self.assertIn("(int?)result.CloseStatus == 4003", connection)
self.assertIn("节点身份已被服务端拒绝", connection)
self.assertNotIn("Node credentials were rejected", connection)
def test_adapter_runtime_probe_is_worker_owned_and_reported(self) -> None:
runner = (PROJECT / "AdapterProcessRunner.cs").read_text(encoding="utf-8")
worker = (
ROOT / "adapters" / "origin.plot@v2" / "worker.py"
).read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
self.assertIn('r"Origin.ApplicationSI\\CLSID"', worker)
self.assertIn('["--probe"]', runner)
self.assertIn('root.GetProperty("adapter_version")', runner)
self.assertIn('startInfo.Environment["PYTHONUTF8"] = "1"', runner)
self.assertIn('startInfo.Environment["PYTHONIOENCODING"] = "utf-8"', runner)
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
self.assertIn('AppendLine($"Adapter: {adapter.AdapterVersion}")', form)
self.assertIn('$"{group.Key} · 版本未知"', form)
self.assertIn(
"&& !jobInbox.HasPendingJobs",
connection,
)
self.assertIn("originAdapter?.HasActiveJobs", connection)
self.assertIn(
"ReadRecoverableJobs().Any(item => item.Terminal is null)",
(PROJECT / "JobInboxStore.cs").read_text(encoding="utf-8"),
)
self.assertNotIn("CreateInstance", worker)
for marker in (
"software_version = origin.SoftwareVersion",
"adapter_version = origin.AdapterVersion",
"health = origin.Health",
"detail = origin.Detail",
):
self.assertIn(marker, connection)
def test_job_offer_is_persisted_before_acceptance(self) -> None:
inbox = (PROJECT / "JobInboxStore.cs").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
adapters = (PROJECT / "NodeAdapters.cs").read_text(encoding="utf-8")
self.assertIn("adapters.Find(capability)", inbox)
self.assertIn("adapter.ValidateRequest(request)", inbox)
self.assertIn(
'JsonSchema.Build(contractRoot.GetProperty("request_schema").Clone())',
adapters,
)
self.assertIn("var contractPath = ResolveFile(root, manifest.Contract)", adapters)
self.assertNotIn("ResolveContractFile", adapters)
self.assertNotIn('AppContext.BaseDirectory, "software-contracts"', adapters)
self.assertNotIn("IsValidOriginRequest", inbox)
self.assertNotIn("IsValidPlot", inbox)
self.assertIn('root.TryGetProperty("input_transfers"', inbox)
self.assertIn("transfers.GetArrayLength() > 16", inbox)
self.assertNotIn("transfers.GetArrayLength() is < 1 or > 16", inbox)
self.assertIn('"input", key, filename', inbox)
self.assertIn("FileOptions.WriteThrough", inbox)
self.assertIn("stream.Flush(flushToDisk: true)", inbox)
new_record = inbox.split("var record =", 1)[1].split("private static JsonElement?", 1)[0]
self.assertLess(
new_record.index("AtomicWrite(requestPath, record"),
new_record.index("JobOfferResult.Accept"),
)
self.assertIn('offerResult.Accepted ? "job_accept" : "job_reject"', connection)
self.assertIn("sendLock.WaitAsync", connection)
self.assertIn("!jobInbox.HasPendingJobs", connection)
self.assertIn("capability_runtime = capabilityRuntime", connection)
self.assertIn("adapter.RunAsync(job)", connection)
self.assertIn("ReportRecoverableJobsAsync", connection)
self.assertIn("ConcurrentDictionary<Guid, Task> jobPipelines", connection)
self.assertIn("StartJobPipeline(socket, acceptedJob)", connection)
self.assertIn("inputDownloader.DownloadAsync(job, forcedStop.Token)", connection)
self.assertNotIn("CreateLinkedTokenSource(cancellationToken, forcedStop.Token)", connection)
self.assertIn('stage = "uploading_outputs"', connection)
self.assertIn('stage = "software_running"', connection)
self.assertNotIn('stage = "origin_running"', connection)
self.assertIn("&& !job.UploadComplete", connection)
self.assertIn("&& !job.CloudTerminal", connection)
self.assertIn(
"await Task.WhenAll(jobPipelines.Values.Concat(exportPipelines.Values).ToArray())",
connection,
)
self.assertIn("StartJobPipeline(socket, job)", connection)
self.assertIn('stage = "downloading_inputs"', connection)
self.assertIn('Path.Combine(jobDirectory, "terminal.json")', inbox)
self.assertIn("AtomicWrite(requestPath, updated, overwrite: true)", inbox)
downloader = (PROJECT / "JobInputDownloader.cs").read_text(encoding="utf-8")
self.assertIn('new AuthenticationHeaderValue("Bearer", config.NodeToken)', downloader)
self.assertIn('DefaultRequestHeaders.Add("X-Node-Id"', downloader)
self.assertIn("HttpCompletionOption.ResponseHeadersRead", downloader)
self.assertIn("IncrementalHash.CreateHash", downloader)
self.assertIn("total > expectedSize", downloader)
self.assertIn("File.Move(temporaryPath, destination, overwrite: false)", downloader)
self.assertNotIn("Process.Start", downloader)
def test_adapter_worker_launch_is_manifest_driven_and_terminal_driven(self) -> None:
runner = (PROJECT / "AdapterProcessRunner.cs").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
project = (PROJECT / "Zcbot.WindowsNode.csproj").read_text(encoding="utf-8")
worker = (
ROOT / "adapters" / "origin.plot@v2" / "worker.py"
).read_text(encoding="utf-8")
self.assertIn('Environment.GetEnvironmentVariable("ZCBOT_ORIGIN_PYTHON")', runner)
self.assertIn(
'Path.Combine(paths.RootDirectory, "runtimes", runtimeId, "Scripts", "python.exe")',
runner,
)
self.assertIn("UseShellExecute = false", runner)
self.assertIn("command.PrefixArguments.Concat(arguments)", runner)
self.assertIn("descriptor.EntrypointPath", runner)
self.assertIn('Path.Combine(jobDirectory, "terminal.json")', runner)
self.assertIn('"NODE_RESTARTED_DURING_JOB"', runner)
self.assertIn("CancellationTokenSource.CreateLinkedTokenSource", runner)
self.assertIn("process.Kill(entireProcessTree: true)", runner)
self.assertIn('type.GetString() == "job_cancel"', connection)
self.assertIn('"cancelled", "USER_CANCELLED"', connection)
self.assertIn("..\\adapters\\**\\*", project)
self.assertIn("descriptor.Manifest.WorkerTimeoutMinutes", runner)
adapters = (PROJECT / "NodeAdapters.cs").read_text(encoding="utf-8")
self.assertIn(
'OptionalInteger(manifestRoot, "worker_timeout_minutes", 30, 1, 1440)',
adapters,
)
self.assertIn("if op.oext:", worker)
self.assertIn("op.exit()", worker)
self.assertIn("op.new_graph", worker)
self.assertIn("layer.add_plot", worker)
self.assertIn("op.save", worker)
self.assertIn("graph.save_fig", worker)
self.assertIn('_atomic_json(job_dir / "terminal.json"', worker)
for forbidden in ("subprocess", "eval(", "exec(", "os.system"):
self.assertNotIn(forbidden, worker)
def test_origin_adapter_can_be_packaged_without_building_node(self) -> None:
script = (ROOT / "package-origin-adapter.bat").read_text(encoding="utf-8")
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
self.assertIn('"adapters\\origin.plot@v2\\adapter.json"', script)
self.assertIn('"adapters\\origin.plot@v2\\worker.py"', script)
self.assertIn('"..\\software-contracts\\origin.plot.v2.json"', script)
self.assertNotIn("dotnet", script.lower())
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
self.assertIn("Contract SHA-256: {adapter.ContractSha256}", form)
self.assertIn("Workspace protocol:", form)
self.assertIn("Workspace state:", form)
self.assertIn("workspace_protocol_version", connection)
self.assertIn("contract_sha256", connection)
uploader = (PROJECT / "JobOutputUploader.cs").read_text(encoding="utf-8")
self.assertIn('new AuthenticationHeaderValue("Bearer", config.NodeToken)', uploader)
self.assertIn('DefaultRequestHeaders.Add("X-Node-Id"', uploader)
self.assertIn('DefaultRequestHeaders.Add("X-Lease-Id"', uploader)
self.assertIn("SHA256.HashDataAsync", uploader)
self.assertIn("upload-complete.json", connection + uploader)
self.assertIn("adapter.PreviewOutputIds", connection)
self.assertIn("adapter.WorkspaceStateFilename", connection)
self.assertIn("WorkspaceSize(jobDirectory), allowConflict: true", uploader)
self.assertIn("completeResponse.StatusCode == HttpStatusCode.Conflict", uploader)
self.assertIn('"JOB_ALREADY_TERMINAL"', uploader)
self.assertIn('"cloud-terminal.json"', uploader)
self.assertIn("OpenOutputWithRetryAsync", uploader)
self.assertIn("IsSharingViolation", uploader)
self.assertIn("(exception.HResult & 0xFFFF) is 32 or 33", uploader)
self.assertIn("artifact={artifactId}", uploader)
self.assertIn("hresult=0x{exception.HResult:X8}", connection + uploader)
self.assertIn('"node-output-upload.log"', uploader)
self.assertIn("MaxDiagnosticLogBytes", uploader)
self.assertIn("outputUploader.RecordDeferred(current, exception)", connection)
self.assertIn('completionPath + ".pending"', uploader)
self.assertIn("PreparePendingCompletion(pendingPath, responseBody)", uploader)
self.assertIn("File.Move(pendingPath, completionPath, overwrite: false)", uploader)
self.assertIn("operation=move_pending", uploader)
self.assertNotIn('".tmp-" + Guid.NewGuid()', uploader)
self.assertNotIn("Process.Start", uploader)
tray = (PROJECT / "TrayApplicationContext.cs").read_text(encoding="utf-8")
self.assertIn("_ = form.Handle", tray)
self.assertIn(
'UpdateStatus(NodeStatus.Create(NodeState.Connecting, "正在连接 zcbot…"))',
tray,
)
def test_blender_adapter_can_be_packaged_without_building_node(self) -> None:
script = (ROOT / "package-blender-adapter.bat").read_text(encoding="utf-8")
self.assertIn("adapters\\blender.scene.author@v2", script)
self.assertIn("\\worker.py", script)
self.assertIn("\\blender_worker.py", script)
self.assertIn("..\\software-contracts\\blender.scene.author.v2.json", script)
self.assertIn("tar.exe -a -c -f", script)
self.assertNotIn("dotnet", script.lower())
def test_local_job_monitor_never_shows_update_before_acceptance(self) -> None:
inbox = (PROJECT / "JobInboxStore.cs").read_text(encoding="utf-8")
self.assertIn("state.UpdatedAt > acceptedAt", inbox)
if __name__ == "__main__":
unittest.main()