792 lines
30 KiB
JavaScript
792 lines
30 KiB
JavaScript
"use strict";
|
||
|
||
const test = require("node:test");
|
||
const assert = require("node:assert/strict");
|
||
const pet = require("../pet-runtime.js");
|
||
const bridge = require("../bridge-client.js");
|
||
const wireContract = require("../../Contracts/erp-agent-wire-contract-v1.json");
|
||
|
||
test("Codex v1 atlas geometry and all nine rows are exact", () => {
|
||
assert.deepEqual(pet.ATLAS, {
|
||
columns: 8,
|
||
rows: 9,
|
||
frameWidth: 192,
|
||
frameHeight: 208,
|
||
width: 1536,
|
||
height: 1872
|
||
});
|
||
assert.equal(pet.ANIMATIONS.idle.row, 0);
|
||
assert.equal(pet.ANIMATIONS["running-right"].frames, 8);
|
||
assert.equal(pet.ANIMATIONS["running-left"].row, 2);
|
||
assert.deepEqual(pet.ANIMATIONS.waiting.durations, [150, 150, 150, 150, 150, 260]);
|
||
assert.equal(pet.ANIMATIONS.review.row, 8);
|
||
});
|
||
|
||
test("semantic ERP states map to visible pet states", () => {
|
||
assert.equal(pet.animationForSemanticState("thinking"), "review");
|
||
assert.equal(pet.animationForSemanticState("awaiting_confirmation"), "waiting");
|
||
assert.equal(pet.animationForSemanticState("executing"), "running");
|
||
assert.equal(pet.animationForSemanticState("success"), "jumping");
|
||
assert.equal(pet.animationForSemanticState("error"), "failed");
|
||
assert.throws(() => pet.animationForSemanticState("write_without_confirmation"), /Unknown pet state/);
|
||
});
|
||
|
||
test("frame CSS selects the correct atlas cell", () => {
|
||
const first = pet.frameStyle("idle", 0, "asset.webp");
|
||
const last = pet.frameStyle("review", 5, "asset.webp");
|
||
assert.equal(first.backgroundSize, "800% 900%");
|
||
assert.equal(first.backgroundPosition, "0% 0%");
|
||
assert.equal(last.backgroundPosition, (5 / 7) * 100 + "% 100%");
|
||
assert.throws(() => pet.frameStyle("waving", 4, "asset.webp"), RangeError);
|
||
});
|
||
|
||
test("animator follows per-frame duration and cancels stale schedules", () => {
|
||
const scheduled = [];
|
||
const element = { style: {}, dataset: {} };
|
||
const animator = new pet.AtlasAnimator(element, {
|
||
setTimer(callback, delay) {
|
||
scheduled.push({ callback, delay });
|
||
return scheduled.length;
|
||
},
|
||
clearTimer() {}
|
||
});
|
||
animator.play("waving");
|
||
assert.equal(scheduled[0].delay, 140);
|
||
scheduled[0].callback();
|
||
assert.equal(element.dataset.frame, "1");
|
||
animator.play("waiting");
|
||
assert.equal(element.dataset.animation, "waiting");
|
||
scheduled[1].callback();
|
||
assert.equal(element.dataset.animation, "waiting");
|
||
});
|
||
|
||
test("bridge client keeps one idempotency key for retries of the same plan", async () => {
|
||
const requests = [];
|
||
const transport = {
|
||
async send(request) {
|
||
requests.push(request);
|
||
return validResponse(request, executionData());
|
||
}
|
||
};
|
||
const client = new bridge.BridgeClient(transport, { clientSessionId: "session-test" });
|
||
const planId = "0123456789abcdef0123456789abcdef";
|
||
const correlationId = "plan-correlation-01234567";
|
||
await client.execute(planId, correlationId);
|
||
await client.execute(planId, correlationId);
|
||
assert.equal(requests.length, 2);
|
||
assert.equal(requests[0].payload.idempotencyKey, requests[1].payload.idempotencyKey);
|
||
assert.equal(requests[0].clientSessionId, "session-test");
|
||
assert.equal(requests[0].correlationId, correlationId);
|
||
assert.equal(requests[1].correlationId, correlationId);
|
||
});
|
||
|
||
test("bridge client gives a followup plan an independent idempotency key", async () => {
|
||
const requests = [];
|
||
const transport = {
|
||
async send(request) {
|
||
requests.push(request);
|
||
return validResponse(request, executionData());
|
||
}
|
||
};
|
||
const client = new bridge.BridgeClient(transport, { clientSessionId: "session-followup" });
|
||
await client.execute(
|
||
"0123456789abcdef0123456789abcdef",
|
||
"plan-correlation-01234567");
|
||
await client.execute(
|
||
"fedcba9876543210fedcba9876543210",
|
||
"plan-correlation-01234567");
|
||
assert.notEqual(requests[0].payload.idempotencyKey, requests[1].payload.idempotencyKey);
|
||
});
|
||
|
||
test("bridge client binds followup plans to the trusted ERP correlation", async () => {
|
||
const correlationId = "plan-correlation-76543210";
|
||
const followupPlan = {
|
||
planId: "fedcba9876543210fedcba9876543210",
|
||
commandName: "hr.leave.submit"
|
||
};
|
||
const client = new bridge.BridgeClient({
|
||
async send(request) {
|
||
return validResponse(request, executionData({
|
||
followupPlan: { ...followupPlan, bridgeCorrelationId: "forged-correlation" }
|
||
}));
|
||
}
|
||
}, { clientSessionId: "session-correlated-followup" });
|
||
|
||
const data = await client.execute(
|
||
"0123456789abcdef0123456789abcdef",
|
||
correlationId);
|
||
assert.equal(data.bridgeCorrelationId, correlationId);
|
||
assert.equal(data.followupPlan.bridgeCorrelationId, correlationId);
|
||
assert.equal(data.followupPlan.planId, followupPlan.planId);
|
||
});
|
||
|
||
test("bridge client binds response protocol request and correlation", async () => {
|
||
const mismatches = [
|
||
request => ({ ...validResponse(request, {}), protocolVersion: "2.0" }),
|
||
request => ({ ...validResponse(request, {}), requestId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }),
|
||
request => ({ ...validResponse(request, {}), correlationId: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }),
|
||
request => ({ ...validResponse(request, {}), unexpected: true }),
|
||
request => ({ ...validResponse(request, {}), success: 1 })
|
||
];
|
||
for (const makeResponse of mismatches) {
|
||
const client = new bridge.BridgeClient({
|
||
async send(request) { return makeResponse(request); }
|
||
}, { clientSessionId: "session-protocol" });
|
||
await assert.rejects(
|
||
client.health(),
|
||
error => error && error.code === "bridge_protocol_error"
|
||
);
|
||
}
|
||
});
|
||
|
||
test("bridge client preserves a valid business error only after binding", async () => {
|
||
const recovery = {
|
||
action: "inspect_existing_record",
|
||
retryable: false,
|
||
planInvalidated: true,
|
||
message: "先查询现有发票,再重新生成预览。"
|
||
};
|
||
const client = new bridge.BridgeClient({
|
||
async send(request) {
|
||
return {
|
||
...validResponse(request, { recovery }),
|
||
success: false,
|
||
code: "duplicate_invoice",
|
||
message: "该供应商的发票号码已经存在。"
|
||
};
|
||
}
|
||
}, { clientSessionId: "session-business-error" });
|
||
let boundError;
|
||
await assert.rejects(
|
||
client.health(),
|
||
error => {
|
||
boundError = error;
|
||
return error
|
||
&& error.code === "duplicate_invoice"
|
||
&& error.message === "该供应商的发票号码已经存在。"
|
||
&& error.response.data.recovery.action === "inspect_existing_record";
|
||
}
|
||
);
|
||
const summary = pet.bridgeFailureSummary(boundError);
|
||
assert.equal(summary.planInvalidated, true);
|
||
assert.equal(summary.retryable, false);
|
||
assert.match(summary.text, /下一步:先查询现有发票.*错误码:duplicate_invoice/);
|
||
assert.match(summary.text, /关联 ID:/);
|
||
const timeout = pet.bridgeFailureSummary({
|
||
code: "bridge_timeout",
|
||
message: "untrusted SQL SELECT password"
|
||
});
|
||
assert.equal(timeout.retryable, true);
|
||
assert.equal(timeout.planInvalidated, false);
|
||
assert.doesNotMatch(timeout.text, /password|SELECT/);
|
||
|
||
const malformedRecoveries = [
|
||
{},
|
||
{ recovery: { ...recovery, action: "run_sql" } },
|
||
{ recovery: { ...recovery, retryable: "false" } },
|
||
{ recovery: { ...recovery, sql: "select secret" } },
|
||
{ recovery: { ...recovery, message: "下一步\n执行" } }
|
||
];
|
||
for (const data of malformedRecoveries) {
|
||
const malformedClient = new bridge.BridgeClient({
|
||
async send(request) {
|
||
return {
|
||
...validResponse(request, data),
|
||
success: false,
|
||
code: "duplicate_invoice",
|
||
message: "该供应商的发票号码已经存在。"
|
||
};
|
||
}
|
||
}, { clientSessionId: "session-malformed-recovery" });
|
||
await assert.rejects(
|
||
malformedClient.health(),
|
||
error => error && error.code === "bridge_protocol_error"
|
||
);
|
||
}
|
||
});
|
||
|
||
test("diagnostic execution receipt is strict and gives a safe next-turn summary", async () => {
|
||
const correlationId = "diagnostic-correlation-01234567";
|
||
const client = new bridge.BridgeClient({
|
||
async send(request) {
|
||
return validResponse(
|
||
request,
|
||
diagnosticExecutionData(request.correlationId));
|
||
}
|
||
}, { clientSessionId: "session-diagnostic-result" });
|
||
const data = await client.execute(
|
||
"0123456789abcdef0123456789abcdef",
|
||
correlationId);
|
||
const summary = pet.diagnosticExecutionSummary(data.result);
|
||
assert.match(summary, /数据库字段不存在.*下一轮对话.*具体哪里配置错了/);
|
||
assert.doesNotMatch(summary, /password|SELECT|physical_table/i);
|
||
|
||
const malformedClient = new bridge.BridgeClient({
|
||
async send(request) {
|
||
const responseData = diagnosticExecutionData(request.correlationId);
|
||
responseData.result.data.sql = "SELECT password FROM physical_table";
|
||
return validResponse(request, responseData);
|
||
}
|
||
}, { clientSessionId: "session-diagnostic-injection" });
|
||
await assert.rejects(
|
||
malformedClient.execute(
|
||
"fedcba9876543210fedcba9876543210",
|
||
correlationId),
|
||
error => error && error.code === "bridge_protocol_error"
|
||
);
|
||
});
|
||
|
||
test("bridge client rejects invalid request shapes before transport", async () => {
|
||
let calls = 0;
|
||
const client = new bridge.BridgeClient({
|
||
async send(request) {
|
||
calls += 1;
|
||
return validResponse(request, {});
|
||
}
|
||
}, { clientSessionId: "session-request-shape" });
|
||
await assert.rejects(client.plan("module.diagnose", []), /计划请求格式无效/);
|
||
await assert.rejects(
|
||
client.execute("not-a-server-plan", "plan-correlation-01234567"),
|
||
/执行请求格式无效/);
|
||
await assert.rejects(
|
||
client.execute("0123456789abcdef0123456789abcdef"),
|
||
/执行请求格式无效/);
|
||
assert.equal(calls, 0);
|
||
});
|
||
|
||
test("atlas validation rejects dimensions that could crop guga", () => {
|
||
assert.equal(pet.validateAtlas(1536, 1872), true);
|
||
assert.equal(pet.validateAtlas(1536, 1792), false);
|
||
});
|
||
|
||
test("anchor click focuses a hover-opened panel before allowing toggle close", () => {
|
||
assert.equal(pet.panelOpenAfterAnchorClick(false, false), true);
|
||
assert.equal(pet.panelOpenAfterAnchorClick(true, true), true);
|
||
assert.equal(pet.panelOpenAfterAnchorClick(true, false), false);
|
||
assert.throws(
|
||
() => pet.panelOpenAfterAnchorClick("open", false),
|
||
TypeError);
|
||
});
|
||
|
||
test("pet quick actions are fixed context-aware prompts", () => {
|
||
assert.match(
|
||
pet.quickActionPrompt("purchase_invoice_entry"),
|
||
/先读取当前 ERP 上下文和能力.*不要生成写入计划/
|
||
);
|
||
assert.match(
|
||
pet.quickActionPrompt("leave_request"),
|
||
/原始日期表达与时段.*不得替我补全或猜测/
|
||
);
|
||
assert.equal(
|
||
pet.quickActionPrompt("current_module_help"),
|
||
"当前 ERP 界面有哪些功能?请先读取实时上下文,并只根据当前模块的用户级功能说明回答。"
|
||
);
|
||
assert.match(
|
||
pet.quickActionPrompt("current_module_diagnosis"),
|
||
/只读配置诊断.*不要复现初始化.*不要生成修复 SQL/
|
||
);
|
||
assert.match(
|
||
pet.quickActionPrompt("workflow_readiness"),
|
||
/ERP 内置管理员.*adapters\.status.*不要泄露配置证据、SQL 或物理字段/
|
||
);
|
||
assert.throws(
|
||
() => pet.quickActionPrompt("execute_current_module"),
|
||
/Unknown pet quick action/
|
||
);
|
||
});
|
||
|
||
test("admin workflow readiness quick action requires trusted administrator scope", () => {
|
||
const context = {
|
||
userId: "1",
|
||
userName: "管理员",
|
||
accountBook: "测试账套",
|
||
subSystemId: "SCM",
|
||
subSystemName: "供应链",
|
||
databaseScopeFingerprint: "b".repeat(64),
|
||
isAdministrator: true
|
||
};
|
||
assert.equal(pet.quickActionVisible("workflow_readiness", context), true);
|
||
assert.equal(pet.quickActionVisible("purchase_invoice_entry", null), true);
|
||
|
||
const ordinaryUser = structuredClone(context);
|
||
ordinaryUser.userId = "2";
|
||
ordinaryUser.userName = "普通用户";
|
||
ordinaryUser.isAdministrator = false;
|
||
assert.equal(pet.quickActionVisible("workflow_readiness", ordinaryUser), false);
|
||
|
||
const malformedAdministrator = structuredClone(context);
|
||
malformedAdministrator.databaseScopeFingerprint = "B".repeat(64);
|
||
assert.equal(
|
||
pet.quickActionVisible("workflow_readiness", malformedAdministrator),
|
||
false
|
||
);
|
||
assert.throws(
|
||
() => pet.quickActionVisible("unknown_action", context),
|
||
/Unknown pet quick action/
|
||
);
|
||
});
|
||
|
||
test("ERP session scope summary is complete bounded and database-evidence only", () => {
|
||
const context = {
|
||
userId: "USER-001",
|
||
userName: "测试用户",
|
||
accountBook: "生产账套甲",
|
||
subSystemId: "SCM-01",
|
||
subSystemName: "采购管理",
|
||
databaseScopeFingerprint: "a".repeat(64),
|
||
isAdministrator: false
|
||
};
|
||
const summary = pet.erpSessionScopeSummary(context);
|
||
assert.equal(summary.complete, true);
|
||
assert.equal(summary.userId, "USER-001");
|
||
assert.equal(summary.accountBook, "生产账套甲");
|
||
assert.equal(summary.subSystemId, "SCM-01");
|
||
assert.equal(summary.databaseEvidence, "a".repeat(12));
|
||
assert.equal("databaseScopeFingerprint" in summary, false);
|
||
|
||
const missingAccount = structuredClone(context);
|
||
delete missingAccount.accountBook;
|
||
assert.equal(pet.erpSessionScopeSummary(missingAccount).complete, false);
|
||
|
||
const uppercaseFingerprint = structuredClone(context);
|
||
uppercaseFingerprint.databaseScopeFingerprint = "A".repeat(64);
|
||
assert.equal(
|
||
pet.erpSessionScopeSummary(uppercaseFingerprint).complete,
|
||
false);
|
||
|
||
const controlText = structuredClone(context);
|
||
controlText.userName = "测试\u0085用户";
|
||
assert.equal(pet.erpSessionScopeSummary(controlText).complete, false);
|
||
|
||
const forgedAdministrator = structuredClone(context);
|
||
forgedAdministrator.isAdministrator = "false";
|
||
assert.equal(
|
||
pet.erpSessionScopeSummary(forgedAdministrator).complete,
|
||
false);
|
||
assert.equal(pet.erpSessionScopeSummary(null).complete, false);
|
||
});
|
||
|
||
test("purchase execution requires complete trusted header and line previews", () => {
|
||
const plan = structuredClone(wireContract.scenarios.find(
|
||
scenario => scenario.name === "purchase_resolve_to_create").plan);
|
||
const preview = pet.purchaseLinePreview(plan);
|
||
assert.equal(preview.required, true);
|
||
assert.equal(preview.complete, true);
|
||
assert.equal(preview.header.invoiceNumber, "INV-CONTRACT-1");
|
||
assert.equal(preview.header.sourceDocumentCount, 1);
|
||
assert.equal(preview.lines[0].sourceOrderNumber, "PO-1");
|
||
assert.equal(pet.isExecutablePlan(plan), true);
|
||
|
||
const missingSupplier = structuredClone(plan);
|
||
delete missingSupplier.preview["供应商"];
|
||
delete missingSupplier.data.preview["供应商"];
|
||
assert.equal(pet.purchaseLinePreview(missingSupplier).complete, false);
|
||
assert.equal(pet.isExecutablePlan(missingSupplier), false);
|
||
|
||
const forgedHeaderTotal = structuredClone(plan);
|
||
forgedHeaderTotal.preview["价税合计"] = 999;
|
||
forgedHeaderTotal.data.preview["价税合计"] = 999;
|
||
assert.equal(pet.purchaseLinePreview(forgedHeaderTotal).complete, false);
|
||
assert.equal(pet.isExecutablePlan(forgedHeaderTotal), false);
|
||
|
||
const forgedLineCount = structuredClone(plan);
|
||
forgedLineCount.preview["发票行数"] = 2;
|
||
forgedLineCount.data.preview["发票行数"] = 2;
|
||
assert.equal(pet.purchaseLinePreview(forgedLineCount).complete, false);
|
||
assert.equal(pet.isExecutablePlan(forgedLineCount), false);
|
||
|
||
const missingQuantity = structuredClone(plan);
|
||
delete missingQuantity.data.lineMatches[0].invoiceQuantity;
|
||
assert.equal(pet.purchaseLinePreview(missingQuantity).complete, false);
|
||
assert.equal(pet.isExecutablePlan(missingQuantity), false);
|
||
|
||
const ambiguous = structuredClone(plan);
|
||
ambiguous.data.lineMatches[0].candidateCount = 2;
|
||
assert.equal(pet.purchaseLinePreview(ambiguous).complete, false);
|
||
assert.equal(pet.isExecutablePlan(ambiguous), false);
|
||
|
||
assert.equal(pet.previewReviewComplete(0, 340, 800), false);
|
||
assert.equal(pet.previewReviewComplete(458, 340, 800), true);
|
||
assert.equal(pet.previewReviewComplete(460, 340, 800), true);
|
||
assert.equal(pet.previewReviewComplete(Number.NaN, 340, 800), false);
|
||
});
|
||
|
||
test("leave create and submit require complete trusted confirmation previews", () => {
|
||
const createPlan = wireContract.scenarios.find(
|
||
scenario => scenario.name === "leave_resolve_to_create").plan;
|
||
const createPreview = pet.leaveConfirmationPreview(createPlan);
|
||
assert.equal(createPreview.required, true);
|
||
assert.equal(createPreview.complete, true);
|
||
assert.equal(createPreview.employeeId, "EMP-1");
|
||
assert.equal(createPreview.calculatedHours, 4);
|
||
assert.equal(pet.isExecutablePlan(createPlan), true);
|
||
|
||
const unicodeErpCode = structuredClone(createPlan);
|
||
unicodeErpCode.preview["员工"] = "员工一号";
|
||
unicodeErpCode.data.preview["员工"] = "员工一号";
|
||
assert.equal(pet.leaveConfirmationPreview(unicodeErpCode).complete, true);
|
||
|
||
const missingReason = structuredClone(createPlan);
|
||
delete missingReason.preview["原因"];
|
||
delete missingReason.data.preview["原因"];
|
||
assert.equal(pet.leaveConfirmationPreview(missingReason).complete, false);
|
||
assert.equal(pet.isExecutablePlan(missingReason), false);
|
||
|
||
const forgedSubmitIntent = structuredClone(createPlan);
|
||
forgedSubmitIntent.preview["创建后提交"] = true;
|
||
forgedSubmitIntent.data.preview["创建后提交"] = true;
|
||
assert.equal(pet.leaveConfirmationPreview(forgedSubmitIntent).complete, false);
|
||
assert.equal(pet.isExecutablePlan(forgedSubmitIntent), false);
|
||
|
||
const zonedTime = structuredClone(createPlan);
|
||
zonedTime.preview["开始时间"] = "2026-08-12T13:00:00+08:00";
|
||
zonedTime.data.preview["开始时间"] = "2026-08-12T13:00:00+08:00";
|
||
assert.equal(pet.leaveConfirmationPreview(zonedTime).complete, false);
|
||
|
||
const submitPlan = wireContract.scenarios.find(
|
||
scenario => scenario.name === "leave_submit_followup").plan;
|
||
assert.equal(pet.leaveConfirmationPreview(submitPlan).complete, true);
|
||
assert.equal(pet.isExecutablePlan(submitPlan), true);
|
||
const forgedAction = structuredClone(submitPlan);
|
||
forgedAction.preview["动作"] = "删除申请";
|
||
forgedAction.data.preview["动作"] = "删除申请";
|
||
assert.equal(pet.leaveConfirmationPreview(forgedAction).complete, false);
|
||
assert.equal(pet.isExecutablePlan(forgedAction), false);
|
||
});
|
||
|
||
test("initialization trace requires complete scope limits and risk disclosure", () => {
|
||
const plan = wireContract.scenarios.find(
|
||
scenario => scenario.name === "module_trace_initialization").plan;
|
||
const preview = pet.initializationTracePreview(plan);
|
||
assert.equal(preview.required, true);
|
||
assert.equal(preview.complete, true);
|
||
assert.equal(preview.moduleName, "采购订单");
|
||
assert.equal(preview.forceTerminationSupported, false);
|
||
assert.equal(pet.isExecutablePlan(plan), true);
|
||
|
||
const missingModuleName = structuredClone(plan);
|
||
delete missingModuleName.preview.moduleName;
|
||
delete missingModuleName.data.preview.moduleName;
|
||
assert.equal(pet.initializationTracePreview(missingModuleName).complete, false);
|
||
assert.equal(pet.isExecutablePlan(missingModuleName), false);
|
||
|
||
const forcedTermination = structuredClone(plan);
|
||
forcedTermination.preview.forceTerminationSupported = true;
|
||
forcedTermination.data.preview.forceTerminationSupported = true;
|
||
assert.equal(pet.initializationTracePreview(forcedTermination).complete, false);
|
||
assert.equal(pet.isExecutablePlan(forcedTermination), false);
|
||
|
||
const changedLimit = structuredClone(plan);
|
||
changedLimit.preview.maxEvents = 201;
|
||
changedLimit.data.preview.maxEvents = 201;
|
||
changedLimit.data.maxEvents = 201;
|
||
assert.equal(pet.initializationTracePreview(changedLimit).complete, false);
|
||
|
||
const missingWarning = structuredClone(plan);
|
||
missingWarning.warnings = [];
|
||
assert.equal(pet.initializationTracePreview(missingWarning).complete, false);
|
||
assert.equal(pet.isExecutablePlan(missingWarning), false);
|
||
});
|
||
|
||
test("dynamic module create requires bounded opaque master and detail previews", () => {
|
||
const plan = structuredClone(wireContract.scenarios.find(
|
||
scenario => scenario.name === "dynamic_module_resolve_to_create").plan);
|
||
const preview = pet.dynamicModuleConfirmationPreview(plan);
|
||
assert.equal(preview.required, true);
|
||
assert.equal(preview.complete, true);
|
||
assert.equal(preview.mode, "create");
|
||
assert.equal(preview.totalValues, 2);
|
||
assert.equal(preview.masterValues[0].parameterId, "m0123456789abcdef");
|
||
assert.equal(preview.detailRows[0].values[0].value, "第一行");
|
||
assert.equal(pet.isExecutablePlan(plan), true);
|
||
|
||
const emptyValue = structuredClone(plan);
|
||
emptyValue.preview.masterValues[0].value = "";
|
||
bindDynamicCreateProjections(emptyValue);
|
||
assert.equal(pet.dynamicModuleConfirmationPreview(emptyValue).complete, true);
|
||
|
||
const projectionMismatch = structuredClone(plan);
|
||
projectionMismatch.data.parameterPreview.masterValues[0].value = "被篡改";
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(projectionMismatch).complete,
|
||
false);
|
||
assert.equal(pet.isExecutablePlan(projectionMismatch), false);
|
||
|
||
const physicalFieldLeak = structuredClone(plan);
|
||
physicalFieldLeak.preview.masterValues[0].physicalField = "CUSTOMER_NAME";
|
||
bindDynamicCreateProjections(physicalFieldLeak);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(physicalFieldLeak).complete,
|
||
false);
|
||
|
||
const duplicateParameter = structuredClone(plan);
|
||
duplicateParameter.preview.masterValues.push(
|
||
structuredClone(duplicateParameter.preview.masterValues[0]));
|
||
bindDynamicCreateProjections(duplicateParameter);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(duplicateParameter).complete,
|
||
false);
|
||
|
||
const wrongRowNumber = structuredClone(plan);
|
||
wrongRowNumber.preview.detailRows[0].rowNumber = 2;
|
||
bindDynamicCreateProjections(wrongRowNumber);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(wrongRowNumber).complete,
|
||
false);
|
||
|
||
const controlCharacter = structuredClone(plan);
|
||
controlCharacter.preview.detailRows[0].values[0].value = "第一行\u0085";
|
||
bindDynamicCreateProjections(controlCharacter);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(controlCharacter).complete,
|
||
false);
|
||
|
||
const aggregateTooLarge = structuredClone(plan);
|
||
aggregateTooLarge.preview.masterValues = Array.from({ length: 5 }, (_, index) => ({
|
||
parameterId: "m" + index.toString(16).padStart(16, "0"),
|
||
label: "大字段" + index,
|
||
valueType: "string",
|
||
value: "值".repeat(30000)
|
||
}));
|
||
bindDynamicCreateProjections(aggregateTooLarge);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(aggregateTooLarge).complete,
|
||
false);
|
||
|
||
const exposedBlocker = structuredClone(plan);
|
||
exposedBlocker.data.writeExecutionBlocker = "adapter_missing";
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(exposedBlocker).complete,
|
||
false);
|
||
|
||
const changedVersion = structuredClone(plan);
|
||
changedVersion.commandVersion = "2.0";
|
||
assert.equal(pet.dynamicModuleConfirmationPreview(changedVersion).complete, false);
|
||
assert.equal(pet.isExecutablePlan(changedVersion), false);
|
||
});
|
||
|
||
test("dynamic module update requires a bounded concurrent before-after preview", () => {
|
||
const plan = structuredClone(wireContract.scenarios.find(
|
||
scenario => scenario.name === "dynamic_module_resolve_to_update").plan);
|
||
const preview = pet.dynamicModuleConfirmationPreview(plan);
|
||
assert.equal(preview.required, true);
|
||
assert.equal(preview.complete, true);
|
||
assert.equal(preview.mode, "update");
|
||
assert.equal(preview.recordDisplay, "客户 C-001 / 朗速客户");
|
||
assert.equal(preview.changes[0].previousValue, "100.00");
|
||
assert.equal(preview.changes[0].newValue, "120.50");
|
||
assert.equal(pet.isExecutablePlan(plan), true);
|
||
|
||
const missingPrevious = structuredClone(plan);
|
||
delete missingPrevious.preview.changes[0].previousValue;
|
||
bindDynamicUpdateProjection(missingPrevious);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(missingPrevious).complete,
|
||
false);
|
||
|
||
const noActualChange = structuredClone(plan);
|
||
noActualChange.preview.changes[0].newValue = "100.00";
|
||
bindDynamicUpdateProjection(noActualChange);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(noActualChange).complete,
|
||
false);
|
||
assert.equal(pet.isExecutablePlan(noActualChange), false);
|
||
|
||
const physicalFieldLeak = structuredClone(plan);
|
||
physicalFieldLeak.preview.changes[0].columnName = "CREDIT_LIMIT";
|
||
bindDynamicUpdateProjection(physicalFieldLeak);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(physicalFieldLeak).complete,
|
||
false);
|
||
|
||
const duplicateParameter = structuredClone(plan);
|
||
const duplicate = structuredClone(duplicateParameter.preview.changes[0]);
|
||
duplicate.newValue = "130.00";
|
||
duplicateParameter.preview.changes.push(duplicate);
|
||
bindDynamicUpdateProjection(duplicateParameter);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(duplicateParameter).complete,
|
||
false);
|
||
|
||
const unsafeValue = structuredClone(plan);
|
||
unsafeValue.preview.changes[0].newValue = "120.50\u0000";
|
||
bindDynamicUpdateProjection(unsafeValue);
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(unsafeValue).complete,
|
||
false);
|
||
|
||
const projectionMismatch = structuredClone(plan);
|
||
projectionMismatch.data.preview.changes[0].newValue = "999.99";
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(projectionMismatch).complete,
|
||
false);
|
||
|
||
const forgedAdapter = structuredClone(plan);
|
||
forgedAdapter.data.adapter.evidenceSha256 = "not-a-hash";
|
||
assert.equal(
|
||
pet.dynamicModuleConfirmationPreview(forgedAdapter).complete,
|
||
false);
|
||
assert.equal(pet.isExecutablePlan(forgedAdapter), false);
|
||
});
|
||
|
||
test("atlas asset must decode at the exact production dimensions", async () => {
|
||
const loaded = await pet.loadAtlasAsset("https://asset.test/guga.webp", {
|
||
createImage: () => fakeImage(1536, 1872)
|
||
});
|
||
assert.deepEqual(loaded, {
|
||
url: "https://asset.test/guga.webp",
|
||
width: 1536,
|
||
height: 1872
|
||
});
|
||
});
|
||
|
||
test("atlas asset fails closed on crop corruption and timeout", async () => {
|
||
await assert.rejects(
|
||
pet.loadAtlasAsset("https://asset.test/cropped.webp", {
|
||
createImage: () => fakeImage(1536, 1792)
|
||
}),
|
||
error => error && error.code === "pet_sprite_dimensions_invalid"
|
||
);
|
||
await assert.rejects(
|
||
pet.loadAtlasAsset("https://asset.test/corrupt.webp", {
|
||
createImage: () => fakeImage(1536, 1872, true)
|
||
}),
|
||
error => error && error.code === "pet_sprite_decode_failed"
|
||
);
|
||
await assert.rejects(
|
||
pet.loadAtlasAsset("https://asset.test/hung.webp", {
|
||
createImage: () => ({ naturalWidth: 0, naturalHeight: 0 }),
|
||
setTimer(callback) { callback(); return 1; },
|
||
clearTimer() {}
|
||
}),
|
||
error => error && error.code === "pet_sprite_load_timeout"
|
||
);
|
||
});
|
||
|
||
test("invalid server plans can never enable execution", () => {
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p1", valid: true, executionAllowed: true, risk: "write"
|
||
}), true);
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p2", valid: true, executionAllowed: true, risk: "navigate"
|
||
}), true);
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p3", valid: true, executionAllowed: true, risk: "critical"
|
||
}), true);
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p4", valid: true, executionAllowed: false, risk: "draft"
|
||
}), false);
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p5", valid: true, executionAllowed: false, risk: "read"
|
||
}), false);
|
||
assert.equal(pet.isExecutablePlan({
|
||
planId: "p6", valid: false, executionAllowed: true, risk: "write"
|
||
}), false);
|
||
assert.equal(pet.isExecutablePlan({ valid: true, executionAllowed: true, risk: "write" }), false);
|
||
assert.equal(pet.isExecutablePlan(null), false);
|
||
});
|
||
|
||
function validResponse(request, data) {
|
||
return {
|
||
protocolVersion: "1.0",
|
||
requestId: request.requestId,
|
||
correlationId: request.correlationId,
|
||
success: true,
|
||
code: "ok",
|
||
message: null,
|
||
data: data || {}
|
||
};
|
||
}
|
||
|
||
function executionData(overrides) {
|
||
return {
|
||
result: {
|
||
success: true,
|
||
code: "purchase_document_created",
|
||
message: "采购业务单据已创建。",
|
||
recordId: null,
|
||
replayed: false,
|
||
data: {}
|
||
},
|
||
followupPlan: null,
|
||
followupCode: null,
|
||
...(overrides || {})
|
||
};
|
||
}
|
||
|
||
function diagnosticExecutionData(correlationId) {
|
||
return executionData({
|
||
result: {
|
||
success: true,
|
||
code: "initialization_failure_captured",
|
||
message: "已捕获模块初始化失败的诊断证据。",
|
||
recordId: "hr_4011",
|
||
replayed: false,
|
||
data: {
|
||
diagnosticContextSchemaVersion: "1.0",
|
||
diagnosticId: "diag-0123456789abcdef0123456789abcdef",
|
||
correlationId,
|
||
evidencePersisted: true,
|
||
evidenceContentHash: "b".repeat(64),
|
||
outcome: "failed",
|
||
primaryFindingCode: "missing_column",
|
||
moduleOpenSucceeded: false,
|
||
eventCount: 1,
|
||
failedEventCount: 1,
|
||
slowEventCount: 0,
|
||
traceTruncated: false,
|
||
summaryTruncated: false,
|
||
findings: [{
|
||
severity: "error",
|
||
code: "missing_column",
|
||
category: "low_code_configuration",
|
||
stage: "initialization_sql",
|
||
confidence: "observed",
|
||
message: "初始化引用的数据库字段不存在。",
|
||
recommendation: "检查低代码字段映射、客户扩展字段和账套升级版本。",
|
||
occurrenceCount: 1,
|
||
sqlFingerprint: "c".repeat(64),
|
||
caller: "caller_0001"
|
||
}],
|
||
staticDiagnosis: {
|
||
moduleCode: "hr_4011",
|
||
moduleKind: "base",
|
||
healthy: false,
|
||
issueCount: 1,
|
||
issues: [{
|
||
severity: "error",
|
||
code: "base.fields_missing",
|
||
source: "base_field_config"
|
||
}]
|
||
},
|
||
contextAvailable: true
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function fakeImage(width, height, decodeFails) {
|
||
const image = {
|
||
naturalWidth: width,
|
||
naturalHeight: height,
|
||
async decode() {
|
||
if (decodeFails) throw new Error("decode failed");
|
||
}
|
||
};
|
||
Object.defineProperty(image, "src", {
|
||
set() { queueMicrotask(() => image.onload()); }
|
||
});
|
||
return image;
|
||
}
|
||
|
||
function bindDynamicCreateProjections(plan) {
|
||
plan.data.preview = structuredClone(plan.preview);
|
||
plan.data.parameterPreview = structuredClone(plan.preview);
|
||
return plan;
|
||
}
|
||
|
||
function bindDynamicUpdateProjection(plan) {
|
||
plan.data.preview = structuredClone(plan.preview);
|
||
return plan;
|
||
}
|