Repository navigation
Keep the selected record's syAP off a typed host - #395
Conversation
There was a problem hiding this comment.
Thanks for this. The code is tidy, and the new tests plus the nearby suites pass on this branch (772 passed). I'm requesting changes because the PR doesn't fix the issue it closes, and it adds a hard stop based on an unconfirmed assumption about real devices.
Blocking
1. This doesn't fix #359, so it shouldn't close it.
#359 is about a label: the CLI calls syAP 113 "Time Capsule 3rd generation", but the reporter's unit is an A1409. That label is correct. syAP 113 is the 3rd-gen A1355, and the A1409 is 116 (see the table in discussion #334).
The two-device theory is possible in principle, but in v3.1.0 (the version the reporter ran) it can't produce this report:
- Configure uses the Bonjour syAP only when the device's own ACP read over SSH comes back empty or mismatched;
observed_device_identityprefers the probedcompatibility.exact_syap. A healthy A1409 would answer 116, and that wins whatever discovery says. So the A1409's ACP read would have had to fail during configure, and nothing in the report suggests it did. - Every other command reads syAP from the device itself. The
flashread gets it over network ACP (services/flash.py, "Reading ACP product property syAP..."), and deploy and doctor take their label from the SSH probe's ACP read. The reporter'sflashread worked (acp_match=True). If the unit answered 116, those commands would have said "4th generation", and the reporter would have seen 113 and 116 disagree instead of a steady "3rd generation".
So the unit itself most likely reports 113, which means it's really an A1355 or mislabeled. acp -q syAP and acp -q syAM output from the reporter would settle it. Keeping configure from using a different record's syAP is still reasonable hardening for an edge case, so please describe it that way and change Closes #359 to Refs #359.
2. A syAM/syAP disagreement is now a hard refusal, with no evidence it happens on real hardware.
Before, a mismatch left the probe's identity empty and configure carried on. Now run_configure_flow raises DEVICE_IDENTITY_MISMATCH, with no override. If any real unit's firmware reports a syAM and syAP that disagree, configure stops working on it. Please confirm that real devices never disagree before making this fatal. Telemetry can't settle it, because the mismatch detail was never recorded.
The user also sees the raw parser text ("AirPort identity mismatch: syAM … expects syAP 113, got 116"). And the recovery card for DEVICE_IDENTITY_MISMATCH (app/recovery.py) says the device "did not identify itself as the saved device… Refresh discovery and reconnect" and marks it retryable. That advice is wrong here: a device whose own fields disagree will fail the same way every time.
3. The new fallback test uses a state that can't occur.
test_missing_acp_identity_uses_the_selected_record_not_discovery passes discovered_airport_syap="119" with selected_record_airport_syap="113". In production, ConfigureTargetResolution.selected_record_airport_syap returns either discovered_airport_syap or None, so the two can never differ. The case this PR actually changes isn't tested: a typed host, discovered "119", selected None, and no ACP identity, which should now give identity.syap is None. The existing …ignores_unsupported_syap_from_record_the_host_did_not_come_from doesn't cover it, because its probe still returns an ACP identity.
Non-blocking
4. ConfigureFlowRequest.discovered_airport_syap is now dead weight. Its only remaining use is the new callbacks.debug(discovered_airport_syap=…) line, which duplicates existing logging: the CLI already logs that field (cli/configure.py), and the app logs the whole selected record (app/ops/configure.py). The unsupported-model branch also writes the same debug key with the selected record's value, so one key now means two things. Simpler: drop the field from the request and both callers, then rename observed_device_identity's argument to something like record_airport_syap.
5. Two ProbeResult fields where one would do. airport_identity_detail is only set when airport_identity_mismatch is true. A single airport_identity_mismatch: str | None field holding the diagnostic carries the same information.
6. Configure and deploy now disagree. Deploy and doctor still accept a mismatched device, so a hand-written .env skips the new check. That's fine if configure is meant to be the only gate, but please say so in the PR.
Checked and ruled out
- Stale saved syAP when identity comes back empty: not a risk.
TC_AIRPORT_SYAPis inENV_FILE_OMIT_KEYS, so configure never carries an old value forward. - Effect of dropping the discovery fallback: small. Identity only feeds the CLI printout, the app result and telemetry.
jamesyc
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. This commit takes the earlier suggestions: a syAM/syAP mismatch no longer stops configure, the two ProbeResult fields are now one, discovered_airport_syap is gone from the request, and the fallback test now builds its target through resolve_configure_target. Tests and Ruff pass locally (772 passed), and it merges cleanly with main.
I'm still requesting changes. The main new test doesn't catch the bug it describes, the new log line almost never reaches telemetry, and the PR description no longer matches the code.
Blocking
1. The headline test passes on main's code too, so it guards nothing.
I ran test_typed_host_without_acp_identity_leaves_syap_empty against main's src/ and it passed. The test calls resolve_configure_target, asserts the target's discovered_airport_syap is "119", and then never hands that value to run_configure_flow. It only passes selected_record_airport_syap=None, and an empty fallback gives no syAP on any branch.
The actual fix is the two deleted call-site lines in cli/configure.py and app/ops/configure.py, and no test exercises either call site. A real regression test has to run the CLI's main() with a selected record plus a typed host, and a probe with no ACP identity, then assert on the printed or JSON device_syap. The app path can't reach this case at all; see 4.
2. "Log mismatches instead of failing" logs to almost nowhere.
callbacks.debug(...) adds a debug field, and debug fields reach telemetry only inside a failure's error text (OperationContext.build_error, failure_debug). Configure now continues on a mismatch, so a successful run drops the diagnostic. It shows up only if configure later fails for some unrelated reason. The probe summary in that error text doesn't include the new field either.
That leaves a parser flag, a new ProbeResult field, a probe test and a configure branch, all for one line that is usually thrown away. Either send it through a channel that goes out on success, or delete all of it. callbacks.measurement(...) lands in execution on every finish, as locate.py's host_follow does. The case has never been seen on a real device, so deleting it is a fair choice.
3. The PR description is stale and still closes #359.
- It still says configure "raises a device identity mismatch and stops", which this commit reverses.
- It still says "the discovery syAP is logged for telemetry", which this commit deletes.
- Its Validation section still describes the first commit's tests.
Closes #359is unchanged, although point 1 of the earlier review asked forRefs #359. If you disagree with that point, please say why.- The opening claim that a mismatched device "is still configured from that advertisement" overstates things.
TC_AIRPORT_SYAPis inENV_FILE_OMIT_KEYS, andrender_env_textnever writes it, so nothing gets configured from it. The new test'sassertNotIn("TC_AIRPORT_SYAP", result.values)checks a dict that never reaches disk.
Non-blocking, but it changes whether the PR is worth merging
4. The bug is now very hard to reach, so please restate the case for it after rebasing on main.
- CLI only. The macOS app sends either
selected_recordorhost, never both (OperationParams.Configure.save). In the app, a typed host never comes with a record, which leaves only the CLI case where the user picks one record at the prompt and types a different host. - Usually stopped earlier on main now. Since this branch was cut, main gained a check that fails configure with
ACPD_QUITwhen ACPd has quit (services/configure.py, right afterobserved_device_identity). That is the realistic way to get an authenticated probe with no ACP identity, and it now fails before identity matters. - What's left: a transient
acp -qfailure while ACPd is still running, or a syAM/syAP mismatch that has never been observed. - What it costs when it happens: a wrong label in one configure printout and in that run's
device_syaptelemetry. Nothing reaches the.env, deploy, doctor or flash.
GitHub reports it as mergeable, but no CI has run on this PR. Please rebase and get CI green rather than rely on a textual merge.
5. Leftover naming. syap_source="discovered" now means "from the selected record". The CLI prints it as the value's source, so it should be "selected_record" or "record".
Bottom line
After this commit, the code change amounts to deleting two call-site kwargs, plus a diagnostic that rarely leaves the Mac. That's a fine small cleanup if it comes with:
- a test that drives the CLI path,
- the mismatch diagnostic either sent as a measurement or removed,
- an accurate description that says
Refs #359.
fc4d4dd to
2e28456
Compare
|
Thanks for the detailed review. 7854697 covers points 2 through 5:
|
7854697 to
1faca20
Compare
|
Thanks for the second pass. 1faca20 is rebased on current main and replaces the earlier commits:
|
jamesyc
left a comment
There was a problem hiding this comment.
Thanks, this revision resolves every blocking point from the last round:
- The test now catches the bug. I ran
test_configure_typed_host_without_acp_identity_does_not_take_selected_record_syapagainst main'ssrc/: it fails, printingUsing discovered TC_AIRPORT_SYAP: 119, and it passes on this branch. - The mismatch plumbing is gone: no probe field, no parser flag and no configure branch.
- The description now matches the code, scoped as CLI-only hardening behind main's
ACPD_QUITcheck, withRefs #359. - It's rebased onto current main as one commit, CI is green, and the related suites pass locally (799 passed).
The code change itself is right. Accepting the record's own host still clears explicit_host in cli/configure.py, so the correct fallback still works for the normal pick-and-accept flow. What's left is test coverage and the PR title. I'll approve once 1–3 are in.
Should fix before merge
1. The renamed source and its new message have no test, and a mistake there would print nothing.
This PR changes the source label from "discovered" to "selected_record" and adds the line "Using TC_AIRPORT_SYAP from the selected device". No test asserts either one. No test checks syap_source at all, and the service test that passes selected_record_airport_syap="119" only checks the written value. print_automatic_value_choice has no else branch, so a misspelled source, or a producer still sending "discovered", would print nothing while every test still passes.
Please add the positive case: pick the record, accept its default host, probe with no ACP identity, then assert Using TC_AIRPORT_SYAP from the selected device: 119 in the output and TC_AIRPORT_SYAP == "119" in result.values. Next to the new negative test, that covers the fallback from both sides.
2. test_run_configure_flow_ignores_unsupported_syap_from_record_the_host_did_not_come_from no longer tests what its name says.
With discovered_airport_syap="115" gone, there's no unsupported syAP left in its input. It now just passes selected_record_airport_syap=None and checks the probed 119, which is the same as the None subtest of test_run_configure_flow_continues_for_supported_or_unusable_selected_record_syap right above it. The behavior it was written for is already covered at the resolver level: test_configure_target.py asserts that a typed host over a syAP-115 record gives selected_record_airport_syap is None. Please delete it, or rename it to match what it now checks.
3. The PR title still describes the first revision.
"Preserve target identity during Time Capsule configuration" overstates a change that only stops one fallback. This will be squashed and the title usually becomes the commit message, so please retitle it to the commit's own headline: "Keep the selected record's syAP off a typed host".
Nits
- The new call-site comment
# selected_record_airport_syap is empty unless the host came from that record.repeats the field comment a few lines up inConfigureFlowRequest. One of them is enough. - Pre-existing, but confusing next to the new test:
test_configure_uses_discovered_airport_syap_without_promptinguses the same record and typed host as the new test, yet assertsUsing probed TC_AIRPORT_SYAP: 119. Its value comes from the probe after SSH is enabled, not from discovery. Renaming it to say "probed" would make the two tests easier to read side by side.
|
Thanks. 872cf6c covers points 1 and 2 and both nits:
|
The identity fallback used the discovered record's syAP even when the user typed a different host, so a probe with no ACP identity reported that record's syAP for another device. Use selected_record_airport_syap, which is set only when the host came from the record, and drop discovered_airport_syap from ConfigureFlowRequest and both callers. Rename the fallback source to selected_record, printed as "from the selected device", and test the fallback from both sides in the CLI: a kept record host takes the record's syAP, a typed host does not. Refs jamesyc#359
872cf6c to
3437f23
Compare
jamesyc
left a comment
There was a problem hiding this comment.
Thanks for working through the review rounds. I rebased this onto main as one commit and matched the new test's style to its neighbors. The fallback is now tested from both sides, and CI is green.
b0d891b to
09e1f2c
Compare
43de98f to
9611b78
Compare
Why
In the CLI, a user can pick one discovered record at the prompt and then type a different host. If the authenticated probe of that host returns no ACP identity, configure fell back to the selected record's syAP and printed it as the device's syAP, even though that record describes another device. The result is a wrong label in that configure printout and in that run's
device_syaptelemetry. Nothing reaches the.env, deploy, doctor or flash, sinceTC_AIRPORT_SYAPis inENV_FILE_OMIT_KEYS.This is hardening for that CLI-only edge case. The macOS app sends either a selected record or a host, never both, so it cannot reach it. On current main, an ACPd that has quit already fails configure with
ACPD_QUITbefore identity matters, so what is left is a transientacp -qfailure while ACPd is still running.Summary
ConfigureTargetResolution.selected_recordnow holds the record only when the host came from it, and is empty for a typed or saved host. Thetarget_recordproperty is gone, so no caller can pick up a record for another device by mistake. Both callers pass this field asConfigureFlowRequest.selected_record, the only place configure learns about the record.run_configure_flowalready used it for the ACP record and the MAC pin; it now also reads the record's syAP from it, for the unsupported-model check and for the identity fallback.discovered_airport_syapandselected_record_airport_syapare removed fromConfigureFlowRequestandConfigureTargetResolution, and from both callers (cli/configure.py,app/ops/configure.py).observed_device_identity's argument is renamed torecord_airport_syap. Its fallback source is nowselected_recordinstead ofdiscovered, and the source is typed asSyapSource = Literal["probed", "selected_record"]. The CLI prints it as "Using TC_AIRPORT_SYAP from the selected device". The single-useConfigureValueChoicewrapper and itsprint_automatic_value_choicebranches for sources nothing produces are replaced byprint_syap_choice.device_syap_sourcenext todevice_syap, in the CLI and the app, so a report can tell the device's own syAP from the record's stand-in.discovered_airport_syapdebug field is dropped: both the one the CLI added whenever a record was discovered and the one on the unsupported-device failure. Telemetry keeps the value: theselected_bonjour_recorddebug field already carries the record's syAP, and the unsupported-device message names it.Validation
test_configure_record_syap_stands_in_only_for_the_records_own_hostdrives the CLImain(): it selects a record with syAP 119 and probes a device with no ACP identity, in two subtests.TC_AIRPORT_SYAPis 119, the output says "Using TC_AIRPORT_SYAP from the selected device: 119", and telemetry'sfinishgetsdevice_syap119,device_syap_sourceselected_recordanddevice_modelTimeCapsule8,119.root@10.0.0.2: noTC_AIRPORT_SYAPin the output, the values orfinish, and nodevice_syap_sourceordevice_model.Both subtests fail on main's
src/, which prints "Using discovered TC_AIRPORT_SYAP: 119" in both cases, and pass with this change.test_configure_telemetry_says_where_the_syap_came_fromruns the app's configure twice: with a host whose probe reads syAP 119,finishgetsdevice_syap_sourceprobed; with a selected record advertising 119 and a probe with no ACP identity, it getsselected_record.test_configure_typed_host_over_record_enables_ssh_without_the_record(renamed fromtest_configure_uses_discovered_airport_syap_without_prompting): a typed host with SSH closed. ACP gets no record, and the reprobe's syAP 119 is reported over the record's 113. This guards behavior main already had.test_configure_probed_syap_beats_record_and_invalid_existing_syap(renamed fromtest_configure_discovered_syap_beats_invalid_existing_syap): the reprobe's 119 wins over the record's 113 and a saved 999, and the output says "Using probed TC_AIRPORT_SYAP: 119".The resolver tests assert the resolution carries no record for a typed or saved host. The service and target tests now pass a record (
record_advertising_syap) instead of syAP fields. A selected record advertising syAP 115 is still refused before SSH or ACP, and the app's error debug still carries the record's syAP 115.test_run_configure_flow_ignores_unsupported_syap_from_record_the_host_did_not_come_fromis deleted: the resolver test covers it, since a typed host gets no record.Merged with main
5ab32a82: the configure, configure target, configure defaults and app API test files pass, andruff checkpasses. At the head commit, the test files that import a changed module (app API, CLI configure, configure service, configure target, configure defaults, operation context, telemetry, Bonjour integration and labels) pass: 887 passed.Refs #359