Validate Freeze Export loss patch - #2
Closed
PlainSeer wants to merge 128 commits into
Closed
Conversation
…ngfall2008#4562) The dashboard fetched its favicon and header logo from raw.githubusercontent.com on every page load, so a GitHub outage or 429 rate limit left every page hanging for ~15s before it would render. The logo now ships alongside the app and is served from a local route, with no network dependency.
PlainSeer
force-pushed
the
main
branch
2 times, most recently
from
August 18, 2026 11:31
eff1e29 to
fe63a59
Compare
…nchmark Adds a per-scenario IOG dispatch run on its own salted rng stream, matching how cars/iboost/load-scaling were added: regenerating the existing 20 scenarios reproduces every prior field bit-identical and only adds this one. 13 of the 20 scenarios now carry a run, giving the random suite its first coverage of plan.py's io_adjusted handling in sort_window_by_price_combined - previously no generated scenario ever set io_adjusted, so that code path went unexercised by every benchmark run. Regenerates cases/random_scenarios.yaml and the cases/random_results.json baseline to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three scenarios (9, 10, 15) in the committed baseline didn't match a fresh, independently-verified computation: the same values now reproduce consistently across 6 different PYTHONHASHSEED values, isolated vs full-sequence runs, and independently on CI's ubuntu/gcc build - matching CI's numbers to 4 decimal places despite the different compiler/architecture. The originally-committed baseline was a stale capture; this regenerates it against the verified values. Also addresses review feedback: clarifies the fetch.py/octopus.py docstring reference (was readable as one path) and makes apply_scenario_to_predbat degrade to "no run" via .get() defaults instead of raising KeyError if an "iog" block is enabled but missing run_start_minute/run_slots. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Design for a Sunsynk Connect cloud component modelled on the existing DEYE and Fox integrations: standalone sunsynk.py, pure-Python RSA login (no cryptography dependency), three auth methods, multi-inverter discovery, and 6-slot TOU control via the settings blob. Sunsynk is rebadged DEYE hardware, so the spec adopts an explicit rule for the many undocumented details: assume DEYE for semantics, never for encoding. Register-level behaviour transfers; wire format does not. Nobody on the project has a Sunsynk account, so the build is defensive by design - debug tracing on by default, a standalone CLI for dumping raw traffic, and control gated behind an opt-in until field-verified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine TDD tasks from constants through to the diagnostics CLI and docs, each with a failing test, real implementation code, a run command and a commit. Seven test modules mirror the DEYE suite's coverage. The RSA helper's code and its test vectors were verified before writing: a fixed 1024-bit key parses, round-trips four passwords, rejects an oversize one and randomises its padding. Ends with a verification checklist for the first tester, ordered by cost-if-wrong, since every wire-format detail is inferred rather than documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_init_oauth owns self.auth_method, so passing a collapsed ternary would rewrite password_legacy to password and make the plaintext login unreachable. Pass auth_method straight through, as deye.py does. MockBase is the base object a component is built around, not a mixin. Inheriting it put ComponentBase ahead of it in the MRO, so the CLI would hit an unset self.base. Follow deye.py's harness pattern instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Endpoints, regions, the six-slot TOU field templates, telemetry maps and per-field wire serialisation for the Sunsynk Connect API. Inferred encoding values carry VERIFY@SPIKE; no live hardware has confirmed them.
Sunsynk RSA-encrypts the account password at login. Implemented directly rather than adding a cryptography dependency, which would put a Rust-built wheel on the armv7/armhf add-on targets. Public-key encryption only. Tested against a fixed 1024-bit key: DER parsing, padding structure, round-trip recovery, randomisation, and oversize rejection.
_read_tlv sliced data[offset:offset + length] without checking the declared length fits in the remaining data. Python slicing truncates silently rather than raising, so a truncated DER key "parsed" successfully with a corrupted trailing field. On this key, truncating by 1-3 bytes produced exponent 256, 1 or 0 with no exception; exponent 1 would send the password effectively unencrypted, since rsa_encrypt_pkcs1v15 would compute pow(block, 1, modulus). Added bounds checks covering the fixed-length header, the long-form length bytes, and the final value slice, and extended test_parse_rsa_public_key_rejects_rubbish with near-full-length truncation cases ([:-1], [:-2], [:-3]) that reproduce the corrupted-exponent bug and would have caught it.
…lows
RSA login (default), the pre-2025 plaintext login (opt-in) and the
Predbat.com injected-token path. Transport retries with backoff, detects
Sunsynk's 200-with-failure auth errors from the body, and returns {} on
any failure so callers fail closed.
The RSA path never downgrades to plaintext: that would turn an
externally-triggerable public-key failure into a credential leak.
… retry
_request() looped transport attempts with `for attempt in
range(SUNSYNK_RETRIES)`. When a body-level auth error was detected on the
FINAL attempt, fetch_token() would succeed but the `continue` had nowhere
left to go - the for loop was exhausted, so execution fell through to
`return None` and the successful refresh was thrown away.
Switch to a `while attempt < SUNSYNK_RETRIES` loop where `attempt` only
counts genuine transport failures (non-200 / exception), never the
refresh branch. A refresh now always earns its own retry, regardless of
how much of the transport budget was already spent, while `refreshed`
still caps it to at most one extra pass so total work stays bounded.
Also adds real coverage of _request() itself (transport retries, backoff,
the None/{} success contract, and the auth-refresh path), since every
existing sunsynk test replaced _request() with a fake rather than
exercising it - the gap that let this bug through undetected. The new
regression test was confirmed to fail against the pre-fix code before
the fix was applied.
Pages /inverters with an optional serial filter, polls the four realtime endpoints per inverter, and derives soc_max and battery_rate_max from an amp-hour capacity and a pack voltage inferred from the BMS charge target. Absent fields stay absent rather than publishing an invented zero, and a capacity with no derivable voltage reports nothing rather than a guess.
…ess page Round 1 review found get_device_list could loop forever: a page whose entries never carry sn left the serial count stuck at 0 while a server-reported total stayed above it, and a bogus multi-million total ran millions of calls before terminating even when every page carried real serials. Overlapping pages also duplicated serials unconditionally. Fixes with progress-based termination: dedupe into a set while keeping first-seen order (automatic_config builds arg lists positionally from it), break as soon as a page contributes zero new serials, and add a SUNSYNK_MAX_DISCOVERY_PAGES hard cap as defence in depth against a total that is corrupt or never satisfied, logging a warning if it is hit. Also adds direct coverage for fetch_device_detail's asymmetric write (an omitted or failed re-fetch must never clear a previously known rated power), and documents - without changing - the cell-count rounding assumption in nominal_pack_voltage, which is exact only when the BMS charge target is itself an exact multiple of 3.55V/cell.
Maps Predbat's charge and export windows onto sellTimeN/sellTimeNPac/capN/ timeNon plus a global work mode, applied by read-modify-write so every installer setting Predbat does not own survives verbatim. The global mode follows the window active now, not a static precedence, because Sunsynk has only one. Writes are diff-gated, clamped to the inverter's own SOC floor, and skipped entirely if the read fails.
- build_tou_slots now skips a zero-length window (start == end, compared
normalised) instead of leaving an unterminated action segment - an
enabled charge/export window whose time fields have not yet been set
by update_local_schedule was becoming a multi-hour full-power grid
charge.
- note_settle decodes both sides through encode_setting before comparing,
so a read-back that renders time{n}on as a string ("true"/"1") no
longer looks like a permanent mismatch against a healthy inverter.
- build_settings_payload returns {} when there is no baseline in
device_settings for the serial, and apply_settings treats an empty
payload as "skip" - the guard against an owned-keys-only payload now
lives in the function that builds it, not only in one caller.
- apply_settings gates on an owned-fields-only diff (schedule,
current_soc, now_minutes and the cached battery_reserve_min) BEFORE
any network call, so a no-op tick costs zero GETs and zero POSTs
instead of reading and comparing the whole settings blob every cycle -
which also means an unowned field that changes on every read (e.g. a
server timestamp) can no longer force a write every tick. Only a real
change (or force=True) triggers the read-modify-write, immediately
before the POST.
Also corrected test_payload_clamps_to_the_inverter_soc_floor, which
exercised the hold_charge fallback (reserve, not the requested target)
rather than a genuine sub-floor charge target.
Sensors carry units and are omitted entirely when underivable, so Predbat is never pointed at an entity that never appears. Control entities use HH:MM:SS to match INVERTER_DEF, and the reserve entity deliberately publishes what Predbat wrote rather than the inverter floor, to avoid a write-read-mismatch retry storm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…loads The write-button handler passed force=True, bypassing apply_settings' applied-payload change-detection gate on every routine per-cycle button press (INVERTER_DEF time_button_press), not only when the plan changed. deye.py hit this exact bug first: PR springfall2008#4371 (3e1de75) measured 40 button presses producing 36 byte-identical control orders over two hours on a live site once the button forced the write. Sunsynk's write button now calls apply_schedule(sn) unforced, same as deye.py's fix. Also extends the publish/omit tests to cover the battery_voltage telemetry leaf and the battery_reserve_min rating, which were previously implemented but untested. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One file per tier so each gets an independent storage.age() clock, seeded at startup so the refresh cadence survives a restart. Telemetry is not cached; HA already retains the last published value. The applied-payload cache restores only within a 15-minute window: it asserts the inverter still holds what Predbat wrote, and after a longer outage that assertion is false and the next write must not be skipped. Fixes a call-site bug versus the real Storage.load(module, filename) signature (no `default` kwarg) that would have made restore_state() a silent no-op in production while passing tests against a looser fake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… on a storage failure restore_state() called self.storage.age(...) directly with no try/except, unlike load_cache/save_cache. A raising storage backend or self.storage being None (the normal standalone-CLI state, per mock_base.py) propagated an unhandled exception out of the method. Worse, _cache_restored was set True before any of this ran, so the outer run loop survived the crash but restore_state() then permanently no-op'd on every later call - one transient storage hiccup on first boot silently disabled cache restoration for the life of the process. Adds an age_cache(name) helper alongside load_cache/save_cache that fails soft the same way. Moves the _cache_restored assignment to the end of the method and gates it on a new _restore_had_error flag that load_cache/ age_cache set when they actually catch a storage exception (as opposed to a cache simply not existing yet, which is not an error) - so a failed attempt is retried on a later call once storage recovers, while a successful attempt still sets the guard so restore can never run twice and clobber live state with stale cached state. Also tightens the test double's FakeStorage.load/save signatures to match the real Storage component exactly (no `default` kwarg on load; format/ expiry on save), per the reviewer's note that the previous, more permissive fake was the exact mechanism that had masked the earlier default=None bug. Adds regression tests for a raising storage backend and for storage being None, both confirmed to fail against the pre-fix code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion Registers SunsynkCloud in INVERTER_DEF (identical capability flags to DeyeCloud, because the same registers sit behind both clouds), adds the sunsynk_* apps.yaml schema, and wires the component into COMPONENT_LIST gated on having at least one usable auth path. automatic_config maps an arg only when every discovered inverter reports the underlying value, warning the user to set the rest in apps.yaml. run() explicitly returns True/False rather than falling through to an implicit None on success, which ComponentBase treats identically to a failure - without it the component would never clear its startup "first" flag and would stay in the ever-growing 60s-to-128min backoff forever. apply_settings/apply_schedule gain a settings_fresh flag so a tick where the config tier happens to fire AND a genuine plan change lands reads the settings object once, not twice - the run loop passes it through only when refresh_config actually ran this tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…config, guard its cache save Round-1 review findings on the Task 8 run loop: 1. (CRITICAL) settings_fresh let apply_settings reuse a cached settings baseline instead of reading fresh before a write. The unguarded case was a STALE, non-empty cache: fetch_settings only updates device_settings on a successful read, so a per-serial config-tier read failure left a stale baseline with no signal that it hadn't just refreshed, and restore_state seeds device_settings from the config cache with no age bound at all (unlike the control cache's SUNSYNK_RESTORE_MAX_CONTROL). Traced through run() with two inverters and a failing config read for one of them, this could revert a user's phone-app change and lower the battery's protective floor. Removed entirely - apply_settings/apply_schedule/run() revert to always reading immediately before a write. The saving (~10 GETs/day/inverter, ~2% of daily calls) doesn't pay for weakening a safety invariant on the one call that writes to the battery. 2. test_run_first_cycle_polls_and_publishes discarded run()'s return value, so a regression of the "must return True/False" fix (Task 8's headline catch) would go unnoticed. Now asserts the return value, plus new cases for a login failure and an empty device_list both returning False. 3. refresh_config let fetch_settings overwrite device_settings in place before comparing old vs new, so an external change (e.g. the phone app) was silently absorbed instead of triggering note_external_change - the design spec's named mitigation for the unavoidable read-modify-write race. Fixed by capturing each inverter's previous baseline before the read. 4. refresh_config called save_config() unconditionally, so a tick where every read failed re-stamped a possibly days-stale on-disk cache as fresh; after a restart the config tier would then be skipped for a full TTL while running on stale settings. Mirrors deye.py's got_any pattern. Minor: publish_schedule_settings_ha now runs every tick, not just first (matches deye.py and the spec); the duplicated empty-schedule literal is now one _empty_schedule() helper; automatic_config uses set_arg_auto so an apps.yaml override gets logged once instead of silently discarded. New tests for all of the above were written first and confirmed to fail against the pre-fix code (verified via git stash against the parent commit), then confirmed to pass once each fix landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLI logs in, dumps detail, telemetry and the settings object, and can build a harmless self-use payload and offer to send it. --auth-method lets a tester establish which login their region serves in one command. Nobody on the project has a Sunsynk account, so this is the tool that makes remote verification of the inferred wire format possible. Docs cover the component config table, an inverter-setup walkthrough distinct from the existing local-Modbus SunSynk integration, and the apps.yaml keys - all stressing that control_enable defaults off, password never silently downgrades to password_legacy, writes land via the dongle's next poll, and the phone app and Predbat can race on the single whole-object endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix round 1 on the Sunsynk diagnostics CLI, from review: - input() at the write-test confirmation was unguarded, so a closed or redirected stdin (SSH, CI, a container with no TTY) raised an uncaught EOFError and killed the CLI with a traceback instead of the clean "not sent" it already produced for a typed "n". Now EOFError and KeyboardInterrupt are both caught and treated as "no". This is the only verification tool a remote tester has, so it must survive a non-interactive channel. - _build_sunsynk passed the real args into a second initialize() call after ComponentBase.__init__ already ran it once with all-default args via the constructor - harmless, but it printed a duplicate "SunsynkAPI initialising" pair before the CLI had even reported which region it is using. Now built in one call, matching deye.py's _build_deye. - Removed the dead `client.device_list = [sn]` per-serial assignment in run_cli; none of the methods it calls read device_list, they all take sn directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…view
run() applied a schedule for every inverter on every non-first tick, and seeded
the first tick from an empty schedule. Two consequences, both reproduced:
- With sunsynk_control_enable true, tick 2 posted a full settings object built
from control entities Predbat had never written - sellTime1..6 replaced by the
filler times, cap1..6 forced to 20, peakAndVallery flipped on, the day flags
turned on and sysWorkMode rewritten - wiping the user's own time-of-use
programme on first startup, before there was any plan.
- dashboard_item reaches set_state_wrapper, so publishing the empty first-cycle
seed overwrote Predbat's live plan in the control entities. A restart at 03:00
inside a 02:00-05:00 charge left charge_enable off and a 00:00 window, and the
next tick turned grid charge off mid-window.
Mirror deye.py: read the control entities on every tick including the first, and
only re-apply for inverters whose write button has been pressed (control_active,
until now a vestigial attribute that was never added to or read).
Also, all DEYE-parity gaps from the same review:
- redact() rewrote top-level keys only, but _request traces the whole {code, msg,
success, data} envelope and the login response nests its tokens inside `data`,
so the bearer token was logged verbatim by a debug trace that is on by default
precisely so testers paste it into issue reports. Now recursive over dicts and
lists, as deye.py's is.
- oauth mode never refreshed its injected token: run() now calls
check_and_refresh_oauth_token(), and a body-level auth failure goes through a
reauthenticate() helper that tries handle_oauth_401() before fetch_token().
- refresh_live() now reports whether any telemetry arrived and run() defers
startup when the first poll returns nothing, so automatic_config() - which runs
on the first cycle alone - can no longer lose soc_max and friends for the whole
session. The live tier clock only starts on a successful poll, so the retry
really re-polls.
- refresh_static() keeps the known inverters when discovery comes back empty, and
neither marks the tier fresh nor writes {'device_list': []} to the cache.
- refresh_config, refresh_live, the schedule read and the apply are each wrapped
per serial, so one inverter's failure no longer aborts the tick for the others
or skips publish_data()/update_success_timestamp().
- sysWorkMode now routes through encode_setting like every other owned field;
note_settle's comment corrected (config-tier polls, ~45 minutes, not cycles);
dead cached_values/device_capacity/device_pack_voltage and the unreferenced
TOU_FIELD_PRESERVED/SUNSYNK_PACK_VOLTAGE_FIELD removed.
Tests: the new run()-altitude tests drive run() with control enabled and only the
transport patched - the altitude no previous test reached, which is why the
unplanned write survived nine scoped reviews - and the redaction test now uses a
realistic nested envelope. Every new test was confirmed failing before the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename run_cli to test_sunsynk_api (the house name every other component's diagnostics entry point uses - test_deye_api, test_fox_api, test_solis_api, etc.), add the missing # pragma: no cover on main(), and restructure the default path to call client.run(seconds=0, first=True) instead of the individual fetch_* methods. Nobody on the project has a Sunsynk account, so this CLI is the only tool a remote tester has, and run() is where the real orchestration lives (refresh tiers, the control_active gate, publish_data, automatic_config) - a CLI that bypassed it could not smoke-test what actually runs. run()'s False return (failed login, empty device list, or failed first telemetry) is now reported explicitly instead of falling through into an empty dump. --dump-settings, --write-test, --auth-method, --region and --serial are preserved; --serial now filters discovery itself via inverter_sn, matching deye.py's _build_deye. The rename is safe for the test runner: TEST_REGISTRY in unit_test.py is a manually curated list of explicit `from tests.test_X import ...` imports, not a glob-based collector, so a module-level test_* function inside apps/predbat/sunsynk.py is never auto-discovered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
note_settle hand-rolled its own field list (work mode plus slot time/soc/grid_charge), so a partial apply that dropped a per-slot power limit, solarSell, peakAndVallery or a day flag compared equal on everything checked and silently reset the settle counter, masking a real divergence. Switch to _owned_fields() (minus the echoed sn) so the settle check tracks the same fields the component actually writes. Compare only over keys present in the read-back, and leave the counter untouched when none are: nobody has a Sunsynk account to confirm every owned field is echoed by a real /read, so a missing key means "no information", not "diverged" - treating it as divergence would reintroduce the cry-wolf failure (warning every poll against a healthy inverter) this component was already built to avoid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stage Real user feedback from the Sunsynk diagnostics CLI: a bad-credentials login produced eight unrelated "NoneType has no attribute" warnings (load_cache/ save_cache/age_cache all assume a Storage component exists, but a standalone CLI run has none) and a bare "run() returned False" with three possible causes, forcing the user to dig through Warn: lines for the API's own "Account or password error". - load_cache/save_cache/age_cache now check `self.storage is None` first and return silently - that is the normal standalone-CLI condition, not a fault. A real (raising) storage failure still warns and still retries; only the "no storage configured at all" case goes quiet. Absent storage no longer sets _restore_had_error, so restore_state() completes and marks itself done instead of retrying the same no-op forever. - _request() now captures the API's own failure message on last_api_error (never a credential - the response's `msg` field only), so fetch_token and the CLI can report the real reason. - get_device_list() now tracks discovery_ok, distinguishing a genuinely empty account from a discovery call that failed outright (both used to coerce to the same empty result); refresh_static()'s warning and the CLI both use it. - The CLI's test_sunsynk_api() now inspects component state after a failed run() and names precisely which stage failed - login (with the API's reason), discovery (empty account vs. failed call, with a --serial filtering hint), or the first telemetry poll (naming the serials found). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sunsynk_control_enable now defaults to true, matching solis_control_enable. An inverter component that does not drive the inverter is not what a user configuring it expects, and no other Predbat inverter component gates control behind an extra flag. Set it to false for monitoring only. The write format is still inferred rather than documented, so the docs now recommend running the diagnostics CLI against your own inverter first and switching control off if anything looks wrong, rather than requiring an opt-in to switch it on. Pins the registered defaults in test_component_registered - control_enable decides whether Predbat writes to a real inverter at all, so a silent flip either way should fail the suite. Verified by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR springfall2008#3355 only fixed predbat.temperature (temperature.py); gcoan's follow-up list of 9 more entities with the same gap (fetch.py's battery_temperature, execute.py's 4 "current" power sensors, plan.py's 4 "_best" predicted power sensors) was never actioned. All 9 confirmed still missing device_class on current main. Fixes springfall2008#3352. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PlainSeer
force-pushed
the
main
branch
2 times, most recently
from
August 20, 2026 20:31
7b2c88f to
a323388
Compare
…ining-nan fix(ml): resolve NaN training loss on startup and preserve database zero entries
…ard-device-class-3352 fix(output): set device_class on 9 dashboard sensors missing it
…rge-start-low-power-rate fix(inverter): discharge_start_service sends the planned rate, not always max
…us-power-down-migration feat(octopus): Power Up/Power Down migration - join service and entity regex (points 2 & 3 of springfall2008#4548)
…oc-sentinel-constants refactor(export): name the export_limit freeze/idle sentinels
…live self.intelligent_devices only ever grows: once a device_id is cached it stays forever, even after Octopus stops returning it in devices(). A deregistered/re-paired charger or vehicle then keeps its entities and target-time republished indefinitely and permanently occupies a car slot, which can push num_cars up and misclassify that phantom device's slots against home load. Prune any cached device_id absent from the latest live response in async_update_intelligent_devices. Reported by a customer with a single registered EV who saw two Octopus Intelligent device IDs in Predbat.
…s-prune-stale-intelligent-devices fix(octopus): prune Intelligent devices Octopus no longer returns as live
PlainSeer
force-pushed
the
main
branch
5 times, most recently
from
August 21, 2026 17:02
badf060 to
8aff023
Compare
PlainSeer
force-pushed
the
main
branch
6 times, most recently
from
August 22, 2026 07:11
b3f696a to
842d699
Compare
github-actions
Bot
force-pushed
the
main
branch
from
August 22, 2026 07:11
7bee6cf to
e9eb3aa
Compare
PlainSeer
force-pushed
the
main
branch
2 times, most recently
from
August 22, 2026 07:14
c6db07a to
842d699
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Temporary validation-only PR for commit 657aa6e. This is not intended to merge; it exists only to run the repository's normal pull-request checks against the exact main tree.