feat(sync): add --force-missing-dependencies to import command#550
feat(sync): add --force-missing-dependencies to import command#550michael-richey wants to merge 11 commits into
Conversation
|
Review notes from my pass: P2: import dependency discovery is still affected by destination state
Suggested fix: add an import-specific dependency extractor that compares referenced IDs only against def _missing_source_dependencies_for_resource(self, resource_type: str, _id: str) -> set[tuple[str, str]]:
missing = set()
resource = deepcopy(self.config.state.source[resource_type][_id])
r_class = self.config.resources[resource_type]
def source_connect_id(key: str, r_obj: dict, dep_type: str) -> list[str]:
values = r_obj[key] if isinstance(r_obj[key], list) else [r_obj[key]]
return [str(value) for value in values if str(value) not in self.config.state.source[dep_type]]
for dep_type, paths in r_class.resource_config.resource_connections.items():
for path in paths:
failed = find_attr(path, dep_type, resource, source_connect_id)
for dep_id in failed or []:
missing.add((dep_type, dep_id))
return missingP2: dependency discovery does not compute closure through already-present source deps
Suggested fix: make discovery a source-state closure walk. Start from the imported def _discover_missing_dependencies(self) -> set[tuple[str, str]]:
missing = set()
seen = set()
queue = [
(resource_type, _id)
for resource_type in self.config.resources_arg
for _id in self.config.state.source[resource_type].keys()
]
while queue:
resource_type, _id = queue.pop()
node = (resource_type, _id)
if node in seen:
continue
seen.add(node)
r_class = self.config.resources[resource_type]
if not r_class.resource_config.resource_connections:
continue
for dep_type, dep_id in self._dependencies_for_source_resource(resource_type, _id):
dep = (dep_type, dep_id)
if dep_id in self.config.state.source[dep_type]:
queue.append(dep)
else:
missing.add(dep)
return missingThe key point is that import-time resolution should be source-state closure logic only. Sync-time |
Allows import to resolve and fetch all transitive dependencies, producing a self-contained local state so sync never calls the source API for missing deps in split import/sync workflows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes two P2 bugs in --force-missing-dependencies import discovery: Bug #1 (destination state leak): _discover_missing_dependencies() was reusing _resource_connections() → connect_id(), which checks config.state.destination first. If a dep existed in stale destination state, it appeared "resolved" and was never checked against source, causing it to be silently skipped even when not imported. Bug #2 (no closure through present deps): Discovery only scanned resources_arg types. Deps already in source state from a prior partial import had their own transitive deps ignored. Example: dashboard_list → dashboard (present) → monitor (missing) — monitor was never found. Fix: Introduce a parallel import-time path: - BaseResource.extract_source_ids(): source-only mirror of connect_id, no destination state check, no mutation. Overridden in Monitors, RestrictionPolicies, TeamMemberships, SyntheticsTests for types that need regex/prefix/type-dispatch logic. - ResourcesHandler._dep_in_source_state(): exact + prefix match for composite synthetics_tests keys ('{public_id}#{monitor_id}'). - ResourcesHandler._source_dependencies_for_resource(): find_attr() traversal using extract_source_ids instead of connect_id. - Rewrite _discover_missing_dependencies() as a BFS closure walk that seeds from resources_arg nodes, follows edges through already-present source resources, and collects only nodes not yet in source state. - Update _import_missing_dep_cb() transitive discovery to use the same source-only path. The sync-time path (_resource_connections, connect_id, get_dependency_graph) is entirely untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P2: synthetics_tests referenceId was emitted under the wrong dep type.
extract_source_ids (and connect_id) internally called super() with
"synthetics_mobile_applications" but _source_dependencies_for_resource
always tagged the result with the outer resource_connections dict key
("synthetics_mobile_applications_versions"). Import would then try to
fetch the application ID from the versions endpoint.
Fix: add "options.mobileApplication.referenceId" as a path under
synthetics_mobile_applications in resource_connections, and split both
connect_id and extract_source_ids into two explicit referenceId branches:
- synthetics_mobile_applications + referenceType=latest → handle
- synthetics_mobile_applications_versions + referenceType=latest → []
(and vice versa for pinned references). Each branch now produces the
correct dep type without needing cross-type redirection.
P3: import_test fixture (module-scoped config) did not save/restore
destination state or force_missing_dependencies. Tests mutating either
leaked state into later tests. Fix uses deepcopy to snapshot both source
and destination state before each test and fully restores all mutated
fields in the teardown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…itor ID discovery
- Replace _dep_in_source_state (bool) with _source_state_key (Optional[str])
so BFS queues the actual dict key (e.g. 'pub#999') rather than the bare
public_id, preventing KeyError in _source_dependencies_for_resource
- Add SLO extract_source_ids override that mirrors connect_id's suffix-match
logic, filtering out monitor_ids that are backed by synthetics_tests to
prevent false ("monitors", id) missing-dep entries
- Update enforcement test allowlist: remove service_level_objectives now that
it has a custom extract_source_ids override
- Rename 4 _dep_in_source_state tests to _source_state_key; add 2 regression
tests covering the KeyError and SLO synthetics false-miss bugs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…source - Replace class-level mutable `versions: List = []` with instance-level `self.versions: Optional[List[Dict]] = None` in __init__ to prevent state leaking across instances - Add _ensure_mobile_versions_loaded(client) lazy loader that fetches SyntheticsMobileApplicationsVersions.get_resources() on first call only; uses None sentinel so orgs with zero versions don't refetch every import - Update get_resources() and import_resource() mobile branch to use the lazy loader, fixing the bug where force-missing-dep imports of mobile tests set mobileApplicationsVersions=[] because get_resources() was never called - Add 2 regression tests: lazy load on direct import, and None-vs-[] sentinel (no double fetch for empty version list) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
32f213a to
6f41167
Compare
Summary
_discover_missing_dependencies()toResourcesHandler: scans imported source state via_resource_connections()and returns the set of(resource_type, id)pairs not yet in source state_import_missing_dep_cb(): async worker callback that imports a single dep by ID from the source API, emits success/skip/failure events, then recursively discovers and enqueues transitive depsimport_resources()to run the discovery + worker loop betweenimport_resources_without_saving()anddump_state(Origin.SOURCE)whenforce_missing_dependencies=True--force-missing-dependenciesinto the integration test helper'simport_resources()methodStacked on: #549
Use case
In split import/sync workflows (import on machine A, sync on machine B), this flag lets
importproduce a fully self-contained local state.synccan then run without any source API access for missing deps.Test plan
pytest tests/unit/test_import_force_missing_deps.py -v— 27 tests, all greenpytest tests/unit/ -v— 363 tests, full regressiontox -e ruff,black— lint clean--force-missing-dependencies, verify transitive dep types appear inresources/source/🤖 Generated with Claude Code