diff --git a/package.json b/package.json index f37a82a99ebb..28d0f5c2effa 100644 --- a/package.json +++ b/package.json @@ -1965,7 +1965,7 @@ "test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts", "test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts", "test:watch": "node --import tsx scripts/test-projects.mts --watch", - "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/agents/tools/media-tool-file-url.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/ports.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/provider-local-service.env-case.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts extensions/memory-core/src/memory-extra-file-path.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/agents/agent-tools.read.windows.test.ts src/agents/agent-tools.read.host-operations.test.ts src/agents/sessions/tools/path-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", + "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/agents/tools/media-tool-file-url.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/ports.test.ts src/infra/advertised-lan-host.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/provider-local-service.env-case.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts extensions/memory-core/src/memory-extra-file-path.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/agents/agent-tools.read.windows.test.ts src/agents/agent-tools.read.host-operations.test.ts src/agents/sessions/tools/path-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", "test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts", "tool-display:check": "node --import tsx scripts/tool-display.ts --check", "tool-display:write": "node --import tsx scripts/tool-display.ts --write", diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 8ba769c4159c..bfcfe44e88d3 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -58,9 +58,9 @@ const WINDOWS_SQLITE_SCOPE_RE = /^src\/(?:state\/|.*sqlite.*\.ts$)/; const WINDOWS_FILE_URL_SCOPE_RE = /^(?:src\/agents\/tools\/(?:media-tool-file-url\.windows\.test|media-tool-shared(?:\.test)?|pdf-tool(?:\.test)?)|src\/auto-reply\/(?:reply\/stage-sandbox-media|reply\.triggers\.trigger-handling\.stages-inbound-media-into-sandbox-workspace\.test)|src\/media\/(?:local-media-path(?:\.windows\.test)?|local-roots(?:\.test)?|web-media(?:\.file-url\.windows\.test)?)|src\/channels\/inbound-event\/media(?:\.test)?|src\/gateway\/managed-image-attachments(?:\.test)?|extensions\/msteams\/src\/(?:media-helpers|messenger)(?:\.test)?)\.ts$/; const WINDOWS_SCOPE_RE = - /^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:exec-allowlist-pattern|fs-safe-remove)(?:\.test)?|ports(?:-inspect|\.test)|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mts|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|mts|js)|lib\/(?:direct-run\.(?:mjs|mts)|format-generated-module\.mts|tsx-cli-shim\.mjs|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/; + /^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:advertised-lan-host|exec-allowlist-pattern|fs-safe-remove)(?:\.windows)?(?:\.test)?|ports(?:-inspect|\.test)|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mts|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|mts|js)|lib\/(?:direct-run\.(?:mjs|mts)|format-generated-module\.mts|tsx-cli-shim\.mjs|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/; const WINDOWS_TEST_SCOPE_RE = - /^(extensions\/mxc\/test\/(?:mxc-backend|sandbox-policy-loader)\.test\.ts$|src\/agents\/bash-tools\.exec\.script-preflight\.test\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive\.worker|store\.session-lifecycle-mutation)\.test\.ts$|src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/(?:exec-allowlist-pattern|fs-safe-remove|ports|ssh-client\.windows|update-managed-service-handoff-(?:command|lifecycle)|windows-install-roots)\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|src\/state\/openclaw-database-paths\.windows\.test\.ts$|src\/test-utils\/openclaw-test-state\.test\.ts$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/; + /^(extensions\/mxc\/test\/(?:mxc-backend|sandbox-policy-loader)\.test\.ts$|src\/agents\/bash-tools\.exec\.script-preflight\.test\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive\.worker|store\.session-lifecycle-mutation)\.test\.ts$|src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/(?:advertised-lan-host(?:\.windows)?|exec-allowlist-pattern|fs-safe-remove|ports|ssh-client\.windows|update-managed-service-handoff-(?:command|lifecycle)|windows-install-roots)\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|src\/state\/openclaw-database-paths\.windows\.test\.ts$|src\/test-utils\/openclaw-test-state\.test\.ts$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/; const WINDOWS_SECRETREF_SCOPE_RE = /^(?:src\/commands\/doctor-gateway-auth-token(?:\.windows\.test)?\.ts|src\/flows\/(?:doctor-core-checks|doctor-health-contributions)\.ts|src\/gateway\/(?:auth-token-resolution|resolve-configured-secret-input-string)\.ts|src\/infra\/(?:fs-safe|fs-safe-defaults|permissions)\.ts|src\/secrets\/(?:resolve|resolve-errors)\.ts|src\/security\/audit-fs\.ts)$/; const WINDOWS_SECRETREF_TEST_SCOPE_RE = diff --git a/src/infra/advertised-lan-host.test.ts b/src/infra/advertised-lan-host.test.ts index f4447091458e..e0de4c28b661 100644 --- a/src/infra/advertised-lan-host.test.ts +++ b/src/infra/advertised-lan-host.test.ts @@ -49,7 +49,7 @@ describe("advertised LAN host", () => { const runner = createRouteRunner( JSON.stringify([ { InterfaceAlias: "Ethernet", RouteMetric: 1, InterfaceMetric: 1000 }, - { InterfaceAlias: "Ethernet 2", RouteMetric: 100, InterfaceMetric: 1 }, + { InterfaceAlias: "réseau-网卡", RouteMetric: 100, InterfaceMetric: 1 }, ]), ); @@ -60,7 +60,7 @@ describe("advertised LAN host", () => { networkInterfaces: () => ({ Ethernet: [ipv4("10.37.129.4")], - "Ethernet 2": [ipv4("10.211.55.3")], + "réseau-网卡": [ipv4("10.211.55.3")], }) as NetworkInterfacesSnapshot, }), ).resolves.toBe("10.211.55.3"); @@ -71,7 +71,9 @@ describe("advertised LAN host", () => { "-ExecutionPolicy", "Bypass", "-Command", - expect.stringContaining("Get-NetRoute"), + expect.stringMatching( + /^\[Console\]::OutputEncoding=\[Text\.UTF8Encoding\]::new\(\$false\); Get-NetRoute/, + ), ], { timeoutMs: 3_000, maxOutputBytes: 16 * 1024 }, ); diff --git a/src/infra/advertised-lan-host.ts b/src/infra/advertised-lan-host.ts index 1ff9b2721458..ba879f2e445d 100644 --- a/src/infra/advertised-lan-host.ts +++ b/src/infra/advertised-lan-host.ts @@ -11,9 +11,8 @@ import { const DEFAULT_ROUTE_HINT_TIMEOUT_MS = 3_000; const DEFAULT_ROUTE_HINT_OUTPUT_BYTES = 16 * 1024; const WINDOWS_DEFAULT_ROUTE_COMMAND = - "Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' | " + - "Select-Object -Property InterfaceAlias,InterfaceIndex,NextHop,RouteMetric,InterfaceMetric,DestinationPrefix | " + - "ConvertTo-Json -Compress"; + "[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false); Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' | " + + "Select-Object -Property InterfaceAlias,RouteMetric,InterfaceMetric | ConvertTo-Json -Compress"; type AdvertisedLanHostCandidate = { interfaceName: string; diff --git a/src/infra/advertised-lan-host.windows.test.ts b/src/infra/advertised-lan-host.windows.test.ts new file mode 100644 index 000000000000..f9dd6eaccab1 --- /dev/null +++ b/src/infra/advertised-lan-host.windows.test.ts @@ -0,0 +1,68 @@ +// Windows-native proof that PowerShell preserves localized adapter aliases. +import { describe, expect, it, vi } from "vitest"; +import { runCommandWithTimeout } from "../process/exec.js"; +import { resolveAdvertisedLanHostCore } from "./advertised-lan-host.js"; +import type { NetworkInterfacesSnapshot } from "./network-interfaces.js"; + +type ResolveOptions = NonNullable[0]>; +type RouteRunner = NonNullable; + +function ipv4(address: string) { + return { + address, + family: "IPv4" as const, + internal: false, + netmask: "255.255.255.0", + mac: "00:00:00:00:00:00", + cidr: `${address}/24`, + }; +} + +describe.runIf(process.platform === "win32")("advertised LAN host PowerShell contract", () => { + it("round-trips a localized route alias through the production command prefix", async () => { + let capturedArgv: string[] | undefined; + const captureRunner: RouteRunner = vi.fn(async (argv) => { + capturedArgv = argv; + return { code: 0, stdout: "", stderr: "" }; + }); + const interfaces = { + "vEthernet (Default Switch)": [ipv4("10.37.129.4")], + "réseau-网卡": [ipv4("192.168.1.20")], + } as NetworkInterfacesSnapshot; + + await resolveAdvertisedLanHostCore({ + platform: "win32", + networkInterfaces: () => interfaces, + runCommandWithTimeout: captureRunner, + }); + + const command = capturedArgv?.at(-1); + const routeCommandIndex = command?.indexOf("Get-NetRoute") ?? -1; + const outputPrefix = routeCommandIndex > 0 ? (command?.slice(0, routeCommandIndex) ?? "") : ""; + const result = await runCommandWithTimeout( + [ + "powershell.exe", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + `[Console]::OutputEncoding=[Text.Encoding]::GetEncoding(437); ${outputPrefix}[pscustomobject]@{InterfaceAlias='réseau-网卡';RouteMetric=1;InterfaceMetric=1} | ConvertTo-Json -Compress`, + ], + { timeoutMs: 3_000, maxOutputBytes: 16 * 1024 }, + ); + expect(result).toMatchObject({ code: 0 }); + expect(JSON.parse(result.stdout)).toMatchObject({ InterfaceAlias: "réseau-网卡" }); + expect(command).toMatch( + /^\[Console\]::OutputEncoding=\[Text\.UTF8Encoding\]::new\(\$false\); Get-NetRoute/, + ); + expect(routeCommandIndex).toBeGreaterThan(0); + + await expect( + resolveAdvertisedLanHostCore({ + platform: "win32", + networkInterfaces: () => interfaces, + runCommandWithTimeout: vi.fn(async () => result), + }), + ).resolves.toBe("192.168.1.20"); + }); +}); diff --git a/src/scripts/ci-changed-scope.windows.test.ts b/src/scripts/ci-changed-scope.windows.test.ts index 0c6460586fa9..3a5d4798e90f 100644 --- a/src/scripts/ci-changed-scope.windows.test.ts +++ b/src/scripts/ci-changed-scope.windows.test.ts @@ -74,6 +74,19 @@ describe("detectChangedScope Windows routing", () => { } }); + it("routes LAN advertisement and its native PowerShell proof to Windows", () => { + for (const lanPath of [ + "src/infra/advertised-lan-host.ts", + "src/infra/advertised-lan-host.test.ts", + "src/infra/advertised-lan-host.windows.test.ts", + ]) { + expect(detectChangedScope([lanPath]), lanPath).toMatchObject({ + runNode: true, + runWindows: true, + }); + } + }); + it("routes MXC runtime changes and Windows-only suites to Windows", () => { for (const mxcPath of [ "extensions/mxc/src/mxc-backend.ts", diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index 640d45702ef9..6a440359de34 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -237,6 +237,12 @@ describe("package scripts", () => { expect(readPackageJson().scripts["test:windows:ci"]).toContain("src/infra/ports.test.ts"); }); + it("runs native LAN advertisement coverage in Windows CI", () => { + expect(readPackageJson().scripts["test:windows:ci"]).toContain( + "src/infra/advertised-lan-host.windows.test.ts", + ); + }); + it("keeps the native Scheduled Task lifecycle proof opt-in", () => { const scripts = readPackageJson().scripts;