diff --git a/.project-docs/30-worklog/tasks/20260914-arrangement-linkage-stability-01a0858b.md b/.project-docs/30-worklog/tasks/20260914-arrangement-linkage-stability-01a0858b.md new file mode 100644 index 0000000..93c1996 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260914-arrangement-linkage-stability-01a0858b.md @@ -0,0 +1,24 @@ +# Arrangement linkage stability sampling + +## Scope and repository gate +- User authorizes fixing hotel create `arrangement_create_linked_fields_not_stable`; a same-team/same-resource success with different dates/quantity does not identify the failed run's cause. No live ERP inspection or writes are authorized. +- Branch main; worktree /Users/andy/IdeaProjects/LWLT-AIBOT; base 8d55e930a2c6f6a02f4cfc30dfee60cc2f134dde; sole worktree. Existing .idea/ and release ZIP 0.5.182 are known and preserved (ZIP may be archived byte-for-byte during release). Overlap: Clear. Previous plugin-status fix is committed in the current base. +- Feature scope: deterministic linked-field observation, failure diagnostics, synthetic tests, extension version/release synchronization. No input/Schema/Skill/DOCX, heartbeat, database or business-rule changes. Main thread only; no subagents. + +## Findings and plan +- Current helper checks its six-second deadline before reading the next sample. A delayed callback can therefore reject unchanged values without a second observation. Existing fake timers always run at the requested interval and miss this case. A continuously changing field produces the same blocker; the supplied log has no sample timing or changed-field evidence, so the live cause is unproven. +- Preserve the 1.5-second stable observation requirement, restart it after excessive sample gaps, and allow a bounded recovery window for missed scheduling. Never treat time spent unobserved as stability or silently retry a save. Fail on persistent changes, detached controls/forms or exhausted observation/wall budget. +- Capture field names and timing only, no business values. Carry create diagnostics through lifecycle evidence and readable failure output; keep raw blocker compatibility. +- Validate normal 6/12-room cases, delayed timers, real oscillation, late changes, boundary sampling, node loss/replacement, and no POST on failure using synthetic native forms. Run required repository/type/control-plane/legacy/build checks and verify release ZIP/source equality. Production acceptance remains pending. + +## Results +- Implemented the shared arrangement wait helper using a monotonic clock when available, six seconds of observed sampling budget and a 30-second wall deadline. Gaps over 1.5 seconds reset stability and consume no observed budget; normal completion requires at least three samples over 1.5 seconds. The final deadline sample is inspected. Existing controls must stay attached/enabled and identical; visible loading/native Ajax prevents readiness. A late-ending request is classified as loading rather than falsely reporting oscillating values. +- Indexed creates stop filling immediately on failed observation and never POST. Value-redacted timing/field names appear in `preflight.arrangement_linkage` and an additional diagnostic blocker retained by current task-event logging. The legacy blocker remains compatible. Worker failure messages distinguish delayed sampling, changing fields, loading and invalidated forms. Success logging/heartbeat behavior is unchanged. +- Synthetic native hotel flows cover 6 and 12 rooms, ordinary and six-second-delayed callbacks, one native resource selection, one save only after successful observation, persistent changes/delays with zero saves, and unchanged old rows. Vehicle/transport/other use the same delayed-sampling tests. Existing update/clear and post-validation write guards remain covered by the complete suite. +- Verification: 47 focused tests; 231/231 control-plane and 378/378 legacy (609 total, focused included), typecheck, build, diff whitespace checks PASS. Four escaped old-version assertions were synchronized during the version bump. Clean isolated-source repository checks 10/10 PASS; original-worktree checks remain 8/10 solely because of preserved .idea/.DS_Store and prior ignored archive logs. No cleanup of that pre-existing material. +- Release 0.5.183 synchronized across extension manifests/runtime version declarations, platform minimum, mapping, tests, active gate, ZIP and release manifest. ZIP/source checked byte-for-byte; old 0.5.182 ZIP and manifest archived unchanged. Skills/DOCX/parser/database versions unchanged. +- Main-thread final read-only review: PASS. Reviewed deadline semantics, delayed-sampling reset, bounded waiting, failure-message forwarding, no-value diagnostics, unchanged save guards and package equality. No subagent review claimed. User's specific 17:25 failed execution remains unobserved at runtime; the fix addresses proven code defects, not a claim of reproducing that entire production incident. +- [Immutable validation summary](../../../archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/README.md). Tests ran against a byte-verified isolated source copy excluding every non-example .env before reads; runtime dependencies were symlinked. No ERP access/write, task resubmission, deployment, restart, extension reload, external send, commit or push. + +## Promotion candidates +- Distinguish elapsed wall time from useful stability observations and retain value-free diagnostics. Delay is not evidence of instability or stability. Canonical promotion is deferred; production acceptance requires updating the plugin and refreshing ERP, then examining the real result under separate user authorization. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index afccd80..6766a7d 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -102,7 +102,7 @@ const persistedExtensionResultVersions = new Map(); let taskCreateInProgress = false; const AUTO_HANDOFF_RETRY_MS = 30_000; -const REQUIRED_EXTENSION_VERSION = '0.5.182'; +const REQUIRED_EXTENSION_VERSION = '0.5.183'; 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 48ba997..66f9fe3 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.182` +- Chrome 插件:`0.5.183` - 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.133` @@ -115,3 +115,5 @@ - `0.5.181` 与程序解析器 `v1.0.12` 保留安排变更原日期中省略的年份,按指定团号下 ERP 原记录的月日唯一定位后固定完整日期;显式年份严格匹配。酒店、用车、大交通和其他/备案一致;歧义显示候选并停止,不修改原日期。服务与插件需同步更新,无数据库迁移;真实 ERP 验收待部署方执行。见[任务记录](../../../.project-docs/30-worklog/tasks/20260914-arrangement-original-date-year-01a0858b.md)。 - `0.5.182` 修复 `0.5.181` 日期匹配组件未注入 ERP MAIN world 的回归。上一版测试手动装载依赖,未覆盖真实脚本装载清单;现已改用生产注入列表的全新隔离页面测试,并在依赖缺失时单独报错。原安排唯一性、原日期、ID 和付款/审核门禁保持。服务端已为 `v1.0.12` 时只需升级插件并刷新 ERP 页面,无迁移;真实 ERP 验收待部署方执行。[修复任务](../../../.project-docs/30-worklog/tasks/20260914-arrangement-original-date-year-01a0858b.md)。 + +- `0.5.183` 修复安排联动检测在定时回调延迟时缺少末次采样的误报:延迟间隔不作为稳定证据,恢复后重新观察,持续变化/延迟及失效表单继续在保存前阻断;增加字段名与采样间隔诊断。用户本次失败的具体触发因素尚未有现场证据;本地回归与发布校验完成后记录于[修复任务](../../../.project-docs/30-worklog/tasks/20260914-arrangement-linkage-stability-01a0858b.md),真实 ERP 验收待部署方执行。 diff --git a/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/README.md b/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/README.md new file mode 100644 index 0000000..eb21ad4 --- /dev/null +++ b/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/README.md @@ -0,0 +1,11 @@ +# Linkage observation validation — 2026-09-14 + +Immutable historical validation of extension 0.5.183; current behavior and deliverables are defined by active source and `dist/release-manifest.json`. No live ERP operation occurred. + +- Source defect: the former loop checked its six-second deadline before inspecting the resumed timer sample; stable values could be rejected without another observation. A synthetic delayed callback reproduced this boundary. The user's live cause is not proven by that reproduction. +- Current checks: 47 targeted cases included in 378 legacy tests; 231 control-plane tests; typecheck/build and clean-source repository 10/10 passed. Original worktree hygiene remains 8/10 due only to preserved IDE/OS files and prior ignored archived logs. +- Safety coverage: 6/12-room hotel creation, delayed sampling recovery, persistent changes and delays without saves, missing/replaced/detached controls, native Ajax including late completion, last-deadline sample, old-row preservation, native validation mutations, and vehicle/transport/other parity. No automatic save retry. +- Detailed diagnostics contain field names/timing only. Failure messages distinguish scheduling from field/loading observations. Real ERP acceptance remains pending. +- `summary.json` freezes source and release hashes plus validation results. Prior plugin ZIP 0.5.182 and its release manifest are preserved under `archive/releases/2026-09-14/`. + +Main-thread read-only review: PASS; no independent reviewer or production verification is claimed. diff --git a/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/summary.json b/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/summary.json new file mode 100644 index 0000000..488e0ea --- /dev/null +++ b/archive/evidence/2026-09-14/arrangement-linkage-stability-01a0858b/summary.json @@ -0,0 +1,49 @@ +{ + "base_commit": "8d55e930a2c6f6a02f4cfc30dfee60cc2f134dde", + "extension_version": "0.5.183", + "checks": { + "control_plane": { + "passed": 231, + "failed": 0 + }, + "legacy": { + "passed": 378, + "failed": 0 + } + }, + "typecheck": "PASS", + "build": "PASS", + "repository_clean_source": { + "passed": 10, + "failed": 0 + }, + "repository_original": { + "passed": 8, + "failed": 2, + "cause": "pre-existing IDE/OS residue and ignored prior archive logs" + }, + "review": { + "mode": "main-thread read-only", + "result": "PASS" + }, + "live_erp_validation": false, + "source_hashes": { + "chrome-extension/ltjt-order-assistant/lifecycle-adapters.js": "70f7e606f97770e41938d061b85894afb902c658de74da1eb8622629dbba0b90", + "chrome-extension/ltjt-order-assistant/background.js": "f40952e773cb4e6d24e2c666a3580452c22a9f51742760efe20ea0f14dba91ca", + "tools/arrangement-linkage-stability.test.mjs": "d2edb573d978d0247a00d894521216967e6782eb412c3ea5053b2d852a3ce11b", + "tools/hotel-create-empty-slot.test.mjs": "46cba4d2c4c0054f5d2fe247a950cfef3f54ee52a798069595ffed97f5e564a8", + "tools/arrangement-create-empty-slot.test.mjs": "6e83e0908fb46d5e45cf86f25f444e83aee038cd6fc901d3a39f956161067f5c", + "tools/lifecycle-contract.test.mjs": "d01bc62921f56a2e22e822805541e40a65ba482c89c8819808a4b5ee12c60d0c", + "mappings/lifecycle.mapping.json": "875ea6f1c0bc92237b33197672dfdeccbfde2b7852ad0b1630a906e32fda224a", + "LianSyn-platform/app.js": "d7681c3a65d4ce92fa11ab71e89ae8a79b61f2f67fea4178418bd44dd5c6946c", + "dist/release-manifest.json": "c37f1f12fe180df454e1e90e11a25029070610d39d19b344d99453d3f05e0407" + }, + "artifact": { + "kind": "chrome_extension", + "version": "0.5.183", + "path": "dist/ltjt-order-assistant-0.5.183.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "95e838dbed02f7892eb1ee6c57c32f5fc363bf7887276475209d78c685867433" + }, + "zip_source_files_verified": 20 +} diff --git a/archive/releases/2026-09-14/README.md b/archive/releases/2026-09-14/README.md index 4bced2f..76f6a64 100644 --- a/archive/releases/2026-09-14/README.md +++ b/archive/releases/2026-09-14/README.md @@ -12,3 +12,5 @@ Historical artifacts only; current rules and deliverables are defined by active - Plugin 0.5.180, instruction DOCX 0.5.132 and their manifest are retained unchanged before original-date year resolution release 0.5.181 / DOCX 0.5.133. Historical evidence only; use the active release manifest for deployment. - Plugin 0.5.181 and its manifest (DOCX 0.5.133) are preserved unchanged before corrective release 0.5.182. Version 0.5.181 omitted the date-matching dependency from ERP MAIN-world injection; its isolated helper tests did not detect this wiring regression. Do not deploy the archived ZIP. + +- Plugin 0.5.182 and its manifest (DOCX 0.5.133) are preserved byte-for-byte before linkage observation release 0.5.183. These are historical artifacts; current source and the active release manifest define deployment. diff --git a/archive/releases/2026-09-14/ltjt-order-assistant-0.5.182.zip b/archive/releases/2026-09-14/ltjt-order-assistant-0.5.182.zip new file mode 100644 index 0000000..360f3bf Binary files /dev/null and b/archive/releases/2026-09-14/ltjt-order-assistant-0.5.182.zip differ diff --git a/archive/releases/2026-09-14/release-manifest-0.5.182-0.5.133.json b/archive/releases/2026-09-14/release-manifest-0.5.182-0.5.133.json new file mode 100644 index 0000000..a4a3ac4 --- /dev/null +++ b/archive/releases/2026-09-14/release-manifest-0.5.182-0.5.133.json @@ -0,0 +1,69 @@ +{ + "manifest_version": 1, + "generated_on": "2026-09-14", + "baselines": { + "chrome_extension": "0.5.182", + "skills": "0.5.126", + "business_instruction_docx": "0.5.133", + "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" + }, + "artifacts": [ + { + "kind": "chrome_extension", + "version": "0.5.182", + "path": "dist/ltjt-order-assistant-0.5.182.zip", + "source": "chrome-extension/ltjt-order-assistant", + "sha256": "56601289c7a0a85d1120498af43887070ba03bab269adcdf2010c58cdbab8705" + }, + { + "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.133", + "path": "dist/老挝联泰AI指令表-0.5.133.docx", + "source": "agent设计规范/templates/business-input-templates.md", + "builder": "tools/build_business_instruction_docx.py", + "sha256": "879f1acc3a6a60842e59ae8396f72d39504ea1f3a4110a9f305d94553b38848c", + "source_sha256": "37b6b689c5c6a80683dd1deeff52a90ac8b4bd3ac0380e6b771dcb557c0918f7", + "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1" + } + ] +} diff --git a/chrome-extension/ltjt-order-assistant/README.md b/chrome-extension/ltjt-order-assistant/README.md index e86fa38..18b226c 100644 --- a/chrome-extension/ltjt-order-assistant/README.md +++ b/chrome-extension/ltjt-order-assistant/README.md @@ -11,9 +11,11 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过 ## 当前版本 -当前源码版本为 `0.5.182`。版本化 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.183`。版本化 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.182 当前重点: +0.5.183 当前重点: + +- 安排联动稳定检查将正常采样观察预算与定时回调延迟分开;长间隔后重新观察至少 1.5 秒并要求多个有效样本,30 秒总等待上限内恢复,持续变化、持续延迟、控件失效或原生加载未结束均不保存。新增失败回执区分检测延迟与字段变化,详细证据仅记录字段名、采样次数和时间,不记录业务值。保留原生联动、唯一空行、付款审核、其他行保护及保存前复核;不自动重试保存。合成回归覆盖,真实 ERP 验收待部署方执行。 - 修复 0.5.181 安排变更的页面依赖漏注入:在 ERP MAIN world 先加载 operation-plans.js,再加载页面适配器。缺失日期组件单独报错,不再误报原安排为 0 条。回归测试从后台实际注入列表加载全新页面环境,覆盖四类日期变更;服务端已为 v1.0.12 时,本修复只需更新插件并刷新 ERP 页面。真实 ERP 验收仍待部署方执行。 diff --git a/chrome-extension/ltjt-order-assistant/background.js b/chrome-extension/ltjt-order-assistant/background.js index 11ca1a7..7e3b70f 100644 --- a/chrome-extension/ltjt-order-assistant/background.js +++ b/chrome-extension/ltjt-order-assistant/background.js @@ -2069,6 +2069,19 @@ function lifecyclePreflightBusinessFailure(report = {}) { }; } +function arrangementLinkageFailureMessage(report = {}) { + if (!report.blockers?.includes('arrangement_create_linked_fields_not_stable')) return ''; + const reason = report.preflight?.arrangement_linkage?.reason; + const detail = { + sampling_delayed: '浏览器页面检测持续延迟,未能完成联动校验', + fields_changing: '安排表单的联动字段在观察期间未稳定', + native_busy: 'ERP 页面加载或联动请求未在观察预算内完成稳定校验', + form_detached: '安排表单在联动校验期间已关闭或刷新', + control_unavailable: '安排表单控件在联动校验期间发生变化或不可用' + }[reason]; + return detail ? `${detail},已停止,未提交 ERP 保存。` : ''; +} + function lifecycleLiveBusinessFailure(operation = {}, report = {}) { if (operation?.action !== 'passenger_list_import') return null; const blockers = Array.isArray(report.blockers) ? report.blockers.map(String) : []; @@ -2640,7 +2653,9 @@ async function executeLifecycleOperation(taskId, operation, operationPlan) { const writeStarted = live.write_attempted === true || live.no_erp_write === false; if (!writeStarted) { const businessFailure = lifecycleLiveBusinessFailure(executionOperation, live); + const linkageFailureMessage = arrangementLinkageFailureMessage(live); const message = businessFailure?.message + || linkageFailureMessage || '生命周期适配器在提交前阻断,未写入 ERP。'; await transitionCurrentExecution(taskId, 'blocked', { blocked_at: nowIso(), @@ -2649,6 +2664,12 @@ async function executeLifecycleOperation(taskId, operation, operationPlan) { await publish(taskId, executionOperation, 'lifecycle_live', live, { status: 'blocked', message, + ...(linkageFailureMessage ? { + error_code: 'arrangement_linkage_not_stable', + failure_stage: 'lifecycle_live', + failure_source: 'plugin_executor', + failure_message: linkageFailureMessage + } : {}), ...(businessFailure ? { error_code: businessFailure.errorCode, failure_stage: 'lifecycle_live', diff --git a/chrome-extension/ltjt-order-assistant/inpage.js b/chrome-extension/ltjt-order-assistant/inpage.js index d5dd86f..0ed85bf 100644 --- a/chrome-extension/ltjt-order-assistant/inpage.js +++ b/chrome-extension/ltjt-order-assistant/inpage.js @@ -6543,7 +6543,7 @@ } window.LTJTOrderAssistant = { - version: '0.5.182', + version: '0.5.183', 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 459cda1..b2f293f 100644 --- a/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js +++ b/chrome-extension/ltjt-order-assistant/lifecycle-adapters.js @@ -2438,21 +2438,73 @@ else changes.push({ field: match.name, target: match.name, operation, before: changed.before, after: changed.after }); } - async function waitForStable(form, names, timeoutMs = 6000) { - const deadline = Date.now() + timeoutMs; - let previous = ''; - let stableSince = Date.now(); - while (Date.now() < deadline) { - const current = names.map((name) => { - const control = getControl(form, name); - return `${name}=${control?.type === 'checkbox' ? control.checked : control?.value ?? ''}`; - }).join('&'); - if (current !== previous) stableSince = Date.now(); - if (current === previous && Date.now() - stableSince >= 1500) return true; - previous = current; - await sleep(250); + async function waitForStable(form, names, timeoutMs = 6000, diagnostic = {}) { + const targetWindow = form?.ownerDocument?.defaultView || window; + const clock = () => targetWindow.performance?.now?.() ?? Date.now(); + const started = clock(); + const wallLimit = timeoutMs * 5; + const initialControls = names.map((name) => getControl(form, name)); + const changedFields = new Set(); + let previous = null; + let lastSampleAt = started; + let observedMs = 0; + let stableMs = 0; + let stableSamples = 0; + let sampleCount = 0; + let delayedSamples = 0; + let busySamples = 0; + let maxGapMs = 0; + const finish = (ready, reason, now, busy, unavailable) => { + Object.assign(diagnostic, { ready, reason, elapsed_ms: Math.round(now - started), + observed_ms: Math.round(observedMs), stable_ms: Math.round(stableMs), required_stable_ms: 1500, + observation_budget_ms: timeoutMs, wall_limit_ms: wallLimit, sample_count: sampleCount, + delayed_sample_count: delayedSamples, max_sample_gap_ms: Math.round(maxGapMs), + changed_fields: [...changedFields].sort(), unavailable_fields: unavailable, + native_busy: busy, native_busy_sample_count: busySamples, value_redacted: true }); + return ready; + }; + while (true) { + const controls = names.map((name) => getControl(form, name)); + const values = controls.map((control) => control?.type === 'checkbox' ? control.checked : control?.value ?? ''); + const now = clock(); + const gap = now - lastSampleAt; + const delayed = previous !== null && gap > 1500; + const unavailable = names.filter((name, i) => initialControls[i] + && (controls[i] !== initialControls[i] || controls[i]?.isConnected === false || controls[i]?.disabled === true)); + const busy = Number(targetWindow.jQuery?.active || 0) > 0 || visibleLoadingSignals(targetWindow).length > 0; + if (busy) busySamples++; + sampleCount++; + maxGapMs = Math.max(maxGapMs, gap); + if (delayed) delayedSamples++; + if (previous && !delayed) observedMs += gap; + const changed = names.filter((name, i) => previous + && (values[i] !== previous.values[i] || controls[i] !== previous.controls[i])); + changed.forEach((name) => changedFields.add(name)); + // A long unobserved interval is not proof of stability. Start a fresh + // observation window when scheduling resumes, within a hard wall limit. + if (!previous || changed.length || delayed || busy) { + stableMs = 0; + stableSamples = busy ? 0 : 1; + } else { + if (stableSamples > 0) stableMs += gap; + stableSamples++; + } + if (form?.isConnected === false) return finish(false, 'form_detached', now, busy, unavailable); + if (unavailable.length || !controls.some(Boolean)) return finish(false, 'control_unavailable', now, busy, unavailable); + if (!busy && stableSamples >= 3 && stableMs >= 1500 && now - started <= wallLimit) { + return finish(true, 'stable', now, busy, unavailable); + } + // Inspect the final sample before deciding: the stable interval may end + // exactly at the observation deadline. Timeouts never authorize a save. + if (observedMs >= timeoutMs || now - started >= wallLimit) { + const reason = observedMs < timeoutMs && delayedSamples ? 'sampling_delayed' + : busy || (!changedFields.size && busySamples) ? 'native_busy' : 'fields_changing'; + return finish(false, reason, now, busy, unavailable); + } + previous = { values, controls }; + lastSampleAt = now; + await sleep(Math.min(250, timeoutMs - observedMs, wallLimit - (now - started))); } - return false; } function requireControlValue(form, name, expected, blockers, { optional = false } = {}) { @@ -2737,7 +2789,14 @@ applyExact(form, field('shuliang0'), 'set', quantity, changes, blockers); } const stableFields = ['danwei0', 'danweiid0', 'fangxing0', 'chehao0', 'shuoming0', 'riqi0', 'riqis0', 'jianshu0', 'shuliang0', 'tianshu0', 'wanshu0', 'danjia0', 'jine0'].map(field); - if (!await waitForStable(form, stableFields)) blockers.push('arrangement_create_linked_fields_not_stable'); + const linkageWait = {}; + if (!await waitForStable(form, stableFields, 6000, linkageWait)) { + blockers.push('arrangement_create_linked_fields_not_stable'); + // Keep the legacy blocker, and add value-free evidence to task-event + // logs as well as the detailed lifecycle report. + blockers.push(`arrangement_linkage_wait:${linkageWait.reason}:elapsed_ms=${linkageWait.elapsed_ms}:samples=${linkageWait.sample_count}:max_gap_ms=${linkageWait.max_sample_gap_ms}:changed_fields=${linkageWait.changed_fields.join(',') || 'none'}:unavailable_fields=${linkageWait.unavailable_fields.join(',') || 'none'}`); + return { changes, blockers, arrangementSlot: slot, linkageWait }; + } // Native date/room callbacks may reset status and remark. Set and assert // the requested final values only after those callbacks have settled. applyExact(form, field('zt0'), 'set', value.status, changes, blockers, { optional: action === 'arrangement_other' }); @@ -2749,7 +2808,7 @@ } const writeGuard = () => arrangementCreateWriteGuard(form, operation, signature, controls, preserved); blockers.push(...writeGuard()); - return { changes, blockers, arrangementSlot: slot, writeGuard }; + return { changes, blockers, arrangementSlot: slot, writeGuard, linkageWait }; } async function applyArrangement(form, operation) { @@ -4446,6 +4505,7 @@ changes = applied.changes; applyBlockers = applied.blockers; writeGuard = applied.writeGuard || null; + if (applied.linkageWait) preflight.preflight.arrangement_linkage = applied.linkageWait; } else if (operation.action.startsWith('order_update_')) { const applied = applyUpdates(form, operation); const receivable = applyReceivableFixture(form, operation); diff --git a/chrome-extension/ltjt-order-assistant/manifest.json b/chrome-extension/ltjt-order-assistant/manifest.json index e4e48b9..9510188 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.182", + "version": "0.5.183", "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 f958f6d..574d423 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.182', + version: '0.5.183', openTeamBatchForm, pingTeamBatchFrame, preflightTeamBatchNative, diff --git a/dist/release-manifest.json b/dist/release-manifest.json index a4a3ac4..7be3184 100644 --- a/dist/release-manifest.json +++ b/dist/release-manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "generated_on": "2026-09-14", "baselines": { - "chrome_extension": "0.5.182", + "chrome_extension": "0.5.183", "skills": "0.5.126", "business_instruction_docx": "0.5.133", "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories" @@ -10,10 +10,10 @@ "artifacts": [ { "kind": "chrome_extension", - "version": "0.5.182", - "path": "dist/ltjt-order-assistant-0.5.182.zip", + "version": "0.5.183", + "path": "dist/ltjt-order-assistant-0.5.183.zip", "source": "chrome-extension/ltjt-order-assistant", - "sha256": "56601289c7a0a85d1120498af43887070ba03bab269adcdf2010c58cdbab8705" + "sha256": "95e838dbed02f7892eb1ee6c57c32f5fc363bf7887276475209d78c685867433" }, { "kind": "skill", diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json index 96f7d85..d3af4aa 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.182", + "current_extension_version": "0.5.183", "original_arrangement_date_selection": "Hotel, vehicle, transport and other/filing update selection accepts MM-DD or YYYY-MM-DD; omitted year matches original ERP month/day within the exact group only. Require exactly one persisted row, then freeze its actual full dates and id. Full year is exact; mixed/cross-year endpoints are matched independently with actual range order checked. No match or ambiguity blocks before mutation; show up to five original candidates and ask to resubmit complete dates. Creation and legacy target-date changes retain existing semantics.", "historical_test_marker": "TEST-202609", "scope": "/System/Business/", @@ -287,6 +287,7 @@ "subpage_identity": "host/list ownership still requires the full created ref (including independent ddid), while native teams*.asp forms prove the target by tdID because they do not carry ddid", "positive_quantity_required": true, "preserved_quantity_exception": "other update may retain its existing empty/zero quantity when no new quantity is provided", + "linked_field_stability": {"stable_ms": 1500, "minimum_samples": 3, "maximum_sample_gap_ms": 1500, "observation_budget_ms": 6000, "wall_limit_ms": 30000, "policy": "restart observation after a delayed sample; inspect the deadline sample; require unchanged live controls and native idle; timeout never authorizes save", "create_diagnostics": "preflight.arrangement_linkage plus value-redacted blocker timing/field names"}, "linkage_sequence": "indexed create: selected row native resource linkage -> date/quantity -> wait stable -> status/remark -> preserve other rows and recheck after native validation", "preflight": ["indexed cost create: native MaxI and continuous complete rows; guide/update/clear retain their existing count gates", "no visible loading indicator", "stable form for at least 1 second", "exact current-run ownership list proof"], "requery": ["fresh arrangement subpage GET", "create returns row/resource target", "clear proves target row/resource/marker absent", "native PrintGridLists with Tpage/P_Size/riqi/departure-date/publication-unit/team-number"] diff --git a/tools/arrangement-create-empty-slot.test.mjs b/tools/arrangement-create-empty-slot.test.mjs index 48425e5..c26eafa 100644 --- a/tools/arrangement-create-empty-slot.test.mjs +++ b/tools/arrangement-create-empty-slot.test.mjs @@ -15,10 +15,11 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l teamSummaryRequery = async (operation, subpage) => subpage; assistant.preflightLifecycleOperation = preflightLifecycleOperation;`); -function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {} } = {}) { +function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [] } = {}) { let now = 10000; let posts = 0; let selections = 0; + let linkageTicks = 0; let savedForm; const form = { elements: [], id: 'InfoForm1', isConnected: true }; const fields = new Map(); @@ -73,7 +74,10 @@ function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => return savedForm.elements.find((control) => control.name === name) || null; } }, text: 'LW-TEST-HOTEL', response: { ok: true, status: 200 } }); const sandbox = { window, document, location, Event: window.Event, URL, URLSearchParams, TextEncoder, - Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { now += ms; callback(); } }; + Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { + now += selections ? linkageDelays[linkageTicks++] ?? ms : ms; + callback(); + } }; window.setTimeout = sandbox.setTimeout; vm.runInNewContext(inpage, sandbox); vm.runInNewContext(adapter, sandbox); @@ -86,6 +90,16 @@ function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => } for (const type of ['vehicle', 'transport', 'other']) { + test(`${type} create recovers from a delayed linkage sample but never saves while sampling stays delayed`, async () => { + for (const [linkageDelays, completed] of [[[6000], true], [Array(6).fill(6000), false]]) { + const f = fixture(type, { linkageDelays }); + const result = await f.api.liveSubmitLifecycleOperation(f.operation); + assert.equal(result.status, completed ? 'lifecycle_completed' : 'lifecycle_apply_blocked', JSON.stringify(result)); + assert.equal(f.posts(), completed ? 1 : 0); assert.equal(f.selections(), 1); + assert.equal(result.preflight.arrangement_linkage.reason, completed ? 'stable' : 'sampling_delayed'); + } + }); + test(`${type} create preserves old rows and binds native selection, dates and quantity to empty rows 0/1/10`, async () => { for (const occupied of [0, 1, 10]) { const f = fixture(type, { occupied }); diff --git a/tools/arrangement-linkage-stability.test.mjs b/tools/arrangement-linkage-stability.test.mjs new file mode 100644 index 0000000..b873530 --- /dev/null +++ b/tools/arrangement-linkage-stability.test.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import vm from 'node:vm'; +import { readFileSync } from 'node:fs'; + +const source = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8') + .replace('assistant.preflightLifecycleOperation = preflightLifecycleOperation;', + 'assistant.testWaitForStable = waitForStable; assistant.preflightLifecycleOperation = preflightLifecycleOperation;'); +const background = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'); + +function fixture({ delays = [], onTick = () => {} } = {}) { + let now = 0; + let ticks = 0; + const control = { name: 'jine1', id: 'jine1', type: 'text', value: 'private-value', isConnected: true }; + const document = { querySelectorAll: () => [], readyState: 'complete' }; + const window = { document, LTJTOrderAssistant: {}, jQuery: { active: 0 }, performance: { now: () => now }, + setTimeout(callback, ms) { now += delays[ticks] ?? ms; onTick({ now, ticks: ++ticks, control, form, window }); callback(); } }; + window.parent = window; document.defaultView = window; + const form = { elements: [control], isConnected: true, ownerDocument: document }; + vm.runInNewContext(source, { window, Date: class extends Date { static now() { return now; } }, URL, URLSearchParams }); + const diagnostic = {}; + return { control, form, window, diagnostic, now: () => now, + run: () => window.LTJTOrderAssistant.testWaitForStable(form, ['jine1', 'optional1'], 6000, diagnostic) }; +} + +test('ordinary linkage requires multiple observations over 1.5 seconds and redacts values', async () => { + const f = fixture(); + assert.equal(await f.run(), true); + assert.equal(f.now(), 1500); + assert.ok(f.diagnostic.sample_count >= 3); + assert.equal(f.diagnostic.ready, true); + assert.equal(f.diagnostic.reason, 'stable'); + assert.equal(JSON.stringify(f.diagnostic).includes('private-value'), false); +}); + +test('one delayed callback restarts observation instead of failing or counting the blind interval', async () => { + const f = fixture({ delays: [6000] }); + assert.equal(await f.run(), true); + assert.equal(f.now(), 7500); + assert.equal(f.diagnostic.delayed_sample_count, 1); + assert.equal(f.diagnostic.max_sample_gap_ms, 6000); + assert.equal(f.diagnostic.stable_ms, 1500); +}); + +test('one-second sampling still proves stability; persistent long gaps fail boundedly', async () => { + const slower = fixture({ delays: [1000, 1000] }); + assert.equal(await slower.run(), true); + assert.equal(slower.now(), 2000); + const paused = fixture({ delays: Array(6).fill(6000) }); + assert.equal(await paused.run(), false); + assert.equal(paused.now(), 30000); + assert.equal(paused.diagnostic.reason, 'sampling_delayed'); + assert.equal(paused.diagnostic.stable_ms, 0); +}); + +test('continuous value changes fail and report field names rather than their values', async () => { + const f = fixture({ onTick: ({ control, ticks }) => { control.value = `secret-${ticks}`; } }); + assert.equal(await f.run(), false); + assert.equal(f.now(), 6000); + assert.equal(f.diagnostic.reason, 'fields_changing'); + assert.deepEqual([...f.diagnostic.changed_fields], ['jine1']); + assert.equal(JSON.stringify(f.diagnostic).includes('secret-'), false); +}); + +test('the last allowed sample is checked; a late change restarts stability', async () => { + const f = fixture({ onTick: ({ now, control }) => { if (now <= 4500) control.value = String(now); } }); + assert.equal(await f.run(), true); + assert.equal(f.now(), 6000); + const late = fixture({ delays: [6000], onTick: ({ now, control }) => { if (now === 7250) control.value = 'late'; } }); + assert.equal(await late.run(), true); + assert.equal(late.now(), 8750); +}); + +test('detached form, missing/replaced controls and pending native Ajax cannot pass', async () => { + for (const mutate of [ + ({ form }) => { form.isConnected = false; }, + ({ form }) => { form.elements = []; }, + ({ form, control }) => { form.elements = [{ ...control }]; }, + ({ control }) => { control.isConnected = false; } + ]) { + const f = fixture({ onTick: mutate }); + assert.equal(await f.run(), false); + assert.match(f.diagnostic.reason, /form_detached|control_unavailable/); + } + const busy = fixture(); busy.window.jQuery.active = 1; + assert.equal(await busy.run(), false); + assert.equal(busy.diagnostic.reason, 'native_busy'); + const finishes = fixture({ onTick: ({ now, window }) => { if (now >= 1000) window.jQuery.active = 0; } }); + finishes.window.jQuery.active = 1; + assert.equal(await finishes.run(), true); + assert.equal(finishes.now(), 2500); + const late = fixture({ onTick: ({ now, window }) => { if (now >= 5500) window.jQuery.active = 0; } }); + late.window.jQuery.active = 1; + assert.equal(await late.run(), false); + assert.equal(late.diagnostic.reason, 'native_busy', 'late Ajax completion is not evidence of oscillating field values'); + assert.equal(late.diagnostic.native_busy, false); + assert.ok(late.diagnostic.native_busy_sample_count > 0); +}); + +test('background reports the observed failure reason without classifying all failures as changing fields', () => { + const start = background.indexOf('function arrangementLinkageFailureMessage('); + const end = background.indexOf('\nfunction lifecycleLiveBusinessFailure(', start); + const message = vm.runInNewContext(`${background.slice(start, end)}; arrangementLinkageFailureMessage;`); + for (const [reason, expected] of [['sampling_delayed', '检测持续延迟'], ['fields_changing', '联动字段在观察期间未稳定'], + ['native_busy', '联动请求'], ['form_detached', '关闭或刷新'], ['control_unavailable', '控件']]) { + const result = message({ blockers: ['arrangement_create_linked_fields_not_stable'], preflight: { arrangement_linkage: { reason } } }); + assert.ok(result.includes(expected)); assert.ok(result.includes('未提交 ERP 保存')); + } + assert.equal(message({ blockers: ['other_error'] }), ''); + assert.equal(message({ blockers: ['arrangement_create_linked_fields_not_stable'] }), ''); + assert.match(background, /failure_message: linkageFailureMessage/); +}); diff --git a/tools/hotel-create-empty-slot.test.mjs b/tools/hotel-create-empty-slot.test.mjs index 4beb2f0..85dd8bf 100644 --- a/tools/hotel-create-empty-slot.test.mjs +++ b/tools/hotel-create-empty-slot.test.mjs @@ -15,10 +15,11 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l teamSummaryRequery = async (operation, subpage) => subpage; assistant.preflightLifecycleOperation = preflightLifecycleOperation;`); -function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {} } = {}) { +function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {} } = {}) { let now = 10000; let posts = 0; let selections = 0; + let linkageTicks = 0; let savedForm; const form = { elements: [], id: 'InfoForm1', isConnected: true }; const fields = new Map(); @@ -69,7 +70,11 @@ function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, on return savedForm.elements.find((control) => control.name === name) || null; } }, text: 'LW-TEST-HOTEL', response: { ok: true, status: 200 } }); const sandbox = { window, document, location, Event: window.Event, URL, URLSearchParams, TextEncoder, - Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { now += ms; callback(); } }; + Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { + now += selections ? linkageDelays[linkageTicks] ?? ms : ms; + if (selections) onLinkageTick(fields, ++linkageTicks); + callback(); + } }; window.setTimeout = sandbox.setTimeout; vm.runInNewContext(inpage, sandbox); vm.runInNewContext(adapter, sandbox); @@ -101,6 +106,35 @@ test('hotel create binds the first empty row through preflight, native linkage, } }); +test('6/12-room hotel create recovers after delayed sampling, with only one native selection and POST', async () => { + for (const roomCount of [6, 12]) for (const linkageDelays of [[], [6000]]) { + const f = fixture({ linkageDelays }); + f.operation.data.arrangement.room_count = roomCount; + const result = await f.api.liveSubmitLifecycleOperation(f.operation); + assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result)); + assert.equal(f.fields.get('jianshu1').value, String(roomCount)); + assert.equal(f.posts(), 1); assert.equal(f.selections(), 1); + assert.equal(result.preflight.arrangement_linkage.elapsed_ms, linkageDelays.length ? 7500 : 1500); + assert.equal(result.preflight.arrangement_linkage.ready, true); + } +}); + +test('unstable or persistently delayed hotel linkage records diagnostics and never saves', async () => { + const changing = fixture({ onLinkageTick(fields, tick) { fields.get('jine1').value = String(tick); } }); + const paused = fixture({ linkageDelays: Array(6).fill(6000) }); + for (const [f, reason] of [[changing, 'fields_changing'], [paused, 'sampling_delayed']]) { + const result = await f.api.liveSubmitLifecycleOperation(f.operation); + assert.equal(result.status, 'lifecycle_apply_blocked', JSON.stringify(result)); + assert.equal(result.write_attempted, false); assert.equal(result.no_erp_write, true); + assert.equal(f.posts(), 0); assert.equal(f.selections(), 1); + assert.equal(f.fields.get('zt1').value, '', 'do not continue filling after failed observation'); + assert.equal(result.preflight.arrangement_linkage.reason, reason); + assert.ok(result.blockers.includes('arrangement_create_linked_fields_not_stable')); + assert.ok(result.blockers.some(value => value.startsWith(`arrangement_linkage_wait:${reason}:`))); + assert.equal(JSON.stringify(result.preflight.arrangement_linkage).includes('测试新酒店'), false); + } +}); + test('hotel create matches the new record after ERP sorts rows and uses persisted evidence on read-only reconciliation', async () => { const f = fixture({ sort: true }); const result = await f.api.liveSubmitLifecycleOperation(f.operation); diff --git a/tools/lifecycle-contract.test.mjs b/tools/lifecycle-contract.test.mjs index 1e3e54b..a4642b2 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\.182'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/); }); 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.182 keeps reload dormant unless its durable execution state is fully idle', async () => { +test('extension 0.5.183 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') @@ -2035,7 +2035,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.182'); + assert.equal(mapping.current_extension_version, '0.5.183'); assert.equal( mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'], 'ertrenshu' @@ -2419,10 +2419,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.182'); - assert.match(inpage, /version: '0\.5\.182'/); - assert.match(teamBatchInpage, /version: '0\.5\.182'/); - assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.182'/); + assert.equal(extensionManifest.version, '0.5.183'); + assert.match(inpage, /version: '0\.5\.183'/); + assert.match(teamBatchInpage, /version: '0\.5\.183'/); + assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/); assert.match(inpage, /function strictIsoDate\(value\)/); assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/); assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);