import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import test from "node:test"; test("customer distribution prefers remote MCP OAuth and keeps a generic compatibility bridge", () => { const packageMetadata = JSON.parse(readFileSync("package.json", "utf8")) as { version: string }; assert.match(packageMetadata.version, /^\d+\.\d+\.\d+-rc\.\d+$/); assert.equal(existsSync("plugins/dingtalk-approval"), false); const installer = readFileSync("distribution/agent-install.template.md", "utf8"); assert.match(installer, /远程.*Streamable HTTP MCP/s); assert.match(installer, /URL:`\{\{GATEWAY_URL\}\}`/); assert.match(installer, /认证:OAuth/); assert.match(installer, /动态客户端注册/); assert.match(installer, /127\.0\.0\.1.*localhost.*同一台设备/s); assert.match(installer, /SSH.*无头终端.*Messenger.*手机或另一台电脑/s); assert.match(installer, /无法确定.*只问用户一个问题/s); assert.match(installer, /不要安装 Node\.js、npm、本地 Bridge/); assert.match(installer, /只有.*不支持远程 Streamable HTTP MCP.*标准 OAuth.*无法.*本机回调.*兼容/s); assert.match(installer, /device-code.*手机或另一台电脑/s); assert.match(installer, /跨设备交互.*直接进入兼容路径.*不必先制造.*失败/s); assert.match(installer, /COMPAT_INSTALL_URL/); assert.match(installer, /approval_list_todos.*source="all".*limit=1/s); assert.match(installer, /钉钉 OA 与薪福通来源都为 `ok`/); assert.doesNotMatch(installer, /PowerShell|Git Bash|stdio|postInstall|loginCommand|npm install -g/); assert.doesNotMatch(installer, /--host|WorkBuddy|Codex|Claude/); const compatibilityInstaller = readFileSync("distribution/agent-compat-install.template.md", "utf8"); assert.match(compatibilityInstaller, /仅当.*不支持远程 Streamable HTTP MCP.*标准 OAuth.*无法.*本机 OAuth 回调/s); assert.match(compatibilityInstaller, /127\.0\.0\.1.*localhost.*SSH.*无头环境.*Messenger.*device-code/s); assert.match(compatibilityInstaller, /Ed25519/); assert.match(compatibilityInstaller, /使用 `mcpConfig`/); assert.match(compatibilityInstaller, /Windows(PowerShell)/); assert.match(compatibilityInstaller, /curl\.exe[\s\S]*Invoke-WebRequest[\s\S]*Get-FileHash[\s\S]*ConvertFrom-Json/); assert.match(compatibilityInstaller, /90 秒[\s\S]*stderr[\s\S]*stdout/); assert.match(compatibilityInstaller, /登录链接过期.*绝不能重新安装.*approval_login.*loginCommand/s); assert.match(compatibilityInstaller, /sourceStatus[\s\S]*dingtalk\.status[\s\S]*xft\.status[\s\S]*都必须是 `ok`/); assert.match(compatibilityInstaller, /references\/local-drafts\.md/); assert.match(compatibilityInstaller, /更新[\s\S]*update --json[\s\S]*卸载[\s\S]*uninstall --yes --json/); assert.doesNotMatch(compatibilityInstaller, /TimeoutSec 300|--max-time 300|--timeout=300|至少 10 分钟超时/); assert.doesNotMatch(compatibilityInstaller, /--host|WorkBuddy|Codex|Claude/); assert.doesNotMatch(compatibilityInstaller, /DWS_CLI_PATH|dingtalk-workspace-cli|APP_MODE=local/); const runtime = JSON.parse(readFileSync("distribution/node-runtime.json", "utf8")); assert.equal(runtime.version, "22.23.2"); assert.deepEqual(Object.keys(runtime.platforms).sort(), [ "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64", ]); assert.equal(runtime.sourceBases.length, 3); assert.ok(runtime.sourceBases.every((value: string) => value.includes(`/v${runtime.version}/`) && !value.includes("latest"))); assert.ok(Object.values(runtime.platforms).every((value) => /^[a-f0-9]{64}$/.test((value as { sha256: string }).sha256))); const runtimeVerifier = readFileSync("scripts/verify-node-runtime-sources.mjs", "utf8"); assert.match(runtimeVerifier, /spawnSync\("curl"[\s\S]*curlAvailable/); assert.match(runtimeVerifier, /--connect-timeout", "10"[\s\S]*--max-time", "20"/); assert.match(runtimeVerifier, /if \(!curlAvailable\)[\s\S]*fetch\(/); const windowsBootstrap = readFileSync("distribution/bootstrap-windows.template.ps1", "utf8"); const posixBootstrap = readFileSync("distribution/bootstrap-posix.template.sh", "utf8"); assert.match(windowsBootstrap, /OSArchitecture[\s\S]*Save-DingDownload[\s\S]*Expand-Archive/); assert.match(windowsBootstrap, /SHA-256 不匹配,停止安装/); assert.match(windowsBootstrap, /Write-DingProgress[\s\S]*\[dingtalk-approval\]/); assert.match(windowsBootstrap, /UTF8Encoding\(\$false\)[\s\S]*Console\]::InputEncoding[\s\S]*Console\]::OutputEncoding[\s\S]*\$OutputEncoding/); assert.match(windowsBootstrap, /dingInstallerUrls[\s\S]*dingManifestMirrorUrls[\s\S]*use-env-proxy/s); assert.match(windowsBootstrap, /curl\.exe[\s\S]*--connect-timeout 10[\s\S]*--max-time 90/); assert.match(windowsBootstrap, /-e "const \[a,b\]=process\.versions\.node\.split\('\.'\)/); assert.match(windowsBootstrap, /-e "process\.exit\(process\.allowedNodeEnvironmentFlags\.has\('--use-env-proxy'\)/); assert.match(windowsBootstrap, /dingRuntimeName = \[IO\.Path\]::GetFileNameWithoutExtension\(\$dingNodeFile\)/); assert.doesNotMatch(windowsBootstrap, /GetFileNameWithoutExtension\(\[IO\.Path\]::GetFileNameWithoutExtension/); assert.doesNotMatch(windowsBootstrap, /TimeoutSec 300|-e '.*split\("\."\)/); assert.match(posixBootstrap, /^#!\/bin\/sh\nset -eu/); assert.match(posixBootstrap, /uname -s[\s\S]*uname -m[\s\S]*tar -xzf/); assert.match(posixBootstrap, /SHA-256 不匹配,停止安装/); assert.match(posixBootstrap, /ding_progress[\s\S]*\[dingtalk-approval\]/); assert.match(posixBootstrap, /ding_installer_urls[\s\S]*ding_manifest_mirror_urls[\s\S]*use-env-proxy/s); assert.match(posixBootstrap, /--connect-timeout 10[\s\S]*--max-time 90[\s\S]*--speed-time 20/); assert.doesNotMatch(posixBootstrap, /--max-time 300|--timeout=300/); assert.equal(spawnSync("sh", ["-n", "distribution/bootstrap-posix.template.sh"]).status, 0); assert.doesNotMatch(`${windowsBootstrap}\n${posixBootstrap}`, /latest-v|npm install -g/); const skill = readFileSync("skills/dingtalk-approval/SKILL.md", "utf8"); const createApproval = readFileSync("skills/dingtalk-approval/references/create-approval.md", "utf8"); const localDrafts = readFileSync("skills/dingtalk-approval/references/local-drafts.md", "utf8"); assert.match(skill, /read `references\/create-approval\.md` completely/); assert.match(skill, /read `references\/local-drafts\.md` completely/); assert.doesNotMatch(skill, /XFT recall is experimental/); assert.match(createApproval, /XFT recall is experimental.*XFTSYS9999/s); assert.match(skill, /native remote-MCP OAuth flow when the user can complete its registered callback/); assert.match(skill, /127\.0\.0\.1.*localhost.*same device.*SSH.*headless.*Messenger.*cross-device.*STDIO Bridge device-code/s); assert.match(skill, /both DingTalk OA and XFT must be available/); assert.match(skill, /cannot use remote Streamable HTTP MCP or standard OAuth.*cross-device users.*loopback callback.*local STDIO Bridge/s); assert.match(skill, /authChallenge\.directUrl.*current reply/s); assert.match(skill, /onboarding\.firstRun=true.*Do not execute an example automatically/s); assert.match(skill, /link expires.*approval_login.*Never reinstall/s); assert.match(localDrafts, /%LOCALAPPDATA%\\DingTalkApproval\\drafts/); assert.match(localDrafts, /Library\/Application Support\/DingTalkApproval\/drafts/); assert.match(localDrafts, /\.local\/share.*dingtalk-approval\/drafts/s); assert.match(localDrafts, /mode `0700`.*mode `0600`/s); assert.match(localDrafts, /atomically rename/); assert.match(localDrafts, /more than 90 days old/); assert.match(localDrafts, /SHA-256/); assert.match(localDrafts, /Never store[\s\S]*templateId[\s\S]*confirmationId[\s\S]*complete upstream schema/); assert.match(localDrafts, /outcome_uncertain[\s\S]*freeze[\s\S]*Never prepare or submit it again automatically/); assert.match(localDrafts, /never on the approval gateway/); assert.match(localDrafts, /localPath.*signed Bridge.*does not upload.*final confirmation.*native-client completion/s); assert.match(createApproval, /Local file paths.*not valid `values` or `customFieldValues`.*signed Bridge `attachments`.*submit uploads only after final confirmation.*native client/s); assert.match(createApproval, /every XFT selection.*label-only.*optionSource.*returned `label` values.*`code` only inside/s); assert.match(createApproval, /Never show, explain, or ask.*city code.*organization code.*employee number.*numeric cabin value.*`expenseNumber`.*`expTypeCode`/s); assert.match(createApproval, /selectionCapability\.status=client_only.*required field stops.*optional field may be skipped/s); assert.match(localDrafts, /every stored selection value.*internal form value.*only that label.*never expose stored city.*department.*expense.*internal values/s); const builder = readFileSync("scripts/build-release.mjs", "utf8"); assert.match(builder, /schemaVersion: 3,\s+version,/s); assert.match(builder, /download-mirror-base-url[\s\S]*manifestMirrors[\s\S]*mirrors:/s); assert.match(builder, /signData\(null, manifestBytes/); assert.match(builder, /dingtalk-approval-client/); assert.match(builder, /bootstrap-windows\.template\.ps1/); assert.match(builder, /bootstrap-posix\.template\.sh/); assert.match(builder, /agent-compat-install\.template\.md/); assert.match(builder, /compat-install\.md/); assert.match(builder, /utf8Bom: true/); assert.match(builder, /validateNodeRuntime/); assert.match(builder, /cpSync\(resolve\("skills\/dingtalk-approval"\).*recursive: true/s); assert.match(builder, /"approval-contract\.js", "version\.js"/); assert.doesNotMatch(builder, /codexPlugin|codex-plugin|\.codex-plugin|openai\.yaml/); assert.doesNotMatch(builder, /dingtalk-workspace-cli|DWS_URL|DWS_SHA256/); const releaseVerifier = readFileSync("scripts/verify-release.mjs", "utf8"); assert.match(releaseVerifier, /assertRelativeModuleClosure\(join\(root, "client", "package", "dist", "src"\)\)/); assert.match(releaseVerifier, /客户端模块依赖缺失/); const deploy = readFileSync("deploy/install.sh", "utf8"); assert.match(deploy, /--download-base-url "\$\{gitea_release_base_url\}"[\s\S]*--download-mirror-base-url "\$\{public_download_base_url\}"/s); assert.match(deploy, /refs\/tags\/\$\{release_tag\}[\s\S]*release_created_at_commit="\$\{release_tag_commit:-\$\{release_commit\}\}"/s); assert.match(deploy, /! -name 'agent-test-install\.md' ! -name 'dingmcp-test-bundle\.tar\.gz'/); const testBundleBuilder = readFileSync("scripts/build-test-bundle.mjs", "utf8"); assert.match(testBundleBuilder, /--sort=name.*--mtime=@0.*--owner=0.*--group=0.*--numeric-owner.*--format=ustar/s); }); test("public install page explicitly declares UTF-8 for browser rendering", () => { const nginx = readFileSync("deploy/approval.justconn.top.nginx.conf", "utf8"); for (const name of ["install", "compat-install"]) { const location = nginx.match(new RegExp(`location = /downloads/dingmcp/${name}\\.md \\{([\\s\\S]*?)\\n \\}`))?.[1] ?? ""; assert.match(location, /default_type text\/markdown;/); assert.match(location, /charset utf-8;/); assert.match(location, /charset_types text\/markdown;/); } }); test("Gitea release lookup tolerates tea field-name corruption and fails closed", () => { const lookup = (body: string, tag = "v1.1.0-rc.11") => spawnSync( process.execPath, ["scripts/release-tag-exists.mjs", tag], { input: body, encoding: "utf8" }, ); assert.equal(lookup('[{"tag_name":"v1.1.0-rc.11"}]').status, 0); assert.equal(lookup('[{"tag-_name":"v1.1.0-rc.11"}]').status, 0); assert.equal(lookup('{"data":[{"Tag":"v1.1.0-rc.11"}]}').status, 0); assert.equal(lookup('[{"tag-_name":"v1.1.0-rc.10"}]').status, 1); const invalid = lookup("not json"); assert.equal(invalid.status, 2); assert.match(invalid.stderr, /无法解析/); });