diff --git a/.project-docs/30-worklog/tasks/20260909-hotel-form-readiness-01a0858b.md b/.project-docs/30-worklog/tasks/20260909-hotel-form-readiness-01a0858b.md new file mode 100644 index 0000000..1d884c2 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260909-hotel-form-readiness-01a0858b.md @@ -0,0 +1,63 @@ +# Hotel form readiness threshold correction + +## Scope and authorization + +- User authorized local code changes and a deployable plugin package; deployment and live ERP validation will be performed by the user. +- Correct hotel-create preflight rejecting an otherwise complete dynamic form with `lifecycle_form_incomplete:154:195`; preserve other business/write gates. +- Latest user direction: use `DoInfo_jiudian: 150` as the minimal correction, replacing the previously delivered structural readiness solution. +- Feature mode: this task owns its record, relevant extension implementation/regressions, and synchronized release files. No canonical memory edits. +- No subagents: repository instructions require explicit user consent, which has not been provided. + +## Worktree safety + +- Branch: `main` +- Worktree: `/Users/andy/IdeaProjects/LWLT-AIBOT` +- Base commit: `275564d1bf00b36bf2ea64edeff3d04a6a401bd0` +- Existing changes: untracked `.idea/`, unrelated and preserved. +- Other worktrees: none. +- Overlap result: Clear for source/release scope; `.idea/` excluded. + +## Current status — 0.5.172 minimal threshold correction + +- Relative to the original base, the only lifecycle-adapter implementation change is `DoInfo_jiudian: 195` → `DoInfo_jiudian: 150`. +- Restored the original readiness flow, removing this task's 0.5.171 structural/MaxI changes. The source was first verified byte-identical to the previously delivered 0.5.171 ZIP before replacing these known task-owned edits. +- Hotel create now accepts 150 or more serialized fields when original required-field/loading/stability and subsequent business gates pass. Update/clear stay at 150; other actions unchanged. Complete pages below 150 can still be blocked. +- Version synchronization advanced to 0.5.172; 0.5.171 ZIP and its manifest archived alongside 0.5.170. Deployment and live ERP acceptance remain the user's next step. + +## Current 0.5.172 verification + +- Targeted regressions PASS, 9/9: 149 blocked; 150/154/195/442 pass the quantity gate; required fields, loading, stability, identity, resource, slot finance/audit and ownership still enforced. +- Full verification PASS on `/private/tmp/ltjt-hotel-150-verify-ho763clc`, an otherwise identical copy excluding local secrets, `.idea/`, Finder files and generated output, with dependencies reused through a symlink. Original local files preserved. +- `node --run check:repo`: 10/10 PASS; `node --run check`: PASS; `node --run test:control-plane`: 183/183 PASS; `node --run test:legacy`: 287/287 PASS; `node --run build`: PASS. +- The 9 targeted tests are included in the 287 legacy tests. Temporary localhost mock HTTP tests ran within the already authorized validation scope; no ERP/runtime deployment access. +- `git diff --check`: PASS. ZIP entry set and all 20 source files match byte-for-byte, with release-manifest SHA-256 verified. Prior 0.5.171 archive retains its original manifest/hash. +- Final local review confirms the adapter differs from the committed base only in the requested 195 → 150 constant. Source and package are ready for the user's deployment test. + +## Superseded 0.5.171 implementation + +- Implemented hotel-create structural readiness: valid current positive MaxI, existing/enabled required slot-0 controls, connected form, no visible loading, and an observed stable interval of at least one second. Serialized count remains diagnostic; no replacement fixed minimum. +- MaxI, control names/ids/types/disabled state and select option counts participate in stability detection, including same-count structural changes. Timeout cannot substitute for an observed stable sample. +- Hotel update/clear and other actions retain their prior minimum counts. Full preflight still applies identity, resource, slot, finance/audit and ownership gates; native submit and post-save evidence paths are unchanged. +- Extension/source handshake, platform minimum, mapping, version assertions and release gate synchronized to `0.5.171`; previous `0.5.170` package/manifest preserved in dated release archive. + +## Previous 0.5.171 verification + +- Regression-first check reproduced `lifecycle_form_incomplete:154:195` on the original code; missing-field and update/clear baseline tests already passed. +- Targeted polling/structure and full-preflight regressions pass after the fix; fixtures are synthetic and do not represent a live page capture. +- ZIP has 20 files, with the complete entry set and every file (including README) byte-identical to current source. +- Initial full-check attempt exposed absent local dependencies (`tsc`, `tsx`, etc.). Installed locked dependencies with scripts disabled; the first network-restricted attempt was stopped, then installation succeeded with network permission. Lockfile unchanged. +- Root hygiene also reports pre-existing/unrelated `.idea/` and Finder `.DS_Store` files; these are preserved and excluded from the plugin package. +- `node --run check`: PASS; `node --run build`: PASS. +- `node --run test:control-plane`: PASS, 183/183. +- `node --run check:repo`: the original checkout reports two unrelated IDE/Finder hygiene failures; all 10 checks PASS on an otherwise identical validation copy excluding `.idea/`, `.DS_Store`, local secrets, and generated output. +- `node --run test:legacy`: PASS, 290/290, on that validation copy. Includes 12 new readiness/preflight regression tests. Localhost-only mock HTTP tests required permission outside the filesystem/network sandbox; no real application services or ERP were contacted. +- Initial legacy regression also found four escaped old-version assertions; these were synchronized to `0.5.171` before the final green run. +- Validation copy: `/private/tmp/ltjt-hotel-readiness-01a0858b-verify-96e49iaz`; dependencies reused through a symlink without reinstalling them during validation. Command outputs remain task-prefixed temporary files in `/private/tmp`, not repository evidence or deployed runtime logs. +- Final local review: scope limited to hotel-create readiness plus synchronized release metadata; native submit, resource selection, identity/ownership, slot finance/audit and post-save verification implementations unchanged. No subagent review claimed. +- `git diff --check`: PASS. Final package verified against every source file, including README; archived previous ZIP verified against its archived release manifest. +- No live ERP access, writes, service restarts, or deployment. + +## Follow-ups and promotion candidates + +- User deployment and live hotel-task acceptance pending; local 0.5.172 implementation, validation and package complete. +- Promotion candidate: canonical current state may advance the extension baseline to `0.5.172` after integration; the accepted implementation is the fixed 150 hotel-create threshold, not the superseded structural solution. No product capability or parser contract expansion. Evidence: this task's focused regressions and release verification. Human runtime validation remains required before asserting deployed ERP success. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index db32824..617a662 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -100,7 +100,7 @@ const persistedExtensionResultVersions = new Map(); let taskCreateInProgress = false; const AUTO_HANDOFF_RETRY_MS = 30_000; -const REQUIRED_EXTENSION_VERSION = '0.5.170'; +const REQUIRED_EXTENSION_VERSION = '0.5.172'; const MANUAL_HANDOFF_LABEL = '确认并提交到 ERP 插件'; const RETRY_HANDOFF_LABEL = '继续提交到 ERP 插件'; const RECONCILE_LABEL = '只读回查 ERP 现有结果'; diff --git a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md index a809500..a883eb5 100644 --- a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md +++ b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md @@ -2,7 +2,7 @@ ## 当前基线 -- Chrome 插件:`0.5.170` +- Chrome 插件:`0.5.172` - Agent Prompt:`ltjt-agent-prompt-v1.8-independent-headcount-categories` - 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08` - 五个 Skill:`0.5.126`;运营 DOCX:`0.5.127` @@ -12,6 +12,7 @@ - `0.5.167` 保留插件侧的空闲证明和受控重载安全协议,并继续包含 `0.5.166` 的散拼入口自适应等待。中央服务私有 OSS/ECS 云助手编排及迁移 019 已按用户决定从主线撤回,因此这些插件更新消息在当前主线没有服务端触发方,属于休眠能力;插件仍按现有人工发布/加载流程使用。 - `0.5.168` 将 ERP 身份门禁从整页子串搜索收紧为唯一 `#Lable_UserName` 登录节点的规范化精确全等;缺失、重复、空白、子串或其他页面文字命中均不再让云电脑进入可执行状态,观察到的账号文本不离开扩展。 - `0.5.170` 保留全部执行门禁,并新增 Program-only 散拼批量子单的“完整只读枚举后逐单执行、首错停止、不自动重试”门禁;ERP 写入不确定后的人工只读核验责任仍明确绑定到任务归属账号,管理员仍完全不进入任务数据面。 +- `0.5.172` 按维护方要求将酒店新增 `DoInfo_jiudian` 的固定字段门槛从 195 调整为 150,恢复原来的预检流程;`0.5.171` 的结构判定方案已撤回并归档。其他模式/业务原门槛及后续执行检查保留,低于 150 的页面仍会阻断。尚未部署或进行真实 ERP 写入验收;验证范围见[任务记录](../../../.project-docs/30-worklog/tasks/20260909-hotel-form-readiness-01a0858b.md)。 - 运营输入/DOCX `0.5.127` 补齐第 19 项`散拼团多个新增子单`的可复制模板与示例;该入口仍固定为 Program-only,不进入 Agent Prompt 或 Skill。 - 名单工作簿 normalizer `v1.4.0`、Skill `0.5.126` 与业务输入 `0.5.127` 继续只选择 12 个必需源字段;身份证、年龄、源证件类型及其他普通额外列不参与行识别、字段校验或 canonical 输出。必需字段完整性、宏/外链、全局超链接和外部公式等安全门槛保持失败关闭。 diff --git a/archive/releases/2026-09-09/README.md b/archive/releases/2026-09-09/README.md index 9ce6c86..7aabb45 100644 --- a/archive/releases/2026-09-09/README.md +++ b/archive/releases/2026-09-09/README.md @@ -4,3 +4,5 @@ - `release-manifest-0.5.170-skills-0.5.126-docx-0.5.126.json`:补齐散拼团多个新增子单运营模板前的历史清单快照。 - `老挝联泰AI指令表-0.5.126.docx`:被 `0.5.127` 的 19 项完整运营输入表替代;旧版缺少`散拼团多个新增子单`。 +- `ltjt-order-assistant-0.5.170.zip` 与 `release-manifest-0.5.170-skills-0.5.126-docx-0.5.127.json`:酒店新增表单就绪修复前的插件和清单快照,由插件 `0.5.171` 替代。 +- `ltjt-order-assistant-0.5.171.zip` 与 `release-manifest-0.5.171-skills-0.5.126-docx-0.5.127.json`:酒店新增结构判定方案及清单快照;维护方随后选择最小改动,由固定门槛 150 的 `0.5.172` 替代。 diff --git a/dist/ltjt-order-assistant-0.5.170.zip b/archive/releases/2026-09-09/ltjt-order-assistant-0.5.170.zip similarity index 100% rename from dist/ltjt-order-assistant-0.5.170.zip rename to archive/releases/2026-09-09/ltjt-order-assistant-0.5.170.zip diff --git a/archive/releases/2026-09-09/ltjt-order-assistant-0.5.171.zip b/archive/releases/2026-09-09/ltjt-order-assistant-0.5.171.zip new file mode 100644 index 0000000..6977ddb Binary files /dev/null and b/archive/releases/2026-09-09/ltjt-order-assistant-0.5.171.zip differ diff --git a/archive/releases/2026-09-09/release-manifest-0.5.170-skills-0.5.126-docx-0.5.127.json b/archive/releases/2026-09-09/release-manifest-0.5.170-skills-0.5.126-docx-0.5.127.json new file mode 100644 index 0000000..0f2ee05 --- /dev/null +++ b/archive/releases/2026-09-09/release-manifest-0.5.170-skills-0.5.126-docx-0.5.127.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-09-09", + "baselines": { + "chrome_extension": "0.5.170", + "skills": "0.5.126", + "business_instruction_docx": "0.5.127", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.170", + "path": "dist/ltjt-order-assistant-0.5.170.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "c041d13f468f46f1130668ae67701342b0dd920c5be20c5b47b97732384f48d8" + }, + { + "kind": "skill", + "name": "lwlt-arrangement", + "version": "0.5.126", + "path": "dist/lwlt-arrangement-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-arrangement", + "sha256": "83770befe3a8e4048eacaeacb567c5139bf8a0b3a49f2b1513c26383422f2a9c" + }, + { + "kind": "skill", + "name": "lwlt-confirmation", + "version": "0.5.126", + "path": "dist/lwlt-confirmation-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-confirmation", + "sha256": "543507ed8b68c183d2df6722061b0e927ebb728de4bf254d15f83311c9264018" + }, + { + "kind": "skill", + "name": "lwlt-lifecycle", + "version": "0.5.126", + "path": "dist/lwlt-lifecycle-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-lifecycle", + "sha256": "fff3879d9a4143c34ae51e091b91411e55382ad3f5d77c99d6202a005b1ddbd9" + }, + { + "kind": "skill", + "name": "lwlt-newbooking", + "version": "0.5.126", + "path": "dist/lwlt-newbooking-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-newbooking", + "sha256": "5eb6a7bf5649a0fcee3fbee488a124e1b1d018a642a3a49ebb5439ccf20ccc2f" + }, + { + "kind": "skill", + "name": "lwlt-updating", + "version": "0.5.126", + "path": "dist/lwlt-updating-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-updating", + "sha256": "3bd665207649cbb8e0e5ebb012c916720201fe160eea62bccdf1c3a1f0275d1d" + }, + { + "kind": "business_instruction_docx", + "version": "0.5.127", + "path": "dist/老挝联泰AI指令表-0.5.127.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "e1b8ae5a98d550ab0785692a01cf1c87b99e4edfaea39e4edbbab7341e585f29", + "source_sha256": "652af8ef645ae56af2dd9990f74b8078de230cb9b87acd13c289d1f4dae0738a", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/archive/releases/2026-09-09/release-manifest-0.5.171-skills-0.5.126-docx-0.5.127.json b/archive/releases/2026-09-09/release-manifest-0.5.171-skills-0.5.126-docx-0.5.127.json new file mode 100644 index 0000000..ef27de5 --- /dev/null +++ b/archive/releases/2026-09-09/release-manifest-0.5.171-skills-0.5.126-docx-0.5.127.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-09-09", + "baselines": { + "chrome_extension": "0.5.171", + "skills": "0.5.126", + "business_instruction_docx": "0.5.127", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.171", + "path": "dist/ltjt-order-assistant-0.5.171.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "ecd358cc1dc9cb79cc72b5a85789a6dceeba7646b421646aa61e3a848607f761" + }, + { + "kind": "skill", + "name": "lwlt-arrangement", + "version": "0.5.126", + "path": "dist/lwlt-arrangement-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-arrangement", + "sha256": "83770befe3a8e4048eacaeacb567c5139bf8a0b3a49f2b1513c26383422f2a9c" + }, + { + "kind": "skill", + "name": "lwlt-confirmation", + "version": "0.5.126", + "path": "dist/lwlt-confirmation-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-confirmation", + "sha256": "543507ed8b68c183d2df6722061b0e927ebb728de4bf254d15f83311c9264018" + }, + { + "kind": "skill", + "name": "lwlt-lifecycle", + "version": "0.5.126", + "path": "dist/lwlt-lifecycle-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-lifecycle", + "sha256": "fff3879d9a4143c34ae51e091b91411e55382ad3f5d77c99d6202a005b1ddbd9" + }, + { + "kind": "skill", + "name": "lwlt-newbooking", + "version": "0.5.126", + "path": "dist/lwlt-newbooking-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-newbooking", + "sha256": "5eb6a7bf5649a0fcee3fbee488a124e1b1d018a642a3a49ebb5439ccf20ccc2f" + }, + { + "kind": "skill", + "name": "lwlt-updating", + "version": "0.5.126", + "path": "dist/lwlt-updating-0.5.126.skill", + "source": "agent设计规范/skills/lwlt-updating", + "sha256": "3bd665207649cbb8e0e5ebb012c916720201fe160eea62bccdf1c3a1f0275d1d" + }, + { + "kind": "business_instruction_docx", + "version": "0.5.127", + "path": "dist/老挝联泰AI指令表-0.5.127.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "e1b8ae5a98d550ab0785692a01cf1c87b99e4edfaea39e4edbbab7341e585f29", + "source_sha256": "652af8ef645ae56af2dd9990f74b8078de230cb9b87acd13c289d1f4dae0738a", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/chrome-extension/ltjt-order-assistant/README.md b/chrome-extension/ltjt-order-assistant/README.md index 3ecf955..7c8f163 100644 --- a/chrome-extension/ltjt-order-assistant/README.md +++ b/chrome-extension/ltjt-order-assistant/README.md @@ -11,9 +11,13 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过 ## 当前版本 -当前源码版本为 `0.5.170`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。 +当前源码版本为 `0.5.172`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。 -0.5.170 当前重点: +0.5.172 当前重点: + +- 按维护方要求,将酒店新增 `DoInfo_jiudian` 的最少可序列化字段数从 195 调整为 150,恢复原有页面预检流程。154 字段页面可以通过数量检查;关键字段、loading、稳定 1 秒、资源、空槽、财务状态、归属和保存后回查检查保持原样。酒店修改/清空仍为 150,其他业务阈值不变。低于 150 的页面仍会阻断;真实 ERP 验收由部署方完成。 + +继续保留 0.5.170 的批量子单执行能力: - 新增 Program-only `shared_child_order_batch_create`:先逐日只读枚举日期段内所有产品匹配母团并锁定完整 `tid` 清单,再复用单母团子单路径顺序创建;首个失败或不确定结果立即停止且不自动重试。 diff --git a/chrome-extension/ltjt-order-assistant/inpage.js b/chrome-extension/ltjt-order-assistant/inpage.js index 9f70767..b2f4dc2 100644 --- a/chrome-extension/ltjt-order-assistant/inpage.js +++ b/chrome-extension/ltjt-order-assistant/inpage.js @@ -6403,7 +6403,7 @@ } window.LTJTOrderAssistant = { - version: '0.5.170', + version: '0.5.172', resolveNativeListSearchValues, lookupKeywordMatchesText, inspectLifecycleSearchCriteria: lifecycleSearchCriteria, diff --git a/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js b/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js index 4483808..db65ab1 100644 --- a/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js +++ b/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js @@ -155,7 +155,7 @@ const FORM_READINESS_MINIMUMS = Object.freeze({ DoInfo_daoyou: 12, DoInfo_cheliang: 90, - DoInfo_jiudian: 195, + DoInfo_jiudian: 150, DoInfo_piao: 120, DoInfo_qita: 120, DoInfoJH: 800, diff --git a/chrome-extension/ltjt-order-assistant/manifest.json b/chrome-extension/ltjt-order-assistant/manifest.json index af3e461..6979337 100644 --- a/chrome-extension/ltjt-order-assistant/manifest.json +++ b/chrome-extension/ltjt-order-assistant/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "联泰下单助手", - "version": "0.5.170", + "version": "0.5.172", "description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。", "permissions": [ "activeTab", diff --git a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js index 98701d2..f5efb93 100644 --- a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js +++ b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js @@ -944,7 +944,7 @@ window.LTJTOrderAssistant = { ...(window.LTJTOrderAssistant || {}), - version: '0.5.170', + version: '0.5.172', openTeamBatchForm, pingTeamBatchFrame, preflightTeamBatchNative, diff --git a/dist/ltjt-order-assistant-0.5.172.zip b/dist/ltjt-order-assistant-0.5.172.zip new file mode 100644 index 0000000..986824c Binary files /dev/null and b/dist/ltjt-order-assistant-0.5.172.zip differ diff --git a/dist/release-manifest.json b/dist/release-manifest.json index 0f2ee05..f818d93 100644 --- a/dist/release-manifest.json +++ b/dist/release-manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "generated_on": "2026-09-09", "baselines": { - "chrome_extension": "0.5.170", + "chrome_extension": "0.5.172", "skills": "0.5.126", "business_instruction_docx": "0.5.127", "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" @@ -10,10 +10,10 @@ "artifacts": [ { "kind": "chrome_extension", - "version": "0.5.170", - "path": "dist/ltjt-order-assistant-0.5.170.zip", + "version": "0.5.172", + "path": "dist/ltjt-order-assistant-0.5.172.zip", "source": "chrome-extension/ltjt-order-assistant", - "sha256": "c041d13f468f46f1130668ae67701342b0dd920c5be20c5b47b97732384f48d8" + "sha256": "5ded1ac1e5ed137f0997d1220ac3c76b66a7443f4fcda6c824e482f122d0c198" }, { "kind": "skill", diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json index 13e16d3..4462228 100644 --- a/mappings/lifecycle.mapping.json +++ b/mappings/lifecycle.mapping.json @@ -1,7 +1,7 @@ { "contract_version": "ltjt-lifecycle-v2.9-roster-leader-contact-2026-08", "current_agent_parse_prompt_version": "ltjt-agent-prompt-v1.8-independent-headcount-categories", - "current_extension_version": "0.5.170", + "current_extension_version": "0.5.172", "historical_test_marker": "TEST-202609", "scope": "/System/Business/", "erp_session_keepalive": { @@ -306,6 +306,14 @@ "arrangement_hotel": { "page": "/System/Business/teams_jiudian.asp", "action": "DoInfo_jiudian", + "create_form_readiness": { + "strategy": "serialized_control_minimum", + "minimum_successful_controls": 150, + "required_fields": ["riqi0", "riqis0", "danwei0", "danweiid0", "fangxing0", "jianshu0", "zt0", "beizhu0", "id0", "yf0", "sh0", "MaxI", "tdID", "ddID", "chufari", "SubmitButton"], + "stable_ms": 1000, + "stability": "retain the original serialized count, passenger row count, required field presence and visible loading checks", + "preserved_gates": ["resource matching", "empty slot 0", "payment and audit state", "exact ERP identity and ownership", "native success response and fresh requery"] + }, "live_validated_update": { "mode": "update", "target": "exact slot_index=0 plus native row_id", diff --git a/tools/hotel-form-readiness.test.mjs b/tools/hotel-form-readiness.test.mjs new file mode 100644 index 0000000..ccffbb6 --- /dev/null +++ b/tools/hotel-form-readiness.test.mjs @@ -0,0 +1,176 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import vm from 'node:vm'; + +const adapterSource = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8'); +// Exercise the actual polling function without adding a production message/API. +const testSource = adapterSource.replace( + 'assistant.preflightLifecycleOperation = preflightLifecycleOperation;', + 'assistant.testReadiness = (operation, form, timeout) => waitForFormReadiness(operation, contractFor(operation), form, timeout);\n' + + 'assistant.testSlot = arrangementSlotEvidence;\n' + + 'if (window.testProofs) { arrangementResourceProof = window.testProofs.resource; ownershipListProof = window.testProofs.ownership; }\n' + + 'assistant.preflightLifecycleOperation = preflightLifecycleOperation;' +); +const hotelFields = ['riqi0', 'riqis0', 'danwei0', 'danweiid0', 'fangxing0', 'jianshu0', 'zt0', 'beizhu0', 'id0', 'yf0', 'sh0', 'MaxI', 'tdID', 'ddID', 'chufari', 'SubmitButton']; + +function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, proofs } = {}) { + let now = 10000; + const initialTime = now; + const values = { MaxI: maxI, tdID: '123', ddID: '0', chufari: '2026-09-15' }; + const field = (name) => ({ + name, id: name, tagName: 'INPUT', type: name === 'SubmitButton' ? 'submit' : 'text', + disabled: false, value: values[name] ?? '' + }); + const form = { id: 'InfoForm1', isConnected: true, elements: hotelFields.map(field) }; + for (let i = hotelFields.length - 1; i < count; i += 1) form.elements.push(field(`extra${i}`)); + const state = { loading: false }; + const loadingElement = { + className: 'datagrid-mask', + getBoundingClientRect: () => ({ width: 50, height: 50, left: 0, top: 0, right: 50, bottom: 50 }), + getClientRects: () => [{}] + }; + const document = { + querySelectorAll: (selector) => selector.startsWith('.datagrid-mask') && state.loading ? [loadingElement] : [], + querySelector: () => form, + readyState: 'complete' + }; + const window = { + document, innerWidth: 1024, innerHeight: 768, + location: { pathname: '/System/Business/teams_jiudian.asp', href: 'https://erp.invalid/System/Business/teams_jiudian.asp?tdID=123' }, + testProofs: proofs, + getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' }) + }; + document.defaultView = window; + window.parent = window; + form.ownerDocument = document; + const sandbox = { + window, document, URL, URLSearchParams, TextEncoder, location: window.location, + Date: class extends Date { static now() { return now; } }, + setTimeout(callback, ms) { now += ms; tick({ elapsed: now - initialTime, form, state }); callback(); } + }; + window.setTimeout = sandbox.setTimeout; + vm.runInNewContext(testSource, sandbox); + const operation = { action: 'arrangement_hotel', data: { arrangement: { mode } } }; + return { + form, state, operation, api: window.LTJTOrderAssistant, + elapsed: () => now - initialTime, + run: (timeout = 2500) => window.LTJTOrderAssistant.testReadiness(operation, form, timeout) + }; +} + +test('hotel create accepts the reported complete 154-field form after one stable second', async () => { + const page = fixture(); + const result = await page.run(); + assert.equal(result.successful_control_count, 154); + assert.equal(result.ready, true, JSON.stringify(result.blockers)); + assert.ok(page.elapsed() >= 1000 && page.elapsed() < 2000); +}); + +test('hotel create fails closed without a form', async () => { + const page = fixture(); + const missing = await page.api.testReadiness(page.operation, null, 2500); + assert.equal(missing.ready, false); + assert.ok(missing.blockers.includes('lifecycle_complete_form_missing')); +}); + +test('hotel create uses the requested 150-field boundary', async () => { + for (const count of [149, 150, 154, 195, 442]) { + const result = await fixture({ count }).run(); + assert.equal(result.successful_control_count, count); + assert.equal(result.minimum_successful_controls, 150); + assert.equal(result.ready, count >= 150, `${count}: ${JSON.stringify(result.blockers)}`); + if (count < 150) assert.ok(result.blockers.includes(`lifecycle_form_incomplete:${count}:150`)); + } +}); + +test('hotel create rejects every missing required field even with more than 195 fields', async () => { + for (const name of hotelFields) { + const page = fixture({ count: 442 }); + page.form.elements = page.form.elements.filter((control) => control.name !== name); + const result = await page.run(); + assert.equal(result.ready, false, name); + assert.ok(result.missing_required_fields.includes(name), name); + } +}); + +test('hotel create waits for visible loading to disappear and then stabilize', async () => { + const page = fixture({ tick: ({ elapsed, state }) => { if (elapsed >= 750) state.loading = false; } }); + page.state.loading = true; + assert.equal((await page.run()).ready, true); + assert.equal(page.elapsed(), 1750); + const stuck = fixture(); + stuck.state.loading = true; + const result = await stuck.run(); + assert.equal(result.ready, false); + assert.ok(result.blockers.includes('lifecycle_loading_overlay_visible')); +}); + +test('hotel create retains the existing one-second count stability gate', async () => { + const grow = (form) => form.elements.push({ name: `extra${form.elements.length}`, tagName: 'INPUT', type: 'text', value: '' }); + const settling = fixture({ tick: ({ elapsed, form }) => { if (elapsed === 750) grow(form); } }); + assert.equal((await settling.run()).ready, true); + assert.equal(settling.elapsed(), 1750); + const changing = fixture({ tick: ({ form }) => grow(form) }); + const result = await changing.run(); + assert.equal(result.ready, false); + assert.ok(result.blockers.includes('lifecycle_form_not_stable')); +}); + +test('hotel update/clear and unrelated lifecycle actions retain their current count gates', async () => { + for (const mode of ['update', 'clear']) { + const result = await fixture({ count: 149, mode }).run(); + assert.equal(result.ready, false); + assert.ok(result.blockers.includes('lifecycle_form_incomplete:149:150')); + assert.equal((await fixture({ count: 154, mode }).run()).ready, true); + } + const page = fixture(); + page.operation.action = 'arrangement_vehicle'; + page.form.elements = page.form.elements.slice(0, 70); + const result = await page.run(); + assert.equal(result.ready, false); + assert.equal(result.minimum_successful_controls, 90); +}); + +test('154-field readiness still leaves occupied slots and payment/audit state blocked', async () => { + for (const [name, value, blocker] of [ + ['id0', '1234', 'arrangement_slot_occupied:id0'], + ['danweiid0', '5678', 'arrangement_slot_occupied:danweiid0'], + ['yf0', '1', 'arrangement_slot_financial_or_audit_state:yf0'], + ['sh0', '1', 'arrangement_slot_financial_or_audit_state:sh0'] + ]) { + const page = fixture(); + page.form.elements.find((item) => item.name === name).value = value; + assert.equal((await page.run()).ready, true); + const slot = page.api.testSlot(page.form, page.operation); + assert.equal(slot.ready, false); + assert.ok(slot.blockers.includes(blocker), JSON.stringify(slot.blockers)); + } +}); + +test('full hotel preflight preserves identity, resource and ownership gates after the count check', async () => { + for (const failure of ['', 'identity', 'resource', 'ownership', 'slot']) { + const calls = []; + const page = fixture({ proofs: { + resource: () => { calls.push('resource'); return { matched: failure !== 'resource', blockers: ['resource_mismatch'] }; }, + ownership: async () => { calls.push('ownership'); return { matched: failure !== 'ownership', blockers: ['account_mismatch'] }; } + } }); + page.operation.data.existing_refs = { + kind: 'independent_order', identifier: 'TEST-HOTEL-READINESS', tid: failure === 'identity' ? '999' : '123', ddid: '456', + resolved: true, resolution_source: 'erp_unique_match' + }; + Object.assign(page.operation.data.arrangement, { + start_date: '2026-09-16', end_date: '2026-09-18', room_count: 9, + resource: { id: '789', name: '测试酒店' }, status: '未安排', side_effect_policy: 'no_external' + }); + if (failure === 'slot') page.form.elements.find((item) => item.name === 'yf0').value = '1'; + const result = await page.api.preflightLifecycleOperation(page.operation); + assert.equal(result.preflight.form_readiness.ready, true); + assert.equal(result.status, failure ? 'lifecycle_preflight_blocked' : 'lifecycle_preflight_ready', JSON.stringify(result.blockers)); + const expected = { identity: 'form_tid_mismatch:', resource: 'resource_mismatch', ownership: 'ownership_proof:account_mismatch', slot: 'arrangement_slot_financial_or_audit_state:yf0' }; + if (failure) assert.ok(result.blockers.some((blocker) => blocker.startsWith(expected[failure])), JSON.stringify(result.blockers)); + assert.deepEqual(calls, failure === 'identity' ? [] : ['resource', ...(['', 'ownership'].includes(failure) ? ['ownership'] : [])]); + assert.equal(result.no_erp_write, true); + assert.equal(result.write_attempted, false); + } +}); diff --git a/tools/lifecycle-contract.test.mjs b/tools/lifecycle-contract.test.mjs index a43256c..6cbaeae 100644 --- a/tools/lifecycle-contract.test.mjs +++ b/tools/lifecycle-contract.test.mjs @@ -1374,7 +1374,7 @@ test('passenger explicit server success is terminal without a post-save row requ assert.doesNotMatch(passengerBranch, /passengerRequery|verifyLifecycleOperation/); assert.doesNotMatch(background, /attemptAutomaticPassengerReconciliation/); assert.match(background, /名单已取得 ERP 明确成功响应,按业务规则确认录入成功/); - assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.170'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.172'/); }); test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => { @@ -1584,7 +1584,7 @@ test('lifecycle execution keeps the MV3 worker alive until the durable task sett assert.match(background, /arrangement_resource_candidate_data_missing/); }); -test('extension 0.5.170 keeps reload dormant unless its durable execution state is fully idle', async () => { +test('extension 0.5.172 keeps reload dormant unless its durable execution state is fully idle', async () => { const [background, bridge] = await Promise.all([ readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'), readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8') @@ -2023,7 +2023,7 @@ test('delete guard distinguishes independent, shared child, and shared parent ro for (const operation of operations) assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; ')); const mapping = JSON.parse(await readFile(new URL('../mappings/lifecycle.mapping.json', import.meta.url), 'utf8')); - assert.equal(mapping.current_extension_version, '0.5.170'); + assert.equal(mapping.current_extension_version, '0.5.172'); assert.equal( mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'], 'ertrenshu' @@ -2407,10 +2407,10 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov assert.match(inpage, /`ys_danweiid\$\{index\}`, resolvedCustomerId/); assert.doesNotMatch(inpage, /product_customer_source_region|sourceRegionCheck|source_reference/); assert.doesNotMatch(teamBatchInpage, /product_customer_source_region|sourceRegionCheck/); - assert.equal(extensionManifest.version, '0.5.170'); - assert.match(inpage, /version: '0\.5\.170'/); - assert.match(teamBatchInpage, /version: '0\.5\.170'/); - assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.170'/); + assert.equal(extensionManifest.version, '0.5.172'); + assert.match(inpage, /version: '0\.5\.172'/); + assert.match(teamBatchInpage, /version: '0\.5\.172'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.172'/); assert.match(inpage, /function strictIsoDate\(value\)/); assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/); assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);