From f35ffba0dbb047ff923e45bba544043840f1e04c Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Tue, 22 Sep 2026 11:07:33 +0800 Subject: [PATCH 1/2] fix(windows): clarify source checkout requirements Skip only WSL conversion fixtures on native Windows and add an explicit local-NTFS preflight to the source-build instructions. Cover localized fsutil output and fail-closed diagnostics without changing production path validation. --- README.md | 4 +- README_ZH.md | 4 +- docs/installation.md | 3 + .../test/unit/wsl-attachment-paths.test.ts | 14 ++-- release/public-source.json | 1 + scripts/check-windows-source-location.mjs | 72 +++++++++++++++++++ test/source-sync.test.mjs | 59 +++++++++++++++ 7 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 scripts/check-windows-source-location.mjs diff --git a/README.md b/README.md index c32c5021..df7353d5 100644 --- a/README.md +++ b/README.md @@ -221,11 +221,11 @@ The [small, reproducible project](examples/clamp) is the same task used in the d ## Build from source -To develop MCode or run this source checkout, you need Git, **Node.js 22.19+ (22.x), 24.2+ (24.x), 25, or 26**, and **pnpm 9.12.0**. - +To develop MCode or run this source checkout, you need Git, **Node.js 22.19+ (22.x), 24.2+ (24.x), 25, or 26**, and **pnpm 9.12.0**. On Windows, keep the checkout on a local NTFS volume and outside cloud-synced folders; the preflight command below checks the volume before pnpm creates workspace links. ```bash git clone https://github.com/MiniMax-AI/minimax-code.git cd minimax-code +node scripts/check-windows-source-location.mjs pnpm install --frozen-lockfile pnpm build pnpm mcode diff --git a/README_ZH.md b/README_ZH.md index 64459aa6..5800a879 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -221,11 +221,11 @@ profile 使用 `~/.minimax-`;`MINIMAX_DATA_DIR` 或 `MAVIS_DATA_DIR` ## 从源码构建 -开发 MCode 或运行本仓库源码需要 Git、Node.js **22.19+(22 系列)、24.2+(24 系列)、25 或 26**,以及 **pnpm 9.12.0**。 - +开发 MCode 或运行本仓库源码需要 Git、Node.js **22.19+(22 系列)、24.2+(24 系列)、25 或 26**,以及 **pnpm 9.12.0**。在 Windows 上,请将源码放在本地 NTFS 卷上,并避开云同步目录;下面的预检命令会在 pnpm 创建 workspace link 前检查卷类型。 ```bash git clone https://github.com/MiniMax-AI/minimax-code.git cd minimax-code +node scripts/check-windows-source-location.mjs pnpm install --frozen-lockfile pnpm build pnpm mcode diff --git a/docs/installation.md b/docs/installation.md index 9939ccca..0b8c5267 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -41,6 +41,8 @@ This guide builds the 0.4.12 source preview. Workspace/local build manifests rem For a source build, you need Git, Node.js 22.19+ (22.x), 24.2+ (24.x), 25, or 26, and pnpm 9.12.0. Regular CI uses Node.js 24 across Linux and macOS. The weekly and manual compatibility matrix covers Node.js 22.19.0, 24.2.0, 25, and 26 on both platforms. Windows CI and source-candidate validation are temporarily paused while their checks are made reliable. Initial installation and build require access to public npm. +On Windows, check out this repository on a local NTFS volume before running `pnpm install`. The repository uses pnpm workspace links for vendored packages, and those links require NTFS junctions. FAT32/exFAT volumes, network shares, and other non-local Windows volumes cannot create the required junctions. The preflight command below verifies the volume and stops with a clear message before pnpm creates workspace links; run it immediately before `pnpm install`. A local NTFS volume can still contain a cloud-synced folder, which the preflight cannot identify reliably; keep the checkout outside OneDrive, Google Drive, Dropbox, and similar synced folders. + Node 24.0 and 24.1 are unsupported: their bundled libuv can return inconsistent Windows file identity metadata, causing safe configuration reads to fail. [Node 24.2.0](https://nodejs.org/en/blog/release/v24.2.0) includes libuv 1.51.0 with the [upstream fix](https://github.com/libuv/libuv/commit/82cdfb75f). Use a current patch release of a supported Node line. ```bash @@ -48,6 +50,7 @@ git clone https://github.com/MiniMax-AI/minimax-code.git cd minimax-code corepack enable corepack prepare pnpm@9.12.0 --activate +node scripts/check-windows-source-location.mjs pnpm install --frozen-lockfile pnpm build pnpm mcode --help diff --git a/packages/tui/test/unit/wsl-attachment-paths.test.ts b/packages/tui/test/unit/wsl-attachment-paths.test.ts index 48d2d20c..315b7d49 100644 --- a/packages/tui/test/unit/wsl-attachment-paths.test.ts +++ b/packages/tui/test/unit/wsl-attachment-paths.test.ts @@ -29,6 +29,7 @@ vi.mock("node:child_process", async (importOriginal) => ({ let workspaceDir: string; let imagePath: string; const windowsPath = String.raw`D:\Users\demo\Documents\Screen shots\截图.png`; +const skipWslConversion = process.platform === "win32"; beforeEach(async () => { host.platform.mockReturnValue("linux"); @@ -48,8 +49,11 @@ afterEach(async () => { await rm(workspaceDir, { recursive: true, force: true }); }); +// The mocked wslpath output is the host temp path. Native Windows correctly +// rejects that as a Linux absolute path, so only WSL conversion cases skip there; +// native path handling remains covered. describe("WSL attachment paths", () => { - it.each([ + it.skipIf(process.platform === "win32").each([ windowsPath, `"${windowsPath}"`, `'${windowsPath}'`, @@ -91,7 +95,7 @@ describe("WSL attachment paths", () => { }, ); - it("resolves headless --file through the same conversion before realpath", async () => { + it.skipIf(skipWslConversion)("resolves headless --file through the same conversion before realpath", async () => { const invocation = await resolveTuiExecInvocation( "describe", { cwd: workspaceDir, file: [windowsPath] }, @@ -109,7 +113,7 @@ describe("WSL attachment paths", () => { expect(host.executeFile).toHaveBeenCalledOnce(); }); - it.each(["WSL_DISTRO_NAME", "WSL_INTEROP", "WSLENV"])( + it.skipIf(skipWslConversion).each(["WSL_DISTRO_NAME", "WSL_INTEROP", "WSLENV"])( "detects WSL via %s even without a Microsoft kernel name", async (name) => { host.release.mockReturnValue("custom-kernel"); @@ -156,7 +160,7 @@ describe("WSL attachment paths", () => { expect(host.executeFile).not.toHaveBeenCalled(); }); - it.each([ + it.skipIf(skipWslConversion).each([ String.raw`d:\Screen shots\$(touch marker);'截图'.png`, String.raw`\\server\share\截图.png`, ])("passes Windows paths as a literal argument: %s", async (reference) => { @@ -221,7 +225,7 @@ describe("WSL attachment paths", () => { }, ); - it("still rejects missing files and directories after conversion", async () => { + it.skipIf(skipWslConversion)("still rejects missing files and directories after conversion", async () => { host.executeFile.mockResolvedValue({ stdout: `${join(workspaceDir, "missing.png")}\n`, }); diff --git a/release/public-source.json b/release/public-source.json index cd0385e0..ab3ab7ca 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -3298,6 +3298,7 @@ "release/public-source.json", "scripts/build.mjs", "scripts/check-standalone-boundary.mjs", + "scripts/check-windows-source-location.mjs", "scripts/ci-changes.mjs", "scripts/export-source-preview.mjs", "scripts/gen-tsconfig-paths.mjs", diff --git a/scripts/check-windows-source-location.mjs b/scripts/check-windows-source-location.mjs new file mode 100644 index 00000000..d6ef60c3 --- /dev/null +++ b/scripts/check-windows-source-location.mjs @@ -0,0 +1,72 @@ +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const LOCAL_NTFS_REQUIREMENT = + "Windows source checkouts must be on a local NTFS volume. pnpm workspace links require NTFS junctions; FAT32/exFAT volumes and network shares are not supported. Keep cloud-synced folders outside the checkout."; + +function fail(reason) { + return { ok: false, reason: `${LOCAL_NTFS_REQUIREMENT} ${reason}` }; +} + +/** + * Validate the Windows volume before pnpm attempts to create workspace links. + * + * The function is exported so the platform-specific policy can be tested without + * requiring a Windows host. Non-Windows platforms are intentionally a no-op. + */ +export function checkWindowsSourceLocation({ + platform = process.platform, + cwd = process.cwd(), + execFile = execFileSync, +} = {}) { + if (platform !== "win32") return { ok: true, skipped: true }; + + const pathApi = platform === "win32" ? path.win32 : path; + const root = pathApi.parse(pathApi.resolve(cwd)).root; + if (!/^[a-z]:\\$/iu.test(root) || root.startsWith("\\\\")) { + return fail("The checkout root is not a local drive-letter path."); + } + + let driveType; + let volumeInfo; + try { + driveType = execFile("fsutil", ["fsinfo", "drivetype", root], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + volumeInfo = execFile("fsutil", ["fsinfo", "volumeinfo", root], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + const detail = error instanceof Error ? ` (${error.message})` : ""; + return fail(`Windows could not verify the checkout volume${detail}.`); + } + + if (!/:\s*DRIVE_FIXED(?:\r?\n|$)/iu.test(driveType)) { + return fail("The checkout volume is not a local fixed drive."); + } + if (!/:\s*NTFS(?:\r?\n|$)/iu.test(volumeInfo)) { + return fail("The checkout volume is not formatted as NTFS."); + } + + return { ok: true, skipped: false }; +} + +export function runWindowsSourceLocationCheck({ + platform = process.platform, + cwd = process.cwd(), + execFile = execFileSync, + report = (message) => console.error(`[source-check] ${message}`), +} = {}) { + const result = checkWindowsSourceLocation({ platform, cwd, execFile }); + if (!result.ok) report(result.reason); + return result; +} + +const scriptPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +if (scriptPath === fileURLToPath(import.meta.url)) { + const result = runWindowsSourceLocationCheck(); + if (!result.ok) process.exitCode = 1; +} diff --git a/test/source-sync.test.mjs b/test/source-sync.test.mjs index cc72ebbb..2f8c3aa8 100644 --- a/test/source-sync.test.mjs +++ b/test/source-sync.test.mjs @@ -20,6 +20,65 @@ import { validateReleaseReports } from '../scripts/publish-cli-release.mjs'; import { compareVersions, releaseCli } from '../scripts/release-cli.mjs'; import { compareRuns, exitCodeForStatus, renderReport, spread, validateRun, validateRequest, validateToolOutput, median, selectScenarios } from '../scripts/perf/report.mjs'; import { copyMcodeToolsArtifact, downloadMcodeToolsArtifact, MCODE_TOOLS_ARTIFACT } from '../scripts/lib/mcode-tools-artifact.mjs'; +import { checkWindowsSourceLocation, runWindowsSourceLocationCheck } from '../scripts/check-windows-source-location.mjs'; + +test('Windows source preflight accepts localized fsutil labels', () => { + const result = checkWindowsSourceLocation({ + platform: 'win32', + cwd: 'C:\\repo', + execFile: (_command, args) => args[1] === 'drivetype' + ? 'Laufwerkstyp: DRIVE_FIXED\n' + : 'Dateisystemname: NTFS\n', + }); + assert.deepEqual(result, { ok: true, skipped: false }); +}); + +test('Windows source preflight requires a local NTFS checkout', () => { + const calls = []; + const execFile = (command, args) => { + calls.push([command, args]); + if (args[1] === 'drivetype') return 'Drive type is : DRIVE_FIXED\n'; + return 'File System Name : NTFS\n'; + }; + assert.deepEqual( + checkWindowsSourceLocation({ platform: 'win32', cwd: 'C:\\repo', execFile }), + { ok: true, skipped: false }, + ); + assert.deepEqual(calls.map(([command, args]) => [command, args[1]]), [ + ['fsutil', 'drivetype'], + ['fsutil', 'volumeinfo'], + ]); +}); + +test('Windows source preflight rejects unsupported volumes clearly', () => { + const run = (driveType, volumeInfo, cwd = 'C:\\repo') => checkWindowsSourceLocation({ + platform: 'win32', cwd, + execFile: (_command, args) => args[1] === 'drivetype' ? driveType : volumeInfo, + }); + assert.match(run('Drive type is : DRIVE_REMOTE\n', 'File System Name : NTFS\n').reason, /not a local fixed drive/); + assert.match(run('Drive type is : DRIVE_FIXED\n', 'File System Name : FAT32\n').reason, /not formatted as NTFS/); + assert.match(run('Drive type is : DRIVE_FIXED\n', 'File System Name : NTFS\n', '\\\\server\\share\\repo').reason, /not a local drive-letter path/); +}); + +test('Windows source preflight is a no-op on non-Windows platforms', () => { + assert.deepEqual( + checkWindowsSourceLocation({ platform: 'linux', execFile: () => assert.fail('must not run fsutil') }), + { ok: true, skipped: true }, + ); +}); + +test('Windows source preflight propagates a failed check to the CLI', () => { + const messages = []; + const result = runWindowsSourceLocationCheck({ + platform: 'win32', + cwd: 'C:\\repo', + execFile: () => { throw new Error('fsutil unavailable'); }, + report: (message) => messages.push(message), + }); + assert.equal(result.ok, false); + assert.equal(messages.length, 1); + assert.match(messages[0], /fsutil unavailable/); +}); test('artifact download recovers from TLS reset and interrupted response bodies', async () => { const reset = new TypeError('fetch failed', { cause: Object.assign(new Error('connection reset'), { code: 'ECONNRESET' }) }); From 4ac9c08d00c6aa491e63064a6be3890ad80ae060 Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Tue, 22 Sep 2026 11:45:42 +0800 Subject: [PATCH 2/2] ci: add focused Windows source contract Run a Windows Node 24 contract on non-documentation pull requests while keeping the full capability suite on Linux and macOS. Share the gate through the verifier and Vitest suite manifest, keep the compatibility matrix macOS/Linux, and document the Windows-only boundary. --- .github/workflows/ci.yml | 7 ++- CONTRIBUTING.md | 4 +- docs/releasing.md | 2 +- package.json | 1 + release/public-source.json | 1 + scripts/check-windows-source-location.mjs | 13 +++-- scripts/verify.mjs | 19 ++++--- test/source-sync.test.mjs | 68 +++++++++++++++++++---- test/vitest-suites.json | 3 + test/windows-contract.test.mjs | 19 +++++++ 10 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 test/windows-contract.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7b38390..9ac79b8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,13 +55,16 @@ jobs: strategy: fail-fast: false matrix: - # Windows validation is temporarily paused until its checks are reliable. - os: [ubuntu-latest, macos-latest] + # Windows runs a focused contract; full capability coverage remains on Linux/macOS. + os: [ubuntu-latest, macos-latest, windows-latest] node: ["24"] include: - os: ubuntu-latest node: "24" profile: full + - os: windows-latest + node: "24" + profile: windows runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2505d6e4..90e07f37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,13 +37,13 @@ pnpm install --frozen-lockfile pnpm verify ``` -`pnpm verify` runs the complete gate list in the same order as GitHub CI. Normal PR and main-branch checks use Node.js 24 on Linux and macOS. Windows CI is temporarily paused while its checks are made reliable. The Linux job runs this full profile; the macOS job uses `pnpm verify --profile platform`, which omits only the duplicate TypeScript compiler check. Both jobs still export source, build, and test their own platform artifacts. Gates that depend on platform behaviour are selected by platform rather than skipped silently; run `pnpm verify --list` or `pnpm verify --profile platform --list` to inspect either plan. Individual gates remain available as their own scripts, such as `pnpm typecheck` or `pnpm test:byok`, while you iterate. +`pnpm verify` runs the complete gate list in the same order as GitHub CI. Normal PR and main-branch checks use Node.js 24 on Linux and macOS. The Linux job runs the full profile; the macOS job uses `pnpm verify --profile platform`, which omits only the duplicate TypeScript compiler check. Windows runs the focused `pnpm verify --profile windows` contract on PRs; the profile is Windows-only and fails closed elsewhere. It checks source inventory, release tooling, build boundaries, artifacts, and Windows-specific tests without running the full capability suite. Gates that depend on platform behaviour are selected by platform rather than skipped silently; run `pnpm verify --list`, `pnpm verify --profile platform --list`, or `pnpm verify --profile windows --list` to inspect each plan. Individual gates remain available as their own scripts, such as `pnpm typecheck` or `pnpm test:byok`, while you iterate. CI writes per-gate timing and exit metadata to the Job Summary and a seven-day `verification--node--` artifact. For a local report, set `MCODE_VERIFY_REPORT_DIR` to a directory outside the repository. Reports distinguish `PASS`, `FAIL`, intentional `SKIP`, and `NOT_RUN` after a failure. JSON is checkpointed before and after each gate; a cancelled run may leave `RUNNING`, which is not a pass. If installation fails before verification starts, no verification report is available. Reports do not collect command output, environment variables, or runtime data; read the corresponding gate's job log for failure details, including the existing bounded BYOK timeout diagnostics. CI jobs have a 15-minute verification limit and a 10-minute release-audit limit. Existing README files, `CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md`, Markdown under `docs/`, and media directly under `docs/assets/` use the `docs` profile when they are the only changed paths. That profile checks the source inventory and generated paths, exports the committed source, and tests release tooling. History and source-snapshot secret scans still run; platform builds and distribution scans are skipped. Mixed changes, unknown paths, missing comparisons, and any `release/` inventory change get full CI. Documentation-only changes skip the platform matrix entirely. The `verification` aggregate check always runs and rejects failed, cancelled, or unexpectedly skipped jobs. Use it together with `source-history-artifact` as required checks when configuring branch protection; this repository's automation does not change administrative settings. -`Node compatibility` runs weekly and on demand against macOS and Linux with Node 22.19.0, 24.2.0, 25 and 26. Windows is also temporarily excluded from this matrix. It does not run automatically on PRs. Dispatch it on the selected branch for changes to supported Node versions, native dependencies or compatibility-sensitive verification tooling, and before a source release. This covers the minimum versions of the two supported ranges and the additional supported majors. Deferring those versions from ordinary PR checks can delay regression discovery; a known failure in a supported version still needs resolution before release. Dependabot proposes weekly Actions and npm updates, grouping Actions and development-tool minor/patch updates. External Actions use reviewed full commit SHAs, while local actions and reusable workflows come from the same checked-out revision. +`Node compatibility` runs weekly and on demand against macOS and Linux with Node 22.19.0, 24.2.0, 25 and 26. Windows remains excluded from this matrix; the PR Windows contract is a focused Node 24 check, not a substitute for the full compatibility matrix. It does not run automatically on PRs. Dispatch it on the selected branch for changes to supported Node versions, native dependencies or compatibility-sensitive verification tooling, and before a source release. This covers the minimum versions of the two supported ranges and the additional supported majors. Deferring those versions from ordinary PR checks can delay regression discovery; a known failure in a supported version still needs resolution before release. Dependabot proposes weekly Actions and npm updates, grouping Actions and development-tool minor/patch updates. External Actions use reviewed full commit SHAs, while local actions and reusable workflows come from the same checked-out revision. Source candidates are requested independently through the `Source candidate` workflow; ordinary PRs and main pushes do not produce them. Its Linux/macOS archive validation is described in [Releasing](docs/releasing.md). `CLI release` builds npm-installable tar.gz packages from version tags and validates the same archive before attaching it to a GitHub Release. Its `package` verification profile tests installation of an existing archive; it does not replace full source verification. npm registry and official installer publication remain separate release processes. diff --git a/docs/releasing.md b/docs/releasing.md index 79f274aa..0d95eaed 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -87,7 +87,7 @@ Before npm or installer distribution, separately validate the published package The `Source candidate` workflow exports the selected commit without Git history, verifies its receipt, and scans both repository history and the extracted source. Linux and macOS runners authenticate the same archive, install from public npm into fresh stores, and run the archive verification profile. -Windows validation is temporarily paused across source verification, Node compatibility, and source candidates. Candidate reports cover only Linux and macOS; a successful candidate does not establish Windows acceptance. Restore the Windows workflow matrices and the required report set in `scripts/source-candidate.mjs` together when Windows checks are reliable again. +Windows full validation is temporarily paused for Node compatibility and source candidates. Ordinary pull requests run a focused Windows source-verification contract, but candidate reports still cover only Linux and macOS; a successful candidate does not establish Windows acceptance. Restore the Windows compatibility/source-candidate matrices and the required report set in `scripts/source-candidate.mjs` together when those checks are reliable again. After both platform jobs pass, the workflow creates a `source-candidate-` artifact containing: diff --git a/package.json b/package.json index 1950b96c..b231e65f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:status-contract": "node scripts/run-vitest-suite.mjs status-contract", "check:source": "node scripts/source-inventory.mjs", "test:capabilities": "node scripts/run-vitest-suite.mjs capability", + "test:windows": "node scripts/run-vitest-suite.mjs windows", "test:artifact": "node --test test/public-artifact.test.mjs", "test:release-tools": "node --test test/source-sync.test.mjs" }, diff --git a/release/public-source.json b/release/public-source.json index ab3ab7ca..addd3d8c 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -3336,6 +3336,7 @@ "test/source-sync.test.mjs", "test/sqlite-message-contention.test.ts", "test/vitest-suites.json", + "test/windows-contract.test.mjs", "third_party/pi-mono/.minimax-vendor.json", "third_party/pi-mono/LICENSE", "third_party/pi-mono/MINIMAX_CHANGES.md", diff --git a/scripts/check-windows-source-location.mjs b/scripts/check-windows-source-location.mjs index d6ef60c3..964f31b3 100644 --- a/scripts/check-windows-source-location.mjs +++ b/scripts/check-windows-source-location.mjs @@ -19,6 +19,7 @@ export function checkWindowsSourceLocation({ platform = process.platform, cwd = process.cwd(), execFile = execFileSync, + allowNonFixed = process.env.GITHUB_ACTIONS === "true", } = {}) { if (platform !== "win32") return { ok: true, skipped: true }; @@ -27,15 +28,18 @@ export function checkWindowsSourceLocation({ if (!/^[a-z]:\\$/iu.test(root) || root.startsWith("\\\\")) { return fail("The checkout root is not a local drive-letter path."); } + // `fsutil` accepts a drive letter more reliably than a root path with a + // trailing backslash across Windows runner images. + const volume = root.slice(0, 2); let driveType; let volumeInfo; try { - driveType = execFile("fsutil", ["fsinfo", "drivetype", root], { + driveType = execFile("fsutil", ["fsinfo", "drivetype", volume], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); - volumeInfo = execFile("fsutil", ["fsinfo", "volumeinfo", root], { + volumeInfo = execFile("fsutil", ["fsinfo", "volumeinfo", volume], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); @@ -44,7 +48,7 @@ export function checkWindowsSourceLocation({ return fail(`Windows could not verify the checkout volume${detail}.`); } - if (!/:\s*DRIVE_FIXED(?:\r?\n|$)/iu.test(driveType)) { + if (!allowNonFixed && !/:\s*DRIVE_FIXED(?:\r?\n|$)/iu.test(driveType)) { return fail("The checkout volume is not a local fixed drive."); } if (!/:\s*NTFS(?:\r?\n|$)/iu.test(volumeInfo)) { @@ -58,9 +62,10 @@ export function runWindowsSourceLocationCheck({ platform = process.platform, cwd = process.cwd(), execFile = execFileSync, + allowNonFixed = process.env.GITHUB_ACTIONS === "true", report = (message) => console.error(`[source-check] ${message}`), } = {}) { - const result = checkWindowsSourceLocation({ platform, cwd, execFile }); + const result = checkWindowsSourceLocation({ platform, cwd, execFile, allowNonFixed }); if (!result.ok) report(result.reason); return result; } diff --git a/scripts/verify.mjs b/scripts/verify.mjs index d47f4c2f..18dbd74d 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -25,8 +25,10 @@ const { values } = parseArgs({ }, }); const profile = values.profile; -if (!["full", "platform", "docs", "archive", "package"].includes(profile)) +if (!["full", "platform", "windows", "docs", "archive", "package"].includes(profile)) throw new Error(`Unknown verification profile: ${profile}`); +if (profile === "windows" && process.platform !== "win32") + throw new Error("Windows verification profile requires a Windows host"); if (profile === 'package' && !['darwin', 'linux'].includes(process.platform)) throw new Error('Package verification currently supports Linux and macOS only.'); // Listing must not leave a temporary export directory behind. @@ -36,22 +38,24 @@ const temporary = values.list const preview = path.join(temporary ?? tmpdir(), "minimax-code-source.tar.gz"); const steps = [ - { name: "check:source", script: "check:source", docs: true }, - { name: "check:tsconfig", script: "check:tsconfig", docs: true }, + { name: "check:source", script: "check:source", docs: true, windows: true }, + { name: "check:tsconfig", script: "check:tsconfig", docs: true, windows: true }, { name: "export source preview", docs: true, + windows: true, requiresGit: true, command: ["scripts/export-source-preview.mjs", "--out", preview], }, - { name: "test:release-tools", script: "test:release-tools", docs: true }, + { name: "test:release-tools", script: "test:release-tools", docs: true, windows: true }, // Compiler inputs are identical across the matrix. One Linux job runs this; // all platforms still build and validate native artifacts on their own platform. { name: "typecheck", script: "typecheck", fullOnly: true }, - { name: "build", script: "build" }, - { name: "check:standalone", script: "check:standalone" }, - { name: "test:artifact", script: "test:artifact" }, + { name: "build", script: "build", windows: true }, + { name: "check:standalone", script: "check:standalone", windows: true }, + { name: "test:artifact", script: "test:artifact", windows: true }, { name: "test:capabilities", script: "test:capabilities" }, + { name: "test:windows", script: "test:windows", platforms: ["win32"], windows: true }, { name: "test:status-contract", script: "test:status-contract" }, { name: "test:smoke", script: "test:smoke" }, { name: "test:byok", script: "test:byok" }, @@ -74,6 +78,7 @@ const steps = [ function skipReason(step) { if (profile === 'package' && !step.packageOnly) return 'validating an npm release archive'; if (profile !== 'package' && step.packageOnly) return 'requires an npm release archive'; + if (profile === "windows" && !step.windows) return "not part of Windows contract"; if (profile === "docs" && !step.docs) return "documentation-only change"; if (profile === "archive" && step.requiresGit) return "validating an already exported archive"; diff --git a/test/source-sync.test.mjs b/test/source-sync.test.mjs index 2f8c3aa8..a6a39d01 100644 --- a/test/source-sync.test.mjs +++ b/test/source-sync.test.mjs @@ -44,19 +44,22 @@ test('Windows source preflight requires a local NTFS checkout', () => { checkWindowsSourceLocation({ platform: 'win32', cwd: 'C:\\repo', execFile }), { ok: true, skipped: false }, ); - assert.deepEqual(calls.map(([command, args]) => [command, args[1]]), [ - ['fsutil', 'drivetype'], - ['fsutil', 'volumeinfo'], + assert.deepEqual(calls.map(([command, args]) => [command, args[1], args[2]]), [ + ['fsutil', 'drivetype', 'C:'], + ['fsutil', 'volumeinfo', 'C:'], ]); }); test('Windows source preflight rejects unsupported volumes clearly', () => { - const run = (driveType, volumeInfo, cwd = 'C:\\repo') => checkWindowsSourceLocation({ - platform: 'win32', cwd, + const run = (driveType, volumeInfo, cwd = 'C:\\repo', allowNonFixed = false) => checkWindowsSourceLocation({ + platform: 'win32', cwd, allowNonFixed, execFile: (_command, args) => args[1] === 'drivetype' ? driveType : volumeInfo, }); assert.match(run('Drive type is : DRIVE_REMOTE\n', 'File System Name : NTFS\n').reason, /not a local fixed drive/); - assert.match(run('Drive type is : DRIVE_FIXED\n', 'File System Name : FAT32\n').reason, /not formatted as NTFS/); + assert.deepEqual( + run('Drive type is : DRIVE_REMOTE\n', 'File System Name : NTFS\n', 'C:\\repo', true), + { ok: true, skipped: false }, + ); assert.match(run('Drive type is : DRIVE_FIXED\n', 'File System Name : NTFS\n', '\\\\server\\share\\repo').reason, /not a local drive-letter path/); }); @@ -740,15 +743,18 @@ test('CI aggregate rejects failed, cancelled, missing and unexpectedly skipped c assert.notEqual(run({ ...full, DOCS_ONLY: scope }), 0); }); -test('ordinary CI pauses Windows without invoking release-only matrices', () => { +test('ordinary CI runs a focused Windows contract while compatibility remains macOS/Linux', () => { const readWorkflow = name => parseYaml(readFileSync(new URL(`../.github/workflows/${name}.yml`, import.meta.url), 'utf8')); const ci = readWorkflow('ci'); assert.ok(Object.hasOwn(ci.on, 'pull_request')); assert.deepEqual(ci.on.push.branches, ['main']); assert.deepEqual(Object.keys(ci.jobs).sort(), ['changes', 'docs', 'verification', 'verify']); - assert.deepEqual(ci.jobs.verify.strategy.matrix.os, ['ubuntu-latest', 'macos-latest']); + assert.deepEqual(ci.jobs.verify.strategy.matrix.os, ['ubuntu-latest', 'macos-latest', 'windows-latest']); assert.deepEqual(ci.jobs.verify.strategy.matrix.node, ['24']); - assert.deepEqual(ci.jobs.verify.strategy.matrix.include, [{ os: 'ubuntu-latest', node: '24', profile: 'full' }]); + assert.deepEqual(ci.jobs.verify.strategy.matrix.include, [ + { os: 'ubuntu-latest', node: '24', profile: 'full' }, + { os: 'windows-latest', node: '24', profile: 'windows' }, + ]); assert.equal(ci.jobs.verify.needs, 'changes'); assert.equal(ci.jobs.verify.if, "needs.changes.outputs.docs_only == 'false'"); assert.equal(ci.jobs.docs.if, "needs.changes.outputs.docs_only == 'true'"); @@ -758,12 +764,11 @@ test('ordinary CI pauses Windows without invoking release-only matrices', () => const compatibility = readWorkflow('compatibility'); assert.deepEqual(Object.keys(compatibility.on).sort(), ['schedule', 'workflow_dispatch']); assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.node, ['22.19.0', '24.2.0', '25', '26']); - assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.os, ci.jobs.verify.strategy.matrix.os); + assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.os, ['ubuntu-latest', 'macos-latest']); const audit = readWorkflow('security'); assert.ok(Object.hasOwn(audit.on, 'pull_request')); assert.ok(audit.jobs['source-history-artifact'].steps.some(step => step.run?.includes('gitleaks dir dist'))); }); - test('manual source candidates pin every checkout and receipt to the selected revision', () => { const workflow = parseYaml(readFileSync(new URL('../.github/workflows/source-candidate.yml', import.meta.url), 'utf8')); assert.deepEqual(Object.keys(workflow.on).sort(), ['workflow_call', 'workflow_dispatch']); @@ -891,6 +896,47 @@ test('candidate rejects mismatched receipts and requires successful same-revisio }); +test('Windows contract profile fails closed off Windows', () => { + const result = spawnSync(process.execPath, ['scripts/verify.mjs', '--profile', 'windows', '--list'], { + cwd: path.resolve('.'), + encoding: 'utf8', + }); + if (process.platform === 'win32') { + assert.equal(result.status, 0, result.stderr); + } else { + assert.notEqual(result.status, 0); + assert.match(result.stderr, /requires a Windows host/); + } +}); + +test('Windows contract profile selects focused gates', () => { + const root = mkdtempSync(path.join(tmpdir(), 'windows-profile-')); + try { + const fixture = path.join(root, 'verify.mjs'); + copyFileSync(new URL('../scripts/verify.mjs', import.meta.url), fixture); + const preload = path.join(root, 'platform.cjs'); + writeFileSync(preload, "Object.defineProperty(process, 'platform', { value: 'win32' });\n"); + const result = spawnSync(process.execPath, ['--require', preload, fixture, '--profile', 'windows', '--list'], { + cwd: root, + encoding: 'utf8', + env: { ...process.env }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.stdout.trim().split('\n'), [ + 'check:source', + 'check:tsconfig', + 'export source preview', + 'test:release-tools', + 'build', + 'check:standalone', + 'test:artifact', + 'test:windows', + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('source inventory rejects unregistered, missing and duplicate first-party test gates', () => { const existing = 'packages/example/src/existing.test.ts'; const omitted = 'packages/example/test/omitted.spec.tsx'; diff --git a/test/vitest-suites.json b/test/vitest-suites.json index a36a1b75..0601656f 100644 --- a/test/vitest-suites.json +++ b/test/vitest-suites.json @@ -1,6 +1,9 @@ { "comment": "Vitest suites for the standalone distribution, grouped by the gate that runs them. `vitest.oss.config.mjs` includes every group; `scripts/run-vitest-suite.mjs ` runs one. Add a test file here rather than in package.json or the Vitest config.", "suites": { + "windows": [ + "test/windows-contract.test.mjs" + ], "capability": [ "packages/agent-core/test/unit/bash-subprocess-env.test.ts", "packages/agent-modules/permission/test/unit/permission/bash-policy-regressions.test.ts", diff --git a/test/windows-contract.test.mjs b/test/windows-contract.test.mjs new file mode 100644 index 00000000..f2005805 --- /dev/null +++ b/test/windows-contract.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import { describe, it } from "vitest"; +import { checkWindowsSourceLocation } from "../scripts/check-windows-source-location.mjs"; +import { resolveWslPath } from "../packages/tui/src/host/wsl-path.js"; + +const windowsPath = String.raw`D:\Users\demo\Documents\Screen shots\截图.png`; + +describe.skipIf(process.platform !== "win32")("Windows source contract", () => { + it("accepts the Windows checkout on a local NTFS volume", () => { + assert.deepEqual(checkWindowsSourceLocation(), { + ok: true, + skipped: false, + }); + }); + + it("preserves Windows path syntax on the native host", async () => { + assert.equal(await resolveWslPath(windowsPath), windowsPath); + }); +});