feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,791 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user