Files
dingMCP/test/distribution.test.ts
T

157 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, /local attachment path.*never a valid upstream attachment field value.*native client/s);
assert.match(createApproval, /Local file paths.*not valid attachment field values.*native client/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.doesNotMatch(builder, /codexPlugin|codex-plugin|\.codex-plugin|openai\.yaml/);
assert.doesNotMatch(builder, /dingtalk-workspace-cli|DWS_URL|DWS_SHA256/);
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, /无法解析/);
});