refactor: reduce approval context overhead
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
This commit is contained in:
+23
-2
@@ -1,11 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import { APPROVAL_ONBOARDING_EXAMPLES, RemoteBridge, bridgeAuthToolResult, createBridgeMcpServer } from "../src/bridge.js";
|
||||
import {
|
||||
APPROVAL_ONBOARDING_EXAMPLES, RemoteBridge, bridgeAuthToolResult, createBridgeMcpServer,
|
||||
syncBundledSkillForLegacyUpdate,
|
||||
} from "../src/bridge.js";
|
||||
|
||||
test("legacy updater post-install syncs the complete bundled Skill", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "ding-bridge-skill-sync-"));
|
||||
const bundled = join(root, "package", "skill", "dingtalk-approval");
|
||||
const statePath = join(root, "client", "state", "session.json");
|
||||
const target = join(root, "client", "skill", "dingtalk-approval");
|
||||
mkdirSync(join(bundled, "references"), { recursive: true });
|
||||
mkdirSync(target, { recursive: true });
|
||||
writeFileSync(join(bundled, "SKILL.md"), "new skill");
|
||||
writeFileSync(join(bundled, "references", "create-approval.md"), "create 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.deepEqual(readdirSync(join(root, "client", "skill")), ["dingtalk-approval"]);
|
||||
});
|
||||
|
||||
test("stdio bridge creates a messenger-friendly device login challenge and protects local state", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
@@ -130,6 +130,7 @@ test("generic installer preserves identity across safe updates, rollback, downgr
|
||||
const paths = clientPaths({ DINGTALK_APPROVAL_CLIENT_HOME: appRoot }, process.platform, join(root, "home"));
|
||||
assert.equal(existsSync(paths.bridgeCommand), true);
|
||||
assert.equal(existsSync(paths.skillFile), true);
|
||||
assert.equal(existsSync(join(paths.skillDir, "references", "create-approval.md")), true);
|
||||
const state = JSON.parse(readFileSync(paths.stateFile, "utf8"));
|
||||
assert.equal(state.version, "1.1.0-rc.5");
|
||||
assert.equal(state.host, undefined);
|
||||
@@ -340,6 +341,10 @@ const target = join(prefix, "node_modules", "dingtalk-approval-client", "dist",
|
||||
mkdirSync(target, { recursive: true });
|
||||
writeFileSync(join(target, "bridge.js"), ${JSON.stringify(fakeBridge)});
|
||||
writeFileSync(join(target, "client-installer.js"), "#!/usr/bin/env node\\n");
|
||||
const skill = join(prefix, "node_modules", "dingtalk-approval-client", "skill", "dingtalk-approval");
|
||||
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");
|
||||
`);
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(join(bin, "npm.cmd"), "@echo off\r\nexit /b 99\r\n");
|
||||
|
||||
@@ -20,7 +20,10 @@ test("customer distribution is a generic single-login gateway bridge", () => {
|
||||
assert.doesNotMatch(installer, /DWS_CLI_PATH|dingtalk-workspace-cli|APP_MODE=local/);
|
||||
|
||||
const skill = readFileSync("skills/dingtalk-approval/SKILL.md", "utf8");
|
||||
assert.match(skill, /XFT recall is experimental.*XFTSYS9999/s);
|
||||
const createApproval = readFileSync("skills/dingtalk-approval/references/create-approval.md", "utf8");
|
||||
assert.match(skill, /read `references\/create-approval\.md` completely/);
|
||||
assert.doesNotMatch(skill, /XFT recall is experimental/);
|
||||
assert.match(createApproval, /XFT recall is experimental.*XFTSYS9999/s);
|
||||
assert.match(skill, /does not send or push.*current user-facing reply/s);
|
||||
assert.match(skill, /handle the installer's `postInstall` result immediately.*waiting for the user's first approval request/s);
|
||||
assert.match(skill, /onboarding\.firstRun=true.*Do not execute an example automatically/s);
|
||||
@@ -29,6 +32,7 @@ test("customer distribution is a generic single-login gateway bridge", () => {
|
||||
assert.match(builder, /schemaVersion: 3,\s+version,/s);
|
||||
assert.match(builder, /signData\(null, manifestBytes/);
|
||||
assert.match(builder, /dingtalk-approval-client/);
|
||||
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/);
|
||||
});
|
||||
|
||||
+12
-6
@@ -140,8 +140,11 @@ test("unified approval API returns opaque user-bound IDs and routes DingTalk act
|
||||
const listed = await service.listTodos(user, "all", 50);
|
||||
const approvalId = String(listed.items[0]!.approvalId);
|
||||
assert.deepEqual(listed.items[0]!.amount, { value: 100, currency: "CNY" });
|
||||
assert.equal(listed.items[0]!.submittedAtLocal, "2026-08-11T09:00:00.000+08:00");
|
||||
assert.equal(listed.items[0]!.submittedTimeZone, "Asia/Shanghai");
|
||||
assert.equal(listed.items[0]!.submittedAt, "2026-08-11T09:00:00.000+08:00");
|
||||
assert.equal("submittedAtLocal" in listed.items[0]!, false);
|
||||
assert.equal("submittedTimeZone" in listed.items[0]!, false);
|
||||
assert.equal("sourceType" in listed.items[0]!, false);
|
||||
assert.equal("actionsHint" in listed.items[0]!, false);
|
||||
assert.match(approvalId, /^v4\./);
|
||||
assert.ok(approvalId.length <= 128, `common DingTalk approvalId should be compact, got ${approvalId.length} characters`);
|
||||
assert.doesNotMatch(approvalId, /instance-secret|task-secret/);
|
||||
@@ -247,12 +250,15 @@ test("combined todo lists are deterministic regardless of source response timing
|
||||
} as unknown as ApprovalService;
|
||||
const xft = {
|
||||
async listMyTodos() {
|
||||
return { items: [{ source: "xft", billId: "2026081300000001", title: "XFT" }] };
|
||||
return { items: [{ source: "xft", billId: "2026081300000001", title: "XFT", applyTime: "2026-08-13T01:30:00.000Z" }] };
|
||||
},
|
||||
};
|
||||
const service = new UnifiedApprovalService(config(), ding, xft as never);
|
||||
const listed = await service.listTodos({ userId: "user-1" }, "all", 2);
|
||||
assert.deepEqual(listed.items.map((item) => item.source), ["dingtalk", "xft"]);
|
||||
assert.equal(listed.items[1]!.submittedAt, "2026-08-13T09:30:00.000+08:00");
|
||||
assert.equal("submittedAtLocal" in listed.items[1]!, false);
|
||||
assert.equal("submittedTimeZone" in listed.items[1]!, false);
|
||||
});
|
||||
|
||||
test("v4 references remain backward compatible with unexpired v2 and v3 approvals", async () => {
|
||||
@@ -309,8 +315,8 @@ test("v4 approval references stay compact for representative DingTalk, Yida, and
|
||||
};
|
||||
const service = new UnifiedApprovalService(config(), ding, xft as never);
|
||||
const listed = await service.listTodos({ userId: "user-1" }, "all", 10);
|
||||
const official = String(listed.items.find((item) => item.sourceType === "official_oa")?.approvalId);
|
||||
const yida = String(listed.items.find((item) => item.sourceType === "yida")?.approvalId);
|
||||
const official = String(listed.items.find((item) => item.businessNo === "5VXimwfMSBSWhjzttVCyWA10041785394274")?.approvalId);
|
||||
const yida = String(listed.items.find((item) => item.businessNo === "FINST-7b7ffc7800d942b7b8f6e70e16efab34")?.approvalId);
|
||||
const xftId = String(listed.items.find((item) => item.source === "xft")?.approvalId);
|
||||
assert.match(official, /^v4\./);
|
||||
assert.ok(official.length <= 128, `DingTalk reference is ${official.length} characters`);
|
||||
@@ -589,7 +595,7 @@ test("unified OA return signs live targets, revalidates them, and confirms task
|
||||
const user = { userId: "user-1", name: "测试用户乙" };
|
||||
const listed = await service.listTodos(user, "dingtalk", 10);
|
||||
assert.deepEqual(listed.items[0]!.availableActions, ["approve", "reject"]);
|
||||
assert.match(String(listed.items[0]!.actionsHint), /approval_get_actions/);
|
||||
assert.equal("actionsHint" in listed.items[0]!, false);
|
||||
const approvalId = String(listed.items[0]!.approvalId);
|
||||
const actions = await service.getActions(user, approvalId);
|
||||
const returnAction = actions.actions[2]! as {
|
||||
|
||||
Reference in New Issue
Block a user