feat: add local approval submission drafts

This commit is contained in:
2026-08-19 15:53:47 +00:00
parent 0a7ad6a3b6
commit 5d889d4445
21 changed files with 176 additions and 25 deletions
+3
View File
@@ -19,12 +19,14 @@ test("legacy updater post-install syncs the complete bundled Skill", () => {
mkdirSync(target, { recursive: true });
writeFileSync(join(bundled, "SKILL.md"), "new skill");
writeFileSync(join(bundled, "references", "create-approval.md"), "create rules");
writeFileSync(join(bundled, "references", "local-drafts.md"), "draft rules");
writeFileSync(join(target, "SKILL.md"), "old skill");
syncBundledSkillForLegacyUpdate(statePath, bundled);
assert.equal(readFileSync(join(target, "SKILL.md"), "utf8"), "new skill");
assert.equal(readFileSync(join(target, "references", "create-approval.md"), "utf8"), "create rules");
assert.equal(readFileSync(join(target, "references", "local-drafts.md"), "utf8"), "draft rules");
assert.deepEqual(readdirSync(join(root, "client", "skill")), ["dingtalk-approval"]);
});
@@ -245,6 +247,7 @@ test("MCP approval_login returns both portable text and a standard resource link
const instructions = client.getInstructions() ?? "";
assert.match(instructions, /登录链接过期.*approval_login.*绝不.*重新安装/);
assert.match(instructions, /审批引用.*调用容量.*内部分批.*不得向用户解释/);
assert.match(instructions, /提单草稿仅可保存在 Agent 所在机器.*不得上传网关.*实时模板 Schema/s);
assert.doesNotMatch(instructions, /最多.*20|selectionId|approvalId|targetId/);
const history = tools.tools.find((tool) => tool.name === "approval_list_history")!;
assert.equal((history.inputSchema as any).properties.startDate.default, "");
+2
View File
@@ -389,6 +389,7 @@ test("generic installer preserves identity across safe updates, rollback, downgr
assert.equal(existsSync(paths.bridgeCommand), true);
assert.equal(existsSync(paths.skillFile), true);
assert.equal(existsSync(join(paths.skillDir, "references", "create-approval.md")), true);
assert.equal(existsSync(join(paths.skillDir, "references", "local-drafts.md")), true);
const state = JSON.parse(readFileSync(paths.stateFile, "utf8"));
assert.equal(state.version, "1.1.0-rc.5");
assert.deepEqual(state.manifestMirrors, [`${mirrorBase}manifest.json`]);
@@ -654,6 +655,7 @@ const skill = join(prefix, "node_modules", "dingtalk-approval-client", "skill",
mkdirSync(join(skill, "references"), { recursive: true });
writeFileSync(join(skill, "SKILL.md"), "---\\nname: dingtalk-approval\\ndescription: Bundled test\\n---\\n");
writeFileSync(join(skill, "references", "create-approval.md"), "# Create approval\\n");
writeFileSync(join(skill, "references", "local-drafts.md"), "# Local drafts\\n");
`);
if (process.platform === "win32") {
writeFileSync(join(bin, "npm.cmd"), "@echo off\r\nexit /b 99\r\n");
+15
View File
@@ -36,6 +36,7 @@ test("customer distribution prefers remote MCP OAuth and keeps a generic compati
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/);
@@ -80,7 +81,9 @@ test("customer distribution prefers remote MCP OAuth and keeps a generic compati
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/);
@@ -90,6 +93,18 @@ test("customer distribution prefers remote MCP OAuth and keeps a generic compati
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);
+6
View File
@@ -46,6 +46,12 @@ test("MCP initialization teaches the workflow while tools/list stays compact", a
assert.match(client.getInstructions() ?? "", /单次条数限制.*内部调用细节.*绝不向用户.*一键全选.*如何分组/s);
assert.match(client.getInstructions() ?? "", /用户只是在查看.*业务信息.*不主动讨论.*自动分批.*不得造成重复确认/s);
assert.match(client.getInstructions() ?? "", /近30天.*allowDuplicateInvoices=true/s);
assert.match(client.getInstructions() ?? "", /草稿只能由 Agent 保存在.*当前 OS 用户私有数据目录/s);
assert.match(client.getInstructions() ?? "", /schemaVersion=1.*dingtalk-approval-local-draft.*submit_uncertain/s);
assert.match(client.getInstructions() ?? "", /不得上传网关.*不得.*完整上游 Schema/s);
assert.match(client.getInstructions() ?? "", /恢复时.*实时读取 Schema/s);
assert.match(client.getInstructions() ?? "", /自动清理超过 90 天/s);
assert.match(client.getInstructions() ?? "", /outcome_uncertain.*冻结.*禁止自动重试/s);
const tools = await client.listTools();
assert.deepEqual(tools.tools.map((tool) => tool.name), [
+14
View File
@@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { APPLICATION_VERSION } from "../src/version.js";
test("package, gateway, and bridge share one application version", () => {
const packageMetadata = JSON.parse(readFileSync("package.json", "utf8")) as { version: string };
assert.equal(APPLICATION_VERSION, packageMetadata.version);
for (const path of ["src/server.ts", "src/bridge.ts"]) {
const source = readFileSync(path, "utf8");
assert.match(source, /version: APPLICATION_VERSION/);
assert.doesNotMatch(source, /version: "\d+\.\d+\.\d+(?:-rc\.\d+)?"/);
}
});