Skip to content

Add copy_network, merge_networks, merge_scopes (closes #221)#507

Open
dotsdl wants to merge 36 commits into
mainfrom
an_merge
Open

Add copy_network, merge_networks, merge_scopes (closes #221)#507
dotsdl wants to merge 36 commits into
mainfrom
an_merge

Conversation

@dotsdl

@dotsdl dotsdl commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Revives the work originally proposed in #433 (now unreachable since the contributing fork was deleted) and finishes it.

  • Adds Neo4jStore.merge_networks for combining multiple existing AlchemicalNetwork\s into a new AlchemicalNetwork in a target Scope, with completed and errored Task\s (and their ProtocolDAGResultRef\s) cloned into the new scope so previously-computed results do not need to be re-run. (Originally authored by @ianmkenney; preserved here as-is, then refactored — see commits.)
  • Adds the user-facing surface:
    • POST /networks/merge on AlchemiscaleAPI, which validates the destination Scope and each source network's Scope against the caller's token before delegating to the store.
    • AlchemiscaleClient.merge_networks(networks, name, scope), with client-side guards for wildcard scopes, empty input, and non-AlchemicalNetwork ScopedKey\s.
  • Refactors the database-layer implementation to use KeyedChain.decode_subchains (gufe ≥1.8.0), which replaces a hand-rolled kc_to_gufe helper plus manual chain-slicing loop, and lifts the inline TransformationData dataclass to a private module-level _TransformationData for readability.
  • Bumps the gufe pin to >=1.8.0 in devtools/conda-envs/test.yml and devtools/conda-envs/alchemiscale-server.yml (the only env files that actually exercise this code path).
  • Adds integration tests at both the API and client layers covering: happy-path edge union, scope-based authorization (bad destination and bad source), and full preservation of complete/error Task\s with their ProtocolDAGResultRef\s — including a reachability check via get_network_tasks(merged_sk).

Closes #221.

Notes for reviewers

  • The store-level merge_networks body is unchanged in behavior from the original PR; the diff against Allow server-side copying and merging of AlchemicalNetworks #433's last revision is the decode_subchains refactor plus the lift of TransformationData.
  • The get_network_tasks assertion in test_merge_networks_preserves_tasks_and_results exercises the standard (:AlchemicalNetwork)-[:DEPENDS_ON]->(:Transformation)<-[:PERFORMS]-(:Task) traversal on the merged network. If it fails, that indicates a missing PERFORMS edge in _TransformationData.to_subgraph — flagged here for follow-up rather than fixed in this PR, since the original implementation has the same behavior.
  • subchain_cache is preserved with cleaner contents (KeyedChain.from_gufe(transformation)) but its necessity is an open question tied to the PERFORMS follow-up above.

Test plan

  • pytest -n auto -v alchemiscale/tests/integration/storage/test_statestore.py -k merge_networks
  • pytest -n auto -v alchemiscale/tests/integration/interface/test_api.py -k merge_networks
  • pytest -n auto -v alchemiscale/tests/integration/interface/client/test_client.py -k merge_networks
  • black --check --diff alchemiscale
  • mypy alchemiscale
  • Confirm whether test_merge_networks_preserves_tasks_and_results reveals the suspected missing PERFORMS wiring; open a follow-up if so.

🤖 Generated with Claude Code

ianmkenney and others added 23 commits September 16, 2025 11:57
Networks are merged into a new network under a new scope with a given
name. Task and ProtocolDAGResultRef support is not implemented here.
There is hardly any difference in performance.
Add the client + API surface for the merge_networks database operation
introduced in this branch. Users can now combine multiple existing
AlchemicalNetworks into a single new AlchemicalNetwork through
AlchemiscaleClient, with completed and errored Tasks (and their
ProtocolDAGResultRefs) carried over so previously-computed results do
not need to be re-run.

- POST /networks/merge endpoint validates the destination Scope and
  each source network's Scope against the caller's token, then defers
  to Neo4jStore.merge_networks.
- AlchemiscaleClient.merge_networks wraps the endpoint and guards
  against wildcard Scopes, empty input, and non-AlchemicalNetwork
  ScopedKeys client-side.
- Integration tests cover the happy path, scope authorization (both
  bad destination and bad source), and that Tasks + PDRRs in
  complete/error state are cloned into the new scope and reachable
  through get_network_tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the inline kc_to_gufe helper and manual chain-slicing loop with
a call to KeyedChain.decode_subchains, paired with a list comprehension
that captures the original database GufeKeys in the same order. The
decode_subchains helper (gufe >=1.8.0) reuses a shared tokenizable_map
across yields, so common dependencies in a source network are decoded
only once.

Lift the previously nested TransformationData dataclass to a private
module-level _TransformationData so merge_networks reads top-to-bottom
without chasing an inline class definition, and so the helper can be
referenced by future tests in isolation.

The dropped key_decode_dependencies import goes with kc_to_gufe.

subchain_cache is preserved with the same shape but now populated via
KeyedChain.from_gufe(transformation); whether the cache (and the
unconnected tf_node it feeds) is needed at all is a separate question
to revisit once the PERFORMS-wiring question is settled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
merge_networks now uses KeyedChain.decode_subchains, which was added
in gufe 1.8.0. Bump the pin in:

- devtools/conda-envs/test.yml (was =1.7.1) so the integration tests
  exercise the new code path.
- devtools/conda-envs/alchemiscale-server.yml (was =1.6.1) so server
  deployments running merge_networks pick up the required gufe API.

Client and compute env files are left alone since neither imports the
new code path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review revealed that cloned Tasks on the merged network were
written but never wired back to their Transformations, leaving every
PERFORMS-based traversal (get_network_tasks, get_task_transformation,
set_tasks_status, etc.) blind to them. The new
test_merge_networks_preserves_tasks_and_results check via
get_network_tasks asserts this exact thing and would have failed in
CI.

Changes
-------

- Add the PERFORMS edge in _TransformationData.to_subgraph so cloned
  Tasks are reachable from the standard
  (:AlchemicalNetwork)-[:DEPENDS_ON]->(:Transformation)<-[:PERFORMS]-(:Task)
  traversal. The pre-existing tf_node construction is now meaningful,
  resolving the open question about subchain_cache.

- Add a `state` parameter to Neo4jStore.merge_networks,
  /networks/merge, and AlchemiscaleClient.merge_networks, bringing
  merge_networks to feature parity with assemble_network /
  create_network. Defaults to NetworkStateEnum.active.

- Document the "cloned Tasks are not actioned to the merged TaskHub"
  semantics in the store-level and client docstrings, including the
  remediation path (call action_tasks after merging).

- Replace the O(N^2) `transformation_data.index(transformation)`
  dedup with a `dict[GufeKey, _TransformationData]` keyed on
  transformation.key, avoiding repeated GufeTokenizable equality
  checks on large networks.

- Improve the missing-ScopedKey error to collect and report all
  missing source SKs in a single ValueError, rather than only the
  one that first triggered KeyError.

- Add a chain-order contract comment at the
  zip(database_keys, transformations) site pointing at the
  decode_subchains yield-order guarantee.

- Make /networks/merge an `async def` endpoint, matching the style
  of the neighboring create_network endpoint.

- Add a parametrized client test (state in {active, inactive}) that
  exercises the new state parameter end-to-end.

- Drop the unused network_tyk2 fixture from the two API-level
  authorization tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dotsdl

dotsdl commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Thanks for the careful review. Pushed d0bf4ec addressing the feedback.

Blocking

  • User story: Send a single simulation to F@H #1 Missing PERFORMS edge — fixed in _TransformationData.to_subgraph. The cloned Task is now wired to tf_node with a PERFORMS relationship in the same iteration that creates it. This also retires the open question about subchain_cache: tf_node is now meaningful as the PERFORMS anchor, so the cache stays.
  • [user story] Retrospective analysis of alchemical decisions #2 Cloned tasks not actioned to the new TaskHub — documented as intentional in both the store-level docstring and the client docstring, with the remediation path spelled out (call action_tasks against the merged network's TaskHub after the merge if you want errored tasks back in the queue). Holding behavior change for a follow-up.

Should-fix

Nits picked up

  • /networks/merge is now async def for consistency with create_network.
  • Improved the KeyError handler to collect all missing source ScopedKeys before raising, instead of reporting only the first failure.
  • Added a one-line comment at the zip(database_keys, transformations) site pointing at decode_subchains's yield-order guarantee.
  • Dropped the unused network_tyk2 fixture from test_merge_networks_bad_scope and test_merge_networks_bad_source_scope.

Deferred

  • update_task_trees staticmethod → module-level helper: skipping for this PR to keep the diff focused. Easy follow-up.
  • Stray f"..." strings without placeholders in test_statestore.py: these are in the original contributor's code; cleaning them up here would muddy the diff. Happy to do it in a follow-up cosmetic PR.

Sanity check the reviewer asked for

Static analysis confirms the PERFORMS edge is now emitted inside the per-task loop with the correct direction (TaskTransformation) and scope properties. Will report back once I've run pytest -n auto alchemiscale/tests/integration -k merge_networks in an env with grolt.

dotsdl and others added 5 commits June 8, 2026 18:07
merge_networks now depends on KeyedChain.decode_subchains (gufe 1.8.0).
Pin all five env files to an exact 1.8.0 release, matching the
repo's convention of pinning gufe exactly rather than with a lower
bound. Touches the test, server, compute, client, and docs envs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feflow 0.2.1 hit conda-forge and pins gufe >=1.9.0,<2 plus openfe
>=1.10.0,<2, which means our previous gufe=1.8.0 + openfe=1.8.0 +
feflow>=0.1.4 combination is unsolvable: feflow 0.2.0 caps gufe at
<1.8, and feflow 0.2.1 forces gufe>=1.9.0.

Bump the trio everywhere to the latest mutually-compatible exact
pins:

- gufe = 1.10.0 (latest stable; satisfies openfe 1.11.1's
  gufe >=1.10.0,<1.11)
- openfe = 1.11.1 (latest stable; satisfies feflow 0.2.1's
  openfe >=1.10.0,<2)
- feflow = 0.2.1 (latest stable)

merge_networks only needs KeyedChain.decode_subchains (gufe >=1.8.0),
so the bump is API-safe.

Apply across test, server, compute, and client env files. docs.yml
gets the gufe bump only (no openfe/feflow there) and gains an
explicit `python >=3.11,<3.13` lower bound because gufe 1.10.0
requires python >=3.11; without the lower bound, ReadTheDocs's
mambaforge-4.10 resolved to a python too old for gufe and the docs
build failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI ran the env solve and tests cleanly with the gufe/openfe/feflow
trio bump (b63474e), revealing two real bugs:

1. The four merge_networks client tests merged into destination
   scopes the test identity does not have access to (e.g.
   `test_org-test_campaign-merged_project`). The test identity only
   has the three scopes in `multiple_scopes` (scope_test plus two
   others); creating a "new project" under the same org/campaign was
   not, in fact, authorized. Switch the destination scope to one the
   identity has:

   - test_merge_networks: scope_test (collision-free because the
     merged network's name differs from pre-loaded networks)
   - test_merge_networks_respects_state: scope_test (each parametrize
     case uses a unique network name)
   - test_merge_networks_preserves_tasks_and_results: multiple_scopes[1]
     (different from where the source Tasks were set up, so the
     per-`_project` Task/PDRR counts stay clean)

2. The docs.yml gufe bump from 1.3.0 to 1.10.0 breaks the RTD build,
   because sphinx 9's autodoc dynamic-importer trips on
   `gufe/settings/models.py`:

       ph: PositiveFloat | None = Field(None, ...)
           ~~~~~~~~~~~~~~^~~~~~
       TypeError: unsupported operand type(s) for |: 'PositiveFloat' and 'NoneType'

   gufe 1.3.0 still allows pydantic v1 (where PositiveFloat is a
   class with `__or__`); gufe >=1.8.0 forces pydantic v2 (where
   PositiveFloat is `Annotated[float, ...]`, no `__or__`). docs.yml
   only needs gufe for intersphinx and type-hint resolution; neither
   benefits from the bump. Revert docs.yml to its pre-PR state.

Also polish the client merge_networks docstring with a `set_tasks_status`
hint alongside `action_tasks` for the errored-Task remediation path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the remaining surface area in #221 (copy_network and merge_scopes)
while fixing a latent bug in merge_networks that the previous CI run
surfaced via test_merge_networks_preserves_tasks_and_results.

Root cause of the PERFORMS bug
------------------------------

In `storage/subgraph.py` `merge_subgraph` (lines 90-108), when two
Python Node objects in the same Subgraph share a primary key, the
UNWIND-MERGE Cypher query returns one Neo4j elementId for both, but
the result-iteration loop assigns that elementId only to the first
Python Node. The duplicate keeps `identity=None`, so any Relationship
referencing it gets `start/end_node.identity = None` and is silently
dropped at commit.

merge_networks hit this directly:

  1. The AlchemicalNetwork's KeyedChain produced one Transformation
     Node per edge in an_subgraph.
  2. `_TransformationData.to_subgraph` produced a *second* Python
     Transformation Node from `subchain_cache` -- same `_scoped_key`,
     different Python object -- and attached PERFORMS to it.
  3. After `an_subgraph |= to_subgraph_result`, merge_subgraph kept
     the AN-chain Node identified but left the to_subgraph Node with
     no identity. Every PERFORMS edge was silently dropped on commit.

Fix: `to_subgraph` now takes the *existing* tf_node from the
surrounding subgraph as a parameter. merge_networks (and the new
copy_network) build a `{gufe_key: node}` lookup from `an_subgraph`
after constructing it, and pass the matching tf_node into each call.
This eliminates the `subchain_cache` entirely, which had been left as
an open question from the earlier review round; `tf_node`'s purpose
finally has teeth as the genuine PERFORMS anchor.

task_statuses knob on _TransformationData.update_task_trees
-----------------------------------------------------------

The previous query hard-filtered `WHERE task.status IN ["complete",
"error"]`. That filter is right for merge_networks ("carry over
results, not in-flight work") but wrong for copy_network, which the
issue specifies as carrying over *all* Tasks. Add an optional
`task_statuses: list[str] | None = None` parameter to drop the WHERE
clause when None. merge_networks passes `["complete", "error"]`
explicitly; copy_network passes None.

copy_network
------------

- `Neo4jStore.copy_network(network_scoped_key, scope, name=None,
  state=NetworkStateEnum.active)`: decodes the source AN, optionally
  renames (preserving the gufe key when name is unchanged), clones
  every Task with its PDRRs and EXTENDS edges, and writes the new
  network + NetworkMark + TaskHub into the target scope. Reuses
  `_TransformationData` machinery verbatim, just with the
  task_statuses filter dropped.
- `POST /networks/{network_scoped_key}/copy` endpoint validates the
  source ScopedKey, source scope, and destination scope against the
  caller's token, then delegates to the store.
- `AlchemiscaleClient.copy_network(network, scope, name=None, state,
  visualize)` mirrors create_network / merge_networks shape.

Cloned Tasks are intentionally **not** actioned to the new network's
TaskHub, matching merge_networks; the client docstrings spell out
the action_tasks remediation path for callers who want errored or
waiting Tasks back in the compute queue.

merge_scopes
------------

- `AlchemiscaleClient.merge_scopes(scopes, target_scope, visualize)`
  is a pure client-side loop: query each source scope for every
  AlchemicalNetwork, then copy_network each into target_scope. No
  new endpoint required.

Tests
-----

- test_copy_network: stages 4 Tasks (complete + ok PDRR, error +
  not-ok PDRR, waiting, running) on three Transformations of a source
  network; copies into a different scope; asserts that all four
  Tasks are reachable via the standard `get_network_tasks` traversal
  with statuses preserved, both PDRRs land in the target scope with
  obj_keys intact, and gufe key is preserved when name is unchanged.
- test_copy_network_with_rename: confirms passing a new name produces
  a distinct gufe key.
- test_copy_network_rejects_wildcard_scope /
  test_copy_network_rejects_non_network_scoped_key: client-side
  guards.
- test_merge_scopes: copies every network in two source scopes into
  a third; asserts per-source ScopedKey count, target-scope
  ScopedKey collapse (preloaded fixtures use the same network names
  across scopes, so copies dedupe), and gufe-key preservation.
- test_merge_scopes_rejects_empty / _rejects_wildcard_target:
  client-side guards.

news/issue-221.rst expanded to list all three new methods plus the
merge_networks-only complete|error filter semantic and the
no-ACTIONS policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dotsdl dotsdl changed the title Add merge_networks (closes #221) Add copy_network, merge_networks, merge_scopes (closes #221) Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.93427% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.42%. Comparing base (0939a1f) to head (c5bc737).

Files with missing lines Patch % Lines
alchemiscale/interface/api.py 44.73% 21 Missing ⚠️
alchemiscale/interface/client.py 70.00% 18 Missing ⚠️
alchemiscale/storage/statestore.py 93.04% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #507      +/-   ##
==========================================
- Coverage   80.54%   80.42%   -0.13%     
==========================================
  Files          31       31              
  Lines        4877     5087     +210     
==========================================
+ Hits         3928     4091     +163     
- Misses        949      996      +47     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dotsdl and others added 2 commits June 26, 2026 18:55
The previous to_subgraph emitted

    Relationship.type("EXTENDS")(etask_node, task_node, ...)

which creates ``(etask)-[:EXTENDS]->(task)``, i.e. "extended_task
extends task" -- the opposite of what the source graph encoded:

    OPTIONAL MATCH (task)-[:EXTENDS]->(extended_task:Task)

and what every other place in the store writes
(``Neo4jStore.create_tasks`` at line 3424:
``Relationship.type("EXTENDS")(task_node, extends_task_node, ...)``)
and reads (lines 2392, 3152, 3547, 3634, 4219, all traversing
``(task)-[:EXTENDS]->(other_task)``).

No existing test exercised the cloned EXTENDS direction (the store-
level test_merge_networks only counted PDRRs, and my own client tests
used independent Tasks with no EXTENDS chain), so the bug shipped
silently as part of the original PR's inline to_subgraph. Swap the
argument order and add a focused regression test:

- test_copy_network_preserves_extends_direction stages a base
  complete Task plus an extending complete Task on the same
  Transformation (using ``create_tasks(transformation_sks,
  extends=base_task_sks)``), copies the source network into a
  different scope, then asserts via raw Cypher that exactly one
  EXTENDS edge exists in the target scope with
  ``extender == extending_task.gufe_key`` and
  ``extended == base_task.gufe_key`` -- i.e. the direction matches
  the source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix landed the correct EXTENDS direction but the
test_copy_network_preserves_extends_direction regression test still
failed (assert 0 == 1) -- the cypher MATCH returned no EXTENDS edges
at all on the clone. Same Node-deduplication trap as the AN-chain
PERFORMS bug, this time intra-to_subgraph.

When ``record[i]["task"]`` is the same Task as
``record[j]["extended_task"]`` (the common case: an extending Task's
target also appears as its own complete/error record), the previous
code created two separate Python ``Node`` objects from
``record_to_node``: one when processing ``record[i]`` (named
``task_node``), one when processing ``record[j]`` (named
``etask_node``). Both have the same ``_scoped_key``. ``merge_subgraph``
assigns a Neo4j ``elementId`` only to the first Python Node it sees
per primary key, so the EXTENDS Relationship's ``end_node.identity``
was ``None`` and the edge was silently dropped on commit.

Memoize Task Nodes by their gufe key inside to_subgraph via
``task_node_cache: dict[str, Node]`` and a small
``get_or_create_task_node`` helper; every reference to the same Task
across iterations now reuses the same Python Node and lands a real
Neo4j identity. The cache is local to a single ``to_subgraph`` call
so it does not leak across Transformations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dotsdl added a commit that referenced this pull request Jul 2, 2026
…#512)

feflow 0.2.1 hit conda-forge and now pins gufe >=1.9.0,<2 and
openfe >=1.10.0,<2. The existing pins (gufe=1.6.1 / openfe=1.6.1 /
feflow=0.1.4 across the deployment envs, and gufe=1.7.1 / openfe=1.8.0
in test) leave callers unable to use feflow 0.2.1 against an
alchemiscale env.

Bump the trio to the latest mutually-compatible exact pins:

  - gufe = 1.10.0   (satisfies openfe 1.11.1's gufe >=1.10.0,<1.11)
  - openfe = 1.11.1 (satisfies feflow 0.2.1's openfe >=1.10.0,<2)
  - feflow = 0.2.1  (latest stable)

Apply across the four runtime env files: test, server, compute,
client. devtools/conda-envs/docs.yml is intentionally unchanged --
the docs build runs sphinx 9's autodoc, which trips on the
`PositiveFloat | None` annotation in gufe >=1.8.0 (works with
pydantic v1 in gufe 1.3.0; pydantic v2 in gufe 1.10.0 makes
PositiveFloat an `Annotated[float, ...]` with no `__or__`). docs.yml
only needs gufe for intersphinx and type-hint resolution, so leaving
it at 1.3.0 keeps the docs build green; addressing the docs gufe
bump is a separate concern from this maintenance bump.

Extracted from PR #507 to unblock other inflight PRs needing the
newer dependency set.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dotsdl and others added 6 commits July 2, 2026 16:31
Neither merge_networks nor copy_network carries over the source
network's execution-orchestration state -- ``Strategy`` (via the
``PROGRESSES`` relationship on the source AN) and
``TaskRestartPattern``\s (via ``ENFORCES`` on the source TaskHub and
``APPLIES`` on individual Tasks). Both create a fresh empty TaskHub
via ``create_taskhub_subgraph`` and stop there.

This is intentional: Strategy and TRPs govern *how* Tasks run
(compute planning, retry policy) rather than the results themselves,
which are what merge_networks and copy_network are designed to
preserve. Merge_networks in particular has no natural resolution
across N sources' Strategies or TRPs; copy_network is skipping them
by design for consistency.

Callers wanting either on the new network should set them explicitly
after the merge/copy via ``AlchemiscaleClient.set_network_strategy``
and ``AlchemiscaleClient.add_task_restart_patterns``.

Adds the policy statement to:

- ``Neo4jStore.merge_networks`` docstring
- ``Neo4jStore.copy_network`` docstring
- ``AlchemiscaleClient.merge_networks`` docstring
- ``AlchemiscaleClient.copy_network`` docstring
- news/issue-221.rst (as an additional bullet under Added)

No code change; no tests added, since the behavior is "does not do
X" and is already exercised implicitly by the existing tests (which
would fail if PROGRESSES/ENFORCES/APPLIES were being silently
duplicated across scopes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Merging and copying AlchemicalNetworks" section to
getting_started.rst covering the three new client methods, their
Task carry-over semantics, and the non-preservation policy for
actioning, Strategy, and TaskRestartPatterns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Explicitly verify that copying an AlchemicalNetwork into a scope where
the same AlchemicalNetwork already exists collapses onto the existing
node rather than creating a duplicate. The additive-attach semantics
this test exercises depend on that dedup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Existing merge_scopes coverage only exercised the pure-dedup case,
where every source AlchemicalNetwork was already preloaded into the
target scope with the same gufe key. The new test adds a third
AlchemicalNetwork to the source scope only, then asserts that after
merge_scopes the target scope holds it as a genuinely new node
(alongside the 2 that dedup onto preexisting entries).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The interface-level test_copy_network* tests all start from
n4js_preloaded, which seeds the same networks (and therefore the same
Transformations) into every scope. Every copy under that fixture is
either pure dedup or pure fresh-write, never a mix.

The new store-level test seeds a target scope with only a *subset* of
the source's Transformations, then copies the full source into it.
Post-copy assertions verify:

  - the target ends up with exactly one AN node for the copied
    ScopedKey;
  - the target's Transformation count equals the source's (overlap
    subset dedups, non-overlap subset is created fresh);
  - every source Transformation appears exactly once in the target;
  - every cloned Task is wired via PERFORMS to a target-scope
    Transformation regardless of which side of the overlap it lives on;
  - EXTENDS chains are preserved across the copy;
  - both source PDRRs land in the target scope with obj_keys intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AlchemiscaleClient.copy_network, .merge_networks, and .merge_scopes methods

2 participants