fix: list unadapted XFT approval tasks safely

This commit is contained in:
2026-08-29 18:09:29 +00:00
parent b00c22d74e
commit f301cef3e8
9 changed files with 283 additions and 45 deletions
+59
View File
@@ -388,6 +388,65 @@ test("XFT action restrictions are enforced during zero-write preflight", async (
assert.equal(writes, 0);
});
test("unadapted XFT todos stay visible but detail and every write action remain client-only", async () => {
let detailCalls = 0;
let writes = 0;
const ding = {
async listMyTodos() {
return { items: [{
source: "official_oa", processInstanceId: "mixed-instance", taskId: "mixed-task", title: "钉钉审批", raw: {},
}] };
},
} as unknown as ApprovalService;
const xft = {
async listMyTodos() {
return {
items: [{
source: "xft", taskId: "provider-task-secret", capability: "list_only", unavailableReason: "unsupported_task_type",
title: "薪福通其他审批", allowPass: false, allowReject: false,
}],
diagnostics: { billApiCount: 0, clientOnlyCount: 1 },
};
},
async getTodoDetail() { detailCalls += 1; return {}; },
async listReturnNodes() { detailCalls += 1; return []; },
async processApprovalBatch() { writes += 1; return { results: [{ success: true }] }; },
};
const service = new UnifiedApprovalService(config(), ding, xft as never);
const user = { userId: "user-1" };
const listed = await service.listTodos(user, "all", 10);
assert.equal(listed.returned, 2);
assert.equal("selectionId" in listed, false, "a mixed result containing client-only XFT tasks cannot become a batch selection");
assert.equal(listed.sourceStatus.xft?.billApiCount, 0);
assert.equal(listed.sourceStatus.xft?.clientOnlyCount, 1);
const item = listed.items.find((candidate) => candidate.title === "薪福通其他审批")!;
assert.equal("businessNo" in item, false);
assert.deepEqual(item.availableActions, []);
assert.equal((item.manualAction as Record<string, unknown>).reason, "unsupported_xft_task_type");
const approvalId = String(item.approvalId);
assert.match(approvalId, /^v4\./);
assert.doesNotMatch(approvalId, /provider-task-secret/);
const detail = await service.getDetail(user, approvalId) as unknown as Record<string, unknown>;
assert.equal(detail.status, "client_required");
assert.deepEqual(detail.diagnostics, { realtime: true, persistedBusinessData: false, upstreamDetailRequested: false });
assert.equal(detailCalls, 0, "client-only detail must not call an unverified upstream detail API");
const actions = await service.getActions(user, approvalId);
assert.deepEqual(actions.actions.map((action) => [action.action, action.allowed]), [
["approve", false], ["reject", false], ["return", false],
]);
assert.equal((actions.manualAction as Record<string, unknown>).reason, "unsupported_xft_task_type");
assert.equal(detailCalls, 0, "client-only action discovery must not call an unverified upstream action API");
const processed = await service.processBatch(user, "approve", "同意", [{ approvalId }]);
assert.equal(processed.status, "preflight_failed");
assert.equal(processed.writesStarted, false);
assert.equal(processed.invalidItems?.[0]?.code, "provider_not_supported");
assert.equal(writes, 0);
});
test("approval reference expiry is checked once before the batch starts", async () => {
const originalNow = Date.now;
let now = originalNow();