From a22799711822bfeb05a939ae870e0242e824ab24 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 03:10:16 +0700 Subject: [PATCH 1/4] fix(P1.3): prevent health watchdog from cutting off slow STOP confirmation Suspend only ordinary SHOW health accounting during authorized firmware quiescence; keep heartbeat, OS link monitoring and fresh SV/PTP ACK/readback mandatory. Retire stale deferred reads at maintenance entry, suppress their replay, and apply bounded 40s PTP/45s supervised STOP windows grounded in field traces where STOP ACK followed after 12 seconds. Add S8 regression for premature COM retirement, proof isolation and restored normal health. No forced flash, no silent service restart. --- .../src/DeterministicSessionHarness.cpp | 42 +++++++++++++++++++ apps/arstack_studio/src/DeviceController.cpp | 23 ++++++++-- apps/arstack_studio/src/DeviceController.hpp | 6 +++ .../src/SmartSessionController.hpp | 8 +++- .../src/StudioDeviceController.hpp | 18 ++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index b3c659652..75e73b2da 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -44,6 +44,7 @@ class DeterministicSessionHarness final { {"dual-revision catalog selects only qualified silicon", revisionCatalogFailClosed()}, {"firmware timeout retains port until flasher exits", firmwarePortReleaseWaitsForWorker()}, {"firmware update waits for fresh SV and PTP STOP readback", firmwareStopProofPreventsEarlyFlash()}, + {"slow firmware STOP suspends ordinary health teardown, resumes afterward", maintenanceStopDoesNotTriggerHealthDisconnect()}, {"real timed-out firmware subprocess retires before COM release", firmwareWorkerTimeoutAwaitsProcessExit()}, {"current identity -> READY without recovery", currentIdentityNeverRecovers()}, {"legacy identity -> firmware update", legacyIdentityOffersUpdate()}, @@ -216,6 +217,42 @@ class DeterministicSessionHarness final { } + static bool maintenanceStopDoesNotTriggerHealthDisconnect() { + Fixture f; + if (!f.profileReady) return false; + seedVerified(f, identity(), QStringLiteral("COM7"), false); + auto& d = f.device; + d.controlHealthTimer_.stop(); + d.controlResponsive_ = true; + d.healthProbeOutstanding_ = true; + d.missedHealthReplies_ = 1; + d.firmwareQuiescenceActive_ = true; + d.firmwareQuiescenceSession_ = d.sessionGeneration_; + d.firmwareQuiescenceBoot_ = d.identity_.bootId; + bool failed = false; + QObject::connect(&d, &StudioDeviceController::controlHealthFailed, &d, + [&failed] { failed = true; }); + // The same two health ticks that previously tore down COM must not + // compete with supervised STOP. No fake STOP proof is synthesized. + d.serviceControlHealth(); + d.serviceControlHealth(); + const bool protectedDuringStop = + !failed && d.connected_ && d.deviceVerified_ && + d.controlResponsive_ && d.missedHealthReplies_ == 0 && + !d.healthProbeOutstanding_ && !d.firmwareQuiescenceVerified(); + const bool boundedProof = + DeviceController::firmwarePtpStopProofDeadlineMs() == 40000 && + SmartSessionController::firmwareVerifiedStopDeadlineMs() == 45000 && + SmartSessionController::firmwareTotalDeadlineMs() == 300000; + d.endFirmwareQuiescence(); + // Ordinary health monitoring resumes on the original controller. + // An unresponsive board is still rejected outside maintenance. + d.healthProbeOutstanding_ = true; + d.serviceControlHealth(); + return protectedDuringStop && boundedProof && !failed && + d.missedHealthReplies_ == 1 && !d.controlResponsive_; + } + static bool firmwareStopProofPreventsEarlyFlash() { Fixture f; if (!f.profileReady) return false; @@ -231,11 +268,16 @@ class DeterministicSessionHarness final { d.ptpRunning_ = true; d.ptpStartStage_ = DeviceController::PtpStartStage::stop_unconfirmed; d.scenarioStage_ = DeviceController::ScenarioStage::awaiting_commit; + d.deferredShow_ = true; + d.deferredPtpShow_ = true; + d.deferredProfileShow_ = true; const bool started = s.beginFirmwareUpdate(); const bool safeStage = started && s.updateRequested_ && s.updateStage_ == SmartSessionController::UpdateStage::stopping && s.portOwner_ == SmartSessionController::PortOwner::deviceSession && d.scenarioStage_ == DeviceController::ScenarioStage::idle && + !d.deferredShow_ && !d.deferredPtpShow_ && + !d.deferredProfileShow_ && !d.firmwareQuiescenceVerified() && !f.firmware.busy_; if (!safeStage) { s.updateRequested_ = false; diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index 0bf13a391..d8cf26e15 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -1812,6 +1812,13 @@ bool DeviceController::beginFirmwareQuiescence() { firmwareQuiescenceActive_ = true; firmwareQuiescenceSession_ = sessionGeneration_; firmwareQuiescenceBoot_ = identity_.bootId; + // Old PROFILE/SHOW readbacks belong to the pre-update session. Priority + // STOP retires queued ordinary work in DeviceIoWorker; clear its facade + // coalescing flags too, or a stale PTP SHOW can prevent maintenance proof + // while triggering a spurious firmware-safety error toast. + deferredShow_ = false; + deferredProfileShow_ = false; + deferredPtpShow_ = false; // Explicit operator-authorized STOP for BOTH services, independent of // cached telemetry. STOP preempts pending SV START in the serial worker. @@ -1912,8 +1919,14 @@ bool DeviceController::beginPtpStopVerification() { return false; } // Absolute STOP readback deadline; STOP itself uses the worker safety lane. - ptpStartTimer_.start(profileDeploying_ || scenarioStage_ != ScenarioStage::idle - ? 12000 : 5000); + // A verified maintenance STOP may follow a slow legacy PROFILE SHOW + // response and an already-backlogged UART console. The field trace + // showed the STOP ACK arriving ~12 s after dispatch; 5/12 s cut it off. + // Still fail closed on a bounded timer; supervisor owns the 45 s + // whole-stage limit and never flashes without fresh STOP proof. + ptpStartTimer_.start(firmwareQuiescenceActive_ + ? firmwarePtpStopProofDeadlineMs() + : (profileDeploying_ || scenarioStage_ != ScenarioStage::idle ? 12000 : 5000)); return true; } @@ -2370,7 +2383,11 @@ bool DeviceController::sendCommandBatch(const QStringList& commands, const bool } void DeviceController::flushDeferredReadbacks() { - if (m4BoardInspecting_ || m4Deploying_ || m4ScenarioApplying_ || m4ScenarioRetiring_ || profileDeploying_ || binaryProfileStage_ != BinaryProfileStage::idle || + // STOP/PTP STOP and their fresh authoritative readbacks have the sole + // serial proof lane while a firmware handoff is active. Never replay old + // PROFILE SHOW / telemetry from a retired transaction into this window. + if (firmwareQuiescenceActive_ || + m4BoardInspecting_ || m4Deploying_ || m4ScenarioApplying_ || m4ScenarioRetiring_ || profileDeploying_ || binaryProfileStage_ != BinaryProfileStage::idle || scenarioStage_ != ScenarioStage::idle || !connected_ || !ioWorkerReady_ || ioWorker_ == nullptr || sessionGeneration_ == 0) return; diff --git a/apps/arstack_studio/src/DeviceController.hpp b/apps/arstack_studio/src/DeviceController.hpp index 0ad3b8008..7249e05bf 100644 --- a/apps/arstack_studio/src/DeviceController.hpp +++ b/apps/arstack_studio/src/DeviceController.hpp @@ -317,7 +317,13 @@ class DeviceController : public QObject { // serial owner: the existing STOP/PTP STOP/readback paths remain authoritative. bool beginFirmwareQuiescence(); void endFirmwareQuiescence() noexcept; + [[nodiscard]] bool firmwareQuiescenceActive() const noexcept { + return firmwareQuiescenceActive_; + } [[nodiscard]] bool firmwareQuiescenceVerified() const noexcept; + // P1.3: bounded maintenance proof for slow legacy console responses. + // Normal operator PTP STOP still retains its independent 5 s deadline. + [[nodiscard]] static constexpr int firmwarePtpStopProofDeadlineMs() noexcept { return 40000; } bool configurePtp(const QVariantMap& profile); bool setPtpRole(const QString& requestedRole) { diff --git a/apps/arstack_studio/src/SmartSessionController.hpp b/apps/arstack_studio/src/SmartSessionController.hpp index 12a351f17..8d6a069a3 100644 --- a/apps/arstack_studio/src/SmartSessionController.hpp +++ b/apps/arstack_studio/src/SmartSessionController.hpp @@ -143,6 +143,11 @@ class SmartSessionController : public QObject { const QString& observed) noexcept; [[nodiscard]] static constexpr int firmwareStopAckTimeoutMs() noexcept { return 3500; } + // Legacy board console can take >12 s to drain an older PROFILE SHOW + // before delivering STOP ACK, PTP STOP and fresh SHOW results. This is + // a bounded, operator-authorized maintenance proof stage, never a + // waiver of ACK/readback. Global firmware transaction cap stays 300 s. + [[nodiscard]] static constexpr int firmwareVerifiedStopDeadlineMs() noexcept { return 45000; } [[nodiscard]] static constexpr int firmwareReleaseAckTimeoutMs() noexcept { return 2000; } [[nodiscard]] static constexpr int firmwareTotalDeadlineMs() noexcept { return 300000; } @@ -267,7 +272,8 @@ class SmartSessionController : public QObject { } [[nodiscard]] int timeoutForStage(const UpdateStage stage) const noexcept { return stage == UpdateStage::stopping - ? (owner_ != nullptr && owner_->stoppingPtpForFirmware_ ? 12000 : firmwareStopAckTimeoutMs()) + ? (owner_ != nullptr && owner_->stoppingPtpForFirmware_ + ? firmwareVerifiedStopDeadlineMs() : firmwareStopAckTimeoutMs()) : firmwareReleaseAckTimeoutMs(); } void synchronize() { diff --git a/apps/arstack_studio/src/StudioDeviceController.hpp b/apps/arstack_studio/src/StudioDeviceController.hpp index 29c809d91..049ad7fd2 100644 --- a/apps/arstack_studio/src/StudioDeviceController.hpp +++ b/apps/arstack_studio/src/StudioDeviceController.hpp @@ -243,6 +243,24 @@ class StudioDeviceController : public DeviceController { return; } + // Firmware STOP is an explicitly authorized exclusive safety phase. + // Field evidence: ordinary SHOW health missed twice and disconnected + // COM four seconds after STOP, but the firmware finally acknowledged + // STOP about twelve seconds later. While maintenance STOP owns the + // serial proof lane, ordinary SHOW probes MUST NOT retire the port. + // Clear a pre-existing outstanding probe as it belongs to the old + // transaction; keep the 700 ms heartbeat and physical USB/OS error + // monitoring active in DeviceIoWorker. Normal health resumes after + // endFirmwareQuiescence(), without carrying stale misses. + if (firmwareQuiescenceActive()) { + healthProbeOutstanding_ = false; + if (missedHealthReplies_ != 0) { + missedHealthReplies_ = 0; + emit controlHealthChanged(); + } + return; + } + // Canonical BIN and CFG scenario transactions both own the serial // console. Suppress only probe accounting during known exclusivity, // never the worker's independent safety heartbeat/STOP. Resume From 2f3b1bd0a8b9648c13055455410be55fded0e550 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 03:13:59 +0700 Subject: [PATCH 2/4] fix(firmware): require fresh explicit blank-board consent for unverified ROM retry A failed STOP may retire application identity. Do not let Retry on a remembered COM bypass supervised STOP by entering the blank-board ROM path. Bind that exception to the confirmed beginFirmwareInstall entry only, fail closed with re-identification guidance, and lock it with S8 negative regression. --- .../src/DeterministicSessionHarness.cpp | 16 +++++++++++++ .../src/SmartSessionController.cpp | 23 +++++++++++++++++-- .../src/SmartSessionController.hpp | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index 75e73b2da..571b956ec 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -44,6 +44,7 @@ class DeterministicSessionHarness final { {"dual-revision catalog selects only qualified silicon", revisionCatalogFailClosed()}, {"firmware timeout retains port until flasher exits", firmwarePortReleaseWaitsForWorker()}, {"firmware update waits for fresh SV and PTP STOP readback", firmwareStopProofPreventsEarlyFlash()}, + {"retry after unverified STOP cannot probe ROM without renewed consent", unverifiedFirmwareRetryNeverProbesRom()}, {"slow firmware STOP suspends ordinary health teardown, resumes afterward", maintenanceStopDoesNotTriggerHealthDisconnect()}, {"real timed-out firmware subprocess retires before COM release", firmwareWorkerTimeoutAwaitsProcessExit()}, {"current identity -> READY without recovery", currentIdentityNeverRecovers()}, @@ -217,6 +218,21 @@ class DeterministicSessionHarness final { } + static bool unverifiedFirmwareRetryNeverProbesRom() { + Fixture f; + if (!f.profileReady) return false; + auto& s = f.session; + s.updatePort_ = QStringLiteral("COM7"); + s.updateExpectedDeviceId_ = QStringLiteral("A1B2C3D4E5F6"); + s.setupError_ = true; + const bool accepted = s.retryFirmwareUpdate(); + return !accepted && !s.updateRequested_ && + s.portOwner_ != SmartSessionController::PortOwner::firmwareTool && + !f.firmware.busy_ && !f.firmware.targetVerified_ && + s.lastFirmwareUpdateResult_.contains(QStringLiteral("identity was lost"), + Qt::CaseInsensitive); + } + static bool maintenanceStopDoesNotTriggerHealthDisconnect() { Fixture f; if (!f.profileReady) return false; diff --git a/apps/arstack_studio/src/SmartSessionController.cpp b/apps/arstack_studio/src/SmartSessionController.cpp index ab5d70c48..c30a7ad65 100644 --- a/apps/arstack_studio/src/SmartSessionController.cpp +++ b/apps/arstack_studio/src/SmartSessionController.cpp @@ -612,10 +612,13 @@ bool SmartSessionController::beginFirmwareInstall(const bool supportedBoardAndIs !firmware_->bundleReady() || firmware_->busy() || blankBoardPort_.isEmpty()) { return false; } - return beginFirmwareOperation(blankBoardPort_); + // ROM-only recovery is authorized exclusively from this confirmed entry. + // An ordinary Retry after lost application identity must never inherit it. + return beginFirmwareOperation(blankBoardPort_, true); } -bool SmartSessionController::beginFirmwareOperation(const QString& portName) { +bool SmartSessionController::beginFirmwareOperation( + const QString& portName, const bool authorizedBlankBoardRecovery) { const QString port = portName.trimmed(); if (firmwareProcessRetirementPending_ && firmware_ != nullptr && !firmware_->busy()) { firmwareProcessRetirementPending_ = false; @@ -623,6 +626,22 @@ bool SmartSessionController::beginFirmwareOperation(const QString& portName) { } if (device_ == nullptr || firmware_ == nullptr || port.isEmpty() || firmware_->busy() || firmwareProcessRetirementPending_) return false; + // A lost identity after an interrupted STOP is NOT an authorized blank + // board. Retry must first re-identify the same physical device and + // repeat the supervised STOP, or obtain explicit isolated-ROM recovery + // authorization through beginFirmwareInstall(). Never treat COM15 alone + // as a reason to reset a potentially autonomous publisher. + if (!device_->deviceVerified() && + (!authorizedBlankBoardRecovery || !blankBoardDetected_ || + blankBoardPort_.trimmed().compare(port, Qt::CaseInsensitive) != 0)) { + latchFirmwareFailure(QStringLiteral( + "Board identity was lost during firmware setup. Reconnect and identify " + "the board before retrying; unverified ROM recovery requires explicit " + "supported-board and isolated-bench confirmation. No firmware was written.")); + emit firmwareUpdateFinished(false); + reconcile(); + return false; + } updatePort_ = port; updateExpectedDeviceId_ = device_->deviceVerified() ? device_->deviceId().trimmed() : QString{}; diff --git a/apps/arstack_studio/src/SmartSessionController.hpp b/apps/arstack_studio/src/SmartSessionController.hpp index 8d6a069a3..8b89f9e84 100644 --- a/apps/arstack_studio/src/SmartSessionController.hpp +++ b/apps/arstack_studio/src/SmartSessionController.hpp @@ -390,7 +390,7 @@ class SmartSessionController : public QObject { void refreshRecoveryOfferFromIdentity(); void clearBlankBoardContext(); void latchFirmwareFailure(QString message); - bool beginFirmwareOperation(const QString& portName); + bool beginFirmwareOperation(const QString& portName, bool authorizedBlankBoardRecovery = false); bool ensureDefaultProfile(); void setPresentation(QString state, QString status, bool startReady, bool firmwareUpdateRequired); void refreshFirmwareIdentity(); From c5cf218ec9891a5d927ad5f8774e2db5851702ff Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 03:16:22 +0700 Subject: [PATCH 3/4] fix(P1.3): preserve freshly deferred STOP readbacks through M4 queue retirement Maintenance discards pre-existing readback flags before STOP, but STOP-created safety SHOW/PTP SHOW may still be deferred by M4 exclusive retirement. Drain only those proof readbacks once the serial worker acknowledges retirement, never stale PROFILE SHOW, and add a negative S8 regression ensuring no premature verification. --- .../src/DeterministicSessionHarness.cpp | 32 +++++++++++++++++++ apps/arstack_studio/src/DeviceController.cpp | 25 ++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index 571b956ec..0e660ca98 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -46,6 +46,7 @@ class DeterministicSessionHarness final { {"firmware update waits for fresh SV and PTP STOP readback", firmwareStopProofPreventsEarlyFlash()}, {"retry after unverified STOP cannot probe ROM without renewed consent", unverifiedFirmwareRetryNeverProbesRom()}, {"slow firmware STOP suspends ordinary health teardown, resumes afterward", maintenanceStopDoesNotTriggerHealthDisconnect()}, + {"firmware STOP proof readbacks survive M4 exclusive retirement", maintenanceProofReadbackSurvivesM4Retirement()}, {"real timed-out firmware subprocess retires before COM release", firmwareWorkerTimeoutAwaitsProcessExit()}, {"current identity -> READY without recovery", currentIdentityNeverRecovers()}, {"legacy identity -> firmware update", legacyIdentityOffersUpdate()}, @@ -218,6 +219,37 @@ class DeterministicSessionHarness final { } + static bool maintenanceProofReadbackSurvivesM4Retirement() { + Fixture f; + if (!f.profileReady) return false; + seedVerified(f, identity(), QStringLiteral("COM7"), false); + auto& d = f.device; + d.ioWorkerReady_ = true; + d.firmwareQuiescenceActive_ = true; + d.firmwareQuiescenceSession_ = d.sessionGeneration_; + d.firmwareQuiescenceBoot_ = d.identity_.bootId; + d.firmwareSvStopAck_ = true; + d.ptpStartStage_ = DeviceController::PtpStartStage::awaiting_stop_readback; + d.ptpRunning_ = true; + d.m4ScenarioRetiring_ = true; + d.deferredShow_ = true; + d.deferredPtpShow_ = true; + d.deferredProfileShow_ = true; + d.flushDeferredReadbacks(); + const bool waitForSerialRetirement = + d.deferredShow_ && d.deferredPtpShow_ && + !d.firmwareQuiescenceVerified(); + d.m4ScenarioRetiring_ = false; + d.flushDeferredReadbacks(); + const bool proofsRetried = + !d.deferredShow_ && !d.deferredPtpShow_ && + !d.deferredProfileShow_ && + d.ptpStartStage_ == DeviceController::PtpStartStage::awaiting_stop_readback && + !d.firmwareQuiescenceVerified(); + d.endFirmwareQuiescence(); + return waitForSerialRetirement && proofsRetried; + } + static bool unverifiedFirmwareRetryNeverProbesRom() { Fixture f; if (!f.profileReady) return false; diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index d8cf26e15..6482e86f8 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -2383,14 +2383,29 @@ bool DeviceController::sendCommandBatch(const QStringList& commands, const bool } void DeviceController::flushDeferredReadbacks() { - // STOP/PTP STOP and their fresh authoritative readbacks have the sole - // serial proof lane while a firmware handoff is active. Never replay old - // PROFILE SHOW / telemetry from a retired transaction into this window. - if (firmwareQuiescenceActive_ || - m4BoardInspecting_ || m4Deploying_ || m4ScenarioApplying_ || m4ScenarioRetiring_ || profileDeploying_ || binaryProfileStage_ != BinaryProfileStage::idle || + if (m4BoardInspecting_ || m4Deploying_ || m4ScenarioApplying_ || m4ScenarioRetiring_ || profileDeploying_ || binaryProfileStage_ != BinaryProfileStage::idle || scenarioStage_ != ScenarioStage::idle || !connected_ || !ioWorkerReady_ || ioWorker_ == nullptr || sessionGeneration_ == 0) return; + if (firmwareQuiescenceActive_) { + // The queued STOP-proof SHOW/PTP SHOW were created *after* their + // respective current-boot ACKs: entry already discarded all old + // deferred reads. During exclusive M4 retirement, preserve these + // two proof reads until that serial lease is actually retired. + // Never replay unrelated PROFILE SHOW into firmware maintenance. + deferredProfileShow_ = false; + if (std::exchange(deferredPtpShow_, false) && + ptpStartStage_ == PtpStartStage::awaiting_stop_readback && + !sendQuietCommand(QStringLiteral("PTP SHOW"))) { + markPtpStopUnconfirmed(QStringLiteral( + "Firmware PTP STOP proof readback could not be queued.")); + } + if (std::exchange(deferredShow_, false) && firmwareSvStopAck_ && + !sendQuietCommand(QStringLiteral("SHOW"))) { + setError(QStringLiteral("Firmware SV STOP proof readback could not be queued.")); + } + return; + } // At most one of each telemetry/readback type. No competing serial writer. if (std::exchange(deferredPtpShow_, false)) { if (!sendQuietCommand(QStringLiteral("PTP SHOW")) && From 9256756234a1e7b2c1cffbe8da97413f22d6a8ba Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 4 Nov 2026 03:20:52 +0700 Subject: [PATCH 4/4] test(studio): update S7 maintenance STOP contract for verified slow-console proof The workspace source-policy gate hardcoded the removed 5 s PTP STOP syntax, causing an early CI failure before compilation. Assert the new bounded maintenance PTP/supervisor deadlines, strict deferred readback behavior and SHOW-health isolation instead; retain all existing QML authority guards. --- .../tests/check_operator_workspace.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index d8b9022c6..34aac1476 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -707,14 +707,26 @@ def main() -> None: # The GUI's normal transient conditions are never shown as operator faults. # Validate both intended (positive) paths and removed (negative) paths. + # Normal STOP remains bounded; the authorized firmware-maintenance STOP + # has its own bounded, longer proof window. Importantly, normal SHOW-health + # polling must not retire the COM while STOP ACK/readback is still pending. + # Preserve the original exclusivity checks and assert the replacement + # behavioral contract instead of matching the obsolete 5 s timer syntax. + maintenance_controller = (STUDIO / "src/StudioDeviceController.hpp").read_text(encoding="utf-8") require("Coalesced %1 until exclusive serial authority is released." in device_cpp and "flushDeferredReadbacks()" in device_cpp and "std::exchange(deferredPtpShow_, false)" in device_cpp and "if (readback) return deferReadback();" in device_cpp and "Canonical binary profile transaction owns" not in device_cpp and "Could not request PTP STOP readback." not in device_cpp - and "ptpStartTimer_.start(profileDeploying_" in device_cpp, - "busy device polling must defer quietly; verified STOP has an absolute deadline") + and "ptpStartTimer_.start(firmwareQuiescenceActive_" in device_cpp + and "firmwarePtpStopProofDeadlineMs()" in device_cpp + and "firmwareVerifiedStopDeadlineMs()" in session_h + and "if (firmwareQuiescenceActive()) {" in maintenance_controller + and "healthProbeOutstanding_ = false;" in maintenance_controller + and "deferredProfileShow_ = false;" in device_cpp, + "busy device polling must defer quietly; maintenance STOP requires" + " bounded fresh ACK/readback and no premature COM retirement") require('toolTipText: "Time sync & SV lock details"' in timing_ui and 'Accessible.name: "Time sync & SV lock details"' in timing_ui and 'objectName: "studioTimingDetails"' in timing_ui