521 lines
24 KiB
JavaScript
521 lines
24 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import path from 'node:path';
|
||
import test from 'node:test';
|
||
import Ajv2020 from 'ajv/dist/2020.js';
|
||
|
||
import { buildParserMessage } from '../LianSyn-platform/external-agent-client.mjs';
|
||
|
||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const read = (relativePath) => readFileSync(path.join(repoRoot, relativePath), 'utf8').replace(/\r\n/gu, '\n');
|
||
|
||
const catalog = read('agent设计规范/templates/business-input-templates.md');
|
||
const registry = read('agent设计规范/business-behavior-registry.md');
|
||
const mainPrompt = read('agent设计规范/agent-prompt.md');
|
||
const skillRuntimeFiles = [
|
||
'agent设计规范/skills/lwlt-newbooking/SKILL.md',
|
||
'agent设计规范/skills/lwlt-newbooking/agents/openai.yaml',
|
||
'agent设计规范/skills/lwlt-newbooking/references/actions-and-fields.md',
|
||
'agent设计规范/skills/lwlt-newbooking/references/input-contract.md',
|
||
'agent设计规范/skills/lwlt-newbooking/references/normalization-rules.md',
|
||
'agent设计规范/skills/lwlt-newbooking/references/output-contract.md',
|
||
'agent设计规范/skills/lwlt-updating/SKILL.md',
|
||
'agent设计规范/skills/lwlt-updating/agents/openai.yaml',
|
||
'agent设计规范/skills/lwlt-updating/references/input-contract.md',
|
||
'agent设计规范/skills/lwlt-updating/references/operation-contract.md',
|
||
'agent设计规范/skills/lwlt-arrangement/SKILL.md',
|
||
'agent设计规范/skills/lwlt-arrangement/agents/openai.yaml',
|
||
'agent设计规范/skills/lwlt-arrangement/references/input-contract.md',
|
||
'agent设计规范/skills/lwlt-arrangement/references/operation-contract.md',
|
||
'agent设计规范/skills/lwlt-confirmation/SKILL.md',
|
||
'agent设计规范/skills/lwlt-confirmation/agents/openai.yaml',
|
||
'agent设计规范/skills/lwlt-confirmation/references/input-contract.md',
|
||
'agent设计规范/skills/lwlt-confirmation/references/output-contract.md',
|
||
'agent设计规范/skills/lwlt-lifecycle/SKILL.md',
|
||
'agent设计规范/skills/lwlt-lifecycle/agents/openai.yaml',
|
||
'agent设计规范/skills/lwlt-lifecycle/references/input-contract.md',
|
||
'agent设计规范/skills/lwlt-lifecycle/references/operation-contract.md'
|
||
];
|
||
const skillRuntimeText = skillRuntimeFiles.map(read).join('\n');
|
||
|
||
const executableDirectives = [
|
||
'独立团单个下单',
|
||
'独立团批量下单',
|
||
'散拼团新增计划',
|
||
'散拼团单个新增子单',
|
||
'导入独立团名单',
|
||
'导入散拼子单名单',
|
||
'安排导游',
|
||
'安排用车',
|
||
'安排酒店',
|
||
'安排大交通',
|
||
'安排其他/备案',
|
||
'修改散拼母团计划收客数',
|
||
'追加散拼子单订房说明',
|
||
'取消订单',
|
||
'恢复订单',
|
||
'导出团队文件'
|
||
];
|
||
|
||
const narrowModificationDirectives = [
|
||
'独立团信息修改',
|
||
'安排变更'
|
||
];
|
||
|
||
const deferredDirectives = [
|
||
'真实应收团款处理',
|
||
'团队文件交付自动化',
|
||
'团队文件上传'
|
||
];
|
||
|
||
const agentDirectives = [
|
||
...executableDirectives.slice(0, 13),
|
||
...narrowModificationDirectives,
|
||
...executableDirectives.slice(13)
|
||
];
|
||
|
||
const allDirectives = [
|
||
...agentDirectives.slice(0, 4),
|
||
'散拼团多个新增子单',
|
||
...agentDirectives.slice(4)
|
||
];
|
||
|
||
const skillContracts = {
|
||
'agent设计规范/skills/lwlt-newbooking/references/input-contract.md': [
|
||
'独立团单个下单',
|
||
'独立团批量下单',
|
||
'散拼团新增计划',
|
||
'散拼团单个新增子单'
|
||
],
|
||
'agent设计规范/skills/lwlt-arrangement/references/input-contract.md': [
|
||
'安排导游',
|
||
'安排用车',
|
||
'安排酒店',
|
||
'安排大交通',
|
||
'安排其他/备案',
|
||
'安排变更'
|
||
],
|
||
'agent设计规范/skills/lwlt-updating/references/input-contract.md': [
|
||
'修改散拼母团计划收客数',
|
||
'追加散拼子单订房说明',
|
||
'独立团信息修改'
|
||
],
|
||
'agent设计规范/skills/lwlt-confirmation/references/input-contract.md': ['导出团队文件'],
|
||
'agent设计规范/skills/lwlt-lifecycle/references/input-contract.md': [
|
||
'导入独立团名单',
|
||
'导入散拼子单名单',
|
||
'取消订单',
|
||
'恢复订单'
|
||
]
|
||
};
|
||
|
||
const businessPages = {
|
||
'agent设计规范/businesses/team_order_create.md': '独立团单个下单',
|
||
'agent设计规范/businesses/team_order_batch_create.md': '独立团批量下单',
|
||
'agent设计规范/businesses/shared_plan_create.md': '散拼团新增计划',
|
||
'agent设计规范/businesses/shared_child_order_create.md': '散拼团单个新增子单'
|
||
};
|
||
|
||
const minimumLabels = {
|
||
独立团单个下单: ['发团日期', '预订客户', '产品搜索', '预估人数', '用房数量'],
|
||
独立团批量下单: ['发团日期', '预订客户', '产品搜索', '预估人数', '用房数量', '发团周期'],
|
||
散拼团新增计划: ['发团日期', '产品搜索', '计划收客数', '用房数量'],
|
||
散拼团单个新增子单: ['预订客户', '出发日期', '人数'],
|
||
散拼团多个新增子单: ['预订客户', '出发日期', '产品名称', '人数'],
|
||
导入独立团名单: ['预订客户', '出发日期'],
|
||
导入散拼子单名单: ['预订客户', '出发日期'],
|
||
安排导游: ['团号', '导游搜索'],
|
||
安排用车: ['团号', '结算单位搜索', '用车日期', '数量'],
|
||
安排酒店: ['团号', '入住日期', '离店日期', '酒店搜索', '间数'],
|
||
安排大交通: ['团号', '结算单位搜索', '数量', '交通日期'],
|
||
'安排其他/备案': ['团号', '结算单位搜索', '业务日期'],
|
||
修改散拼母团计划收客数: ['出发日期', '目标计划收客数'],
|
||
追加散拼子单订房说明: ['预订客户', '出发日期', '追加内容'],
|
||
独立团信息修改: ['预订客户', '出发日期', '目标修改'],
|
||
安排变更: ['团号', '目标变更'],
|
||
取消订单: ['预订客户', '出发日期'],
|
||
恢复订单: ['预订客户', '出发日期'],
|
||
导出团队文件: ['预订客户', '出发日期', '文件类型']
|
||
};
|
||
|
||
const textBlocks = (document) =>
|
||
[...document.matchAll(/```text\r?\n([\s\S]*?)```/g)].map((match) => match[1].trim());
|
||
|
||
const headings = [...catalog.matchAll(/^### (\d+)\. (.+)$/gm)];
|
||
const operationSections = headings.map((match, index) => {
|
||
const start = match.index;
|
||
const end = headings[index + 1]?.index ?? catalog.length;
|
||
return {
|
||
number: Number(match[1]),
|
||
directive: match[2].trim(),
|
||
body: catalog.slice(start, end)
|
||
};
|
||
});
|
||
|
||
const sectionByDirective = new Map(operationSections.map((section) => [section.directive, section]));
|
||
|
||
const labeledBlock = (section, label) => {
|
||
const pattern = new RegExp(
|
||
`^#### ${label}\\r?\\n\\r?\\n\`\`\`text\\r?\\n([\\s\\S]*?)\`\`\``,
|
||
'm'
|
||
);
|
||
return section.body.match(pattern)?.[1].trim() ?? null;
|
||
};
|
||
|
||
const fieldLines = (block) =>
|
||
block
|
||
.split(/\r?\n/)
|
||
.slice(1)
|
||
.filter((line) => line.includes(':'));
|
||
|
||
const assertTwoBlockContract = (document, directives, source) => {
|
||
const blocks = textBlocks(document);
|
||
|
||
for (const directive of directives) {
|
||
const matches = blocks.filter((block) => block.startsWith(`${directive}\n`));
|
||
assert.equal(matches.length, 2, `${source}: ${directive} must have exactly template + example`);
|
||
|
||
const [template, example] = matches;
|
||
for (const line of fieldLines(template)) {
|
||
assert.match(line, /(必填|选填):/, `${source}: unmarked template field: ${line}`);
|
||
}
|
||
assert.doesNotMatch(example, /(必填|选填)/, `${source}: example contains field rules`);
|
||
assert.doesNotMatch(example, /<[^>]+>/, `${source}: example contains a placeholder`);
|
||
}
|
||
};
|
||
|
||
test('catalog has exactly 19 ordered operations with adjacent template and example blocks', () => {
|
||
assert.deepEqual(
|
||
operationSections.map((section) => section.number),
|
||
Array.from({ length: 19 }, (_, index) => index + 1)
|
||
);
|
||
assert.deepEqual(operationSections.map((section) => section.directive), allDirectives);
|
||
|
||
for (const section of operationSections) {
|
||
const template = labeledBlock(section, '模板');
|
||
const example = labeledBlock(section, '示例');
|
||
assert.ok(template, `${section.directive}: missing template block`);
|
||
assert.ok(example, `${section.directive}: missing example block`);
|
||
assert.equal(textBlocks(section.body).length, 2, `${section.directive}: must contain only two text blocks`);
|
||
assert.equal(template.split(/\r?\n/, 1)[0], section.directive);
|
||
assert.equal(example.split(/\r?\n/, 1)[0], section.directive);
|
||
assert.ok(section.body.indexOf('#### 模板') < section.body.indexOf('#### 示例'));
|
||
}
|
||
|
||
assert.doesNotMatch(catalog, /最小必填速查|## 二、能力状态/);
|
||
});
|
||
|
||
test('main prompt remains a compact Skill router', () => {
|
||
assert.match(mainPrompt, /prompt_version: ltjt-agent-prompt-v1\.8-independent-headcount-categories/);
|
||
for (const action of [
|
||
'team_order_create',
|
||
'team_order_batch_create',
|
||
'shared_plan_create',
|
||
'shared_child_order_create',
|
||
'passenger_list_import',
|
||
'arrangement_guide',
|
||
'arrangement_vehicle',
|
||
'arrangement_hotel',
|
||
'arrangement_transport',
|
||
'arrangement_other',
|
||
'order_update_shared_plan',
|
||
'order_update_shared_child',
|
||
'order_update_independent',
|
||
'order_cancel',
|
||
'order_restore',
|
||
'confirmation_export'
|
||
]) assert.equal(mainPrompt.includes(action), false, `main prompt duplicates Skill action: ${action}`);
|
||
assert.match(mainPrompt, /`agent_parse_needs_input`/);
|
||
assert.match(mainPrompt, /`agent_parse_blocked`/);
|
||
assert.match(mainPrompt, /你是老挝联泰系统操作员。根据用户实际表达识别业务类型,并调用适合的业务能力处理,最后原样返回标准 JSON 结果。/);
|
||
assert.match(mainPrompt, /不得脱离业务能力自行编造 `operation` 字段或用户事实/);
|
||
assert.doesNotMatch(mainPrompt, /## 18 类标准路由|## 输出状态|## 提取边界|```json/);
|
||
assert.ok(mainPrompt.length < 1_200, `main prompt is too large: ${mainPrompt.length}`);
|
||
assert.doesNotMatch(mainPrompt, /插件|ERP|门禁|TEST-202609|test_context|server_response|native_request|requery|fresh|write_attempted|DoInfo|allowlist|\b(?:tid|ddid|row_id)\b/);
|
||
|
||
const runtimeMessage = buildParserMessage({
|
||
rawText: '独立团信息修改\n单号:LW-260903-A\n目标修改:标间数改为9间',
|
||
taskId: 'TASK-PARSE-ONLY-PROMPT',
|
||
receivedAt: '2026-08-12T08:00:00.000Z'
|
||
});
|
||
assert.match(runtimeMessage, /只解析用户输入,不查询对象、不执行任务、不判断结果/);
|
||
assert.match(runtimeMessage, /新建业务、订单\/安排修改以及取消\/恢复订单中,用户只提供月日且原文没有年份时/);
|
||
assert.match(runtimeMessage, /取消\/恢复的定位出发日期均执行上述规则/);
|
||
assert.doesNotMatch(runtimeMessage, /标准指令路由/);
|
||
assert.doesNotMatch(runtimeMessage, /插件|ERP|门禁|TEST-202609|test_context|server_response|native_request|requery|fresh|write_attempted|DoInfo|allowlist|\b(?:tid|ddid|row_id)\b/);
|
||
});
|
||
|
||
test('all packaged Skill sources contain only parsing knowledge', () => {
|
||
// Business-field names such as the ERP leader lookup column are parsing
|
||
// knowledge. Skills must still stay clear of execution plumbing and
|
||
// internal/runtime references.
|
||
assert.doesNotMatch(skillRuntimeText, /插件|浏览器|门禁|TEST-202609|test_context|server_response|native_request|requery|fresh|write_attempted|DoInfo|allowlist|\b(?:tid|ddid|row_id)\b|E2E\d*/);
|
||
for (const action of [
|
||
'team_order_create',
|
||
'team_order_batch_create',
|
||
'shared_plan_create',
|
||
'shared_child_order_create',
|
||
'passenger_list_import',
|
||
'arrangement_guide',
|
||
'arrangement_vehicle',
|
||
'arrangement_hotel',
|
||
'arrangement_transport',
|
||
'arrangement_other',
|
||
'order_update_shared_plan',
|
||
'order_update_shared_child',
|
||
'order_update_independent',
|
||
'order_cancel',
|
||
'order_restore',
|
||
'confirmation_export'
|
||
]) assert.ok(skillRuntimeText.includes(action), `Skill sources missing parser action: ${action}`);
|
||
});
|
||
|
||
test('Agent parse Schema accepts business facts and rejects execution metadata', () => {
|
||
const operationSchema = JSON.parse(read('schemas/agent_operation.schema.json'));
|
||
const resultSchema = JSON.parse(read('schemas/agent_parse_result.schema.json'));
|
||
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
||
ajv.addFormat('date', /^\d{4}-\d{2}-\d{2}$/);
|
||
ajv.addSchema(operationSchema);
|
||
const validate = ajv.compile(resultSchema);
|
||
const parseOnly = {
|
||
status: 'agent_parse_passed',
|
||
blockers: [],
|
||
operation: {
|
||
action: 'order_cancel',
|
||
order_nature: 'formal',
|
||
submit_mode: 'dry_run',
|
||
data: { existing_refs: { identifier: 'LW-260903-A' } }
|
||
}
|
||
};
|
||
|
||
assert.equal(validate(parseOnly), true, JSON.stringify(validate.errors));
|
||
const multiTargetUpdate = {
|
||
status: 'agent_parse_passed',
|
||
blockers: [],
|
||
operation: {
|
||
action: 'order_update_independent',
|
||
order_nature: 'formal',
|
||
submit_mode: 'dry_run',
|
||
data: {
|
||
existing_refs: { kind: 'independent_order' },
|
||
customer: { name: '衡阳国旅广东', keyword: '衡阳国旅广东' },
|
||
departure_dates: ['2026-09-03'],
|
||
product: { name: '老挝好时光', keyword: '老挝好时光' },
|
||
updates: {
|
||
actions: [
|
||
{ target: 'pax.adult', operation: 'set', value: 8 },
|
||
{ target: 'pax.child_bed', operation: 'set', value: 1 },
|
||
{ target: 'pax.leader', operation: 'set', value: 1 }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
};
|
||
assert.equal(validate(multiTargetUpdate), true, JSON.stringify(validate.errors));
|
||
const businessLookup = structuredClone(parseOnly);
|
||
businessLookup.operation.data = {
|
||
customer: { name: '辽宁康辉', keyword: '辽宁康辉' },
|
||
departure_dates: ['2026-09-03'],
|
||
product: { name: '遇见老挝', keyword: '遇见老挝' },
|
||
leader: { name: '张三', keyword: '张三' }
|
||
};
|
||
assert.equal(validate(businessLookup), true, JSON.stringify(validate.errors));
|
||
const executionMetadata = structuredClone(parseOnly);
|
||
executionMetadata.operation.source = { test_context: { run_id: 'not-agent-data' } };
|
||
assert.equal(validate(executionMetadata), false);
|
||
assert.match(JSON.stringify(validate.errors), /additionalProperties/);
|
||
});
|
||
|
||
test('template fields are marked and examples contain only concrete values', () => {
|
||
for (const section of operationSections) {
|
||
const template = labeledBlock(section, '模板');
|
||
const example = labeledBlock(section, '示例');
|
||
const fields = fieldLines(template);
|
||
|
||
assert.ok(fields.length > 0, `${section.directive}: expected at least one field`);
|
||
assert.ok(fields.some((line) => line.includes('(必填):')), `${section.directive}: no required field`);
|
||
|
||
for (const line of fields) {
|
||
assert.match(line, /(必填|选填):/, `${section.directive}: unmarked template field: ${line}`);
|
||
}
|
||
|
||
assert.doesNotMatch(example, /(必填|选填)/, `${section.directive}: example contains field rules`);
|
||
assert.doesNotMatch(example, /<[^>]+>/, `${section.directive}: example contains a placeholder`);
|
||
}
|
||
});
|
||
|
||
test('user templates never request ERP internal identifiers and use canonical customer label', () => {
|
||
const forbiddenLabels = /^(?:母团\s*)?(?:tid|ddid|row_id|行\s*ID|账号|运行编号|测试标记|allowlist)((?:必填|选填)):/im;
|
||
|
||
for (const section of operationSections) {
|
||
const template = labeledBlock(section, '模板');
|
||
assert.doesNotMatch(template, forbiddenLabels);
|
||
assert.doesNotMatch(template, /^预定客户((?:必填|选填)):/m, 'template must use 预订客户');
|
||
}
|
||
});
|
||
|
||
test('all Agent directives are routed to their Skill contract in the same two-block format', () => {
|
||
for (const directive of agentDirectives) {
|
||
assert.ok(registry.includes(`\`${directive}\``), `registry missing ${directive}`);
|
||
}
|
||
|
||
for (const [relativePath, directives] of Object.entries(skillContracts)) {
|
||
assertTwoBlockContract(read(relativePath), directives, relativePath);
|
||
}
|
||
});
|
||
|
||
test('shared-child batch stays Program-only while remaining copyable from the operator catalog', () => {
|
||
const directive = '散拼团多个新增子单';
|
||
const section = sectionByDirective.get(directive);
|
||
assert.ok(section, `${directive}: missing operator template`);
|
||
assert.match(catalog, /`散拼团多个新增子单`固定走程序解析,不走 AI/);
|
||
assert.doesNotMatch(registry, /^\|[^\n]*`散拼团多个新增子单`[^\n]*\|$/m);
|
||
assert.equal(skillRuntimeText.includes('shared_child_order_batch_create'), false);
|
||
assert.equal(mainPrompt.includes('shared_child_order_batch_create'), false);
|
||
});
|
||
|
||
test('the four detailed business pages use adjacent template and example blocks', () => {
|
||
for (const [relativePath, directive] of Object.entries(businessPages)) {
|
||
const document = read(relativePath);
|
||
const blocks = textBlocks(document).filter((block) => block.startsWith(`${directive}\n`));
|
||
assert.equal(blocks.length, 2, `${relativePath}: expected one template and one example`);
|
||
assert.match(document, /### 1\.1 模板\r?\n/);
|
||
assert.match(document, /### 1\.2 示例\r?\n/);
|
||
for (const line of fieldLines(blocks[0])) {
|
||
assert.match(line, /(必填|选填):/, `${relativePath}: unmarked template field: ${line}`);
|
||
}
|
||
assert.doesNotMatch(blocks[1], /(必填|选填)|<[^>]+>/);
|
||
}
|
||
});
|
||
|
||
test('the two desktop-approved examples remain exactly copyable', () => {
|
||
const independentSingle = [
|
||
'独立团单个下单',
|
||
'发团日期:11-01',
|
||
'预订客户:老挝联泰人民币',
|
||
'产品搜索:遇见老挝',
|
||
'预估人数:15+1',
|
||
'用房数量:8标1单'
|
||
].join('\n');
|
||
const sharedPlan = [
|
||
'散拼团新增计划',
|
||
'发团日期:11-01到11-30',
|
||
'产品搜索:广东衡阳6D',
|
||
'计划收客数:40',
|
||
'用房数量:8标1单',
|
||
'发团周期:11.01/11.15/11.26'
|
||
].join('\n');
|
||
|
||
assert.equal(labeledBlock(sectionByDirective.get('独立团单个下单'), '示例'), independentSingle);
|
||
assert.equal(labeledBlock(sectionByDirective.get('散拼团新增计划'), '示例'), sharedPlan);
|
||
});
|
||
|
||
test('every operation preserves its explicit minimum-required field set', () => {
|
||
for (const [directive, expectedLabels] of Object.entries(minimumLabels)) {
|
||
const template = labeledBlock(sectionByDirective.get(directive), '模板');
|
||
const actualLabels = fieldLines(template)
|
||
.filter((line) => line.includes('(必填):'))
|
||
.map((line) => line.slice(0, line.indexOf('(必填):')));
|
||
assert.deepEqual(actualLabels, expectedLabels, `${directive} minimum-required fields changed`);
|
||
}
|
||
for (const directive of ['导入独立团名单', '导入散拼子单名单']) {
|
||
const section = sectionByDirective.get(directive);
|
||
assert.match(section.body, /名单附件(必填):[\s\S]*`\.xls` 或 `\.xlsx`/);
|
||
assert.doesNotMatch(labeledBlock(section, '模板'), /名单内容/);
|
||
}
|
||
});
|
||
|
||
test('modification input asks only for targets and export stays minimal', () => {
|
||
const controlPlaneReadme = read('control-plane/README.md');
|
||
const releaseGate = read('agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md');
|
||
|
||
assert.doesNotMatch(catalog, /清理测试订单/);
|
||
assert.doesNotMatch(registry, /\| `清理测试订单` \|/);
|
||
|
||
const forbiddenPreviousValueField = /^(?:修改前[^(]*|原值|当前值|当前安排|当前收结状态)((?:必填|选填)):/m;
|
||
for (const directive of [
|
||
'修改散拼母团计划收客数',
|
||
'追加散拼子单订房说明',
|
||
'独立团信息修改',
|
||
'安排变更'
|
||
]) {
|
||
assert.doesNotMatch(labeledBlock(sectionByDirective.get(directive), '模板'), forbiddenPreviousValueField);
|
||
}
|
||
|
||
assert.deepEqual(minimumLabels['导出团队文件'], ['预订客户', '出发日期', '文件类型']);
|
||
assert.doesNotMatch(catalog, /^### \d+\. 读取团队文件源响应$/m);
|
||
assert.doesNotMatch(catalog, /^读取团队文件源响应$/m);
|
||
assert.match(catalog, /游客名单返回 `\.xlsx`;其他文件通常返回 PDF/);
|
||
assert.doesNotMatch(catalog, /before_snapshot|AgentBus|OSS|object key|updates\.actions/);
|
||
assert.match(controlPlaneReadme, /游客信息保留原始 ERP `\.xls` 作为源归档,同时由平台通过 LibreOffice 生成真实 `\.xlsx`/);
|
||
assert.match(releaseGate, /系统自行读取当前值并形成 before\/after/);
|
||
});
|
||
|
||
test('team-file export uses the business name while keeping the internal action stable', () => {
|
||
const confirmationContract = read('agent设计规范/skills/lwlt-confirmation/references/input-contract.md');
|
||
const platformUi = read('LianSyn-platform/app.js');
|
||
const pluginPlanner = read('chrome-extension/ltjt-order-assistant/operation-plans.js');
|
||
|
||
assert.ok(sectionByDirective.has('导出团队文件'));
|
||
assert.match(confirmationContract, /# 导出团队文件输入契约/);
|
||
assert.match(read('agent设计规范/skills/lwlt-confirmation/SKILL.md'), /confirmation_export operation JSON/);
|
||
assert.ok(registry.includes('| `导出团队文件` | `lwlt-confirmation` | `confirmation_export` |'));
|
||
assert.match(platformUi, /confirmation_export: '导出团队文件'/);
|
||
assert.match(pluginPlanner, /confirmation_export:\s*{[\s\S]*?label: '导出团队文件'/);
|
||
assert.match(catalog, /文件类型(必填):[\s\S]*整团游客信息/);
|
||
assert.match(catalog, /整团游客信息.*散拼母团/);
|
||
assert.match(catalog, /游客名单.*独立团.*散拼具体子单/);
|
||
assert.match(confirmationContract, /`整团游客信息`.*散拼母团/);
|
||
assert.match(confirmationContract, /`游客名单`.*独立团.*散拼具体子单/);
|
||
assert.match(pluginPlanner, /validateSharedPlanVisitorExport/);
|
||
assert.doesNotMatch(platformUi, /确认件源读取\/恢复/);
|
||
assert.doesNotMatch(pluginPlanner, /label: '确认件源读取\/恢复'/);
|
||
});
|
||
|
||
test('independent-order updates preserve broad parser targets while arrangement changes stay narrow', () => {
|
||
const parser = read('LianSyn-platform/external-agent-client.mjs');
|
||
const updatingSkill = read('agent设计规范/skills/lwlt-updating/SKILL.md');
|
||
const arrangementSkill = read('agent设计规范/skills/lwlt-arrangement/SKILL.md');
|
||
const modificationSection = catalog.slice(catalog.indexOf('## 五、修改'), catalog.indexOf('## 六、取消与恢复'));
|
||
|
||
for (const directive of narrowModificationDirectives) {
|
||
assert.ok(modificationSection.includes(`### ${sectionByDirective.get(directive).number}. ${directive}`));
|
||
assert.ok(registry.includes(`\`${directive}\``));
|
||
}
|
||
|
||
assert.match(updatingSkill, /只提取目标值,不询问、不生成当前值、原值或 before\/after/);
|
||
assert.match(updatingSkill, /保留业务可见目标/);
|
||
assert.match(arrangementSkill, /安排变更只接受酒店离店日期和\/或目标房间总数/);
|
||
assert.match(parser, /修改类只提取目标值/);
|
||
assert.match(parser, /updates\.actions 包含多个动作/);
|
||
assert.match(parser, /new Set\(\['end_date', 'room_count'\]\)/);
|
||
assert.match(labeledBlock(sectionByDirective.get('独立团信息修改'), '示例'), /目标修改:人数改为8\+1(占)\+1/);
|
||
assert.match(updatingSkill, /兼容旧三段 `人数改为8\+1(占)\+1`/);
|
||
assert.match(updatingSkill, /pax\.child_no_bed=1/);
|
||
assert.match(sectionByDirective.get('独立团信息修改')?.body ?? '', /未出现的类别保持原值/);
|
||
assert.match(labeledBlock(sectionByDirective.get('安排变更'), '示例'), /离店日期改为11-05;房间数改为15间/);
|
||
assert.doesNotMatch(catalog, /需求登记:/);
|
||
});
|
||
|
||
test('deferred items 20 to 22 are absent from current user and Skill input contracts', () => {
|
||
const currentInputSurface = [
|
||
catalog,
|
||
...Object.keys(skillContracts).map((relativePath) => read(relativePath))
|
||
].join('\n');
|
||
|
||
for (const directive of deferredDirectives) {
|
||
assert.doesNotMatch(currentInputSurface, new RegExp(directive));
|
||
}
|
||
|
||
assert.doesNotMatch(catalog, /^### (?:20|21|22)\./m);
|
||
});
|
||
|
||
test('new business adaptation template enforces the two-block writing convention', () => {
|
||
const template = read('agent设计规范/templates/business-adaptation-template.md');
|
||
assert.match(template, /#### 模板\r?\n/);
|
||
assert.match(template, /#### 示例\r?\n/);
|
||
assert.match(template, /模板和示例必须紧邻出现/);
|
||
assert.match(template, /逐字段标明`(必填)\/(选填)`/);
|
||
});
|