Repository: spack/spack

Issue:
post-solve `output.satisfies(input)` check returns false positive when input is (partially) concrete
### Steps to reproduce

`spack spec pkg ^/hash` currently fails post-solve even though the solve is correct

The reason is that the input spec contains build dependencies that are omitted in the solver.

Reproducer:

```
$ spack spec --fresh --json cmake > cmake.json
$ spack spec --fresh fmt ^./cmake.json 
==> Error: Internal Spack error: the solver completed but produced specs that do not satisfy the request. Please report a bug at https://github.com/spack/spack/issues
```

The issue is that `spec.json` contains all dependencies of cmake, including `pkgconf`, which is a pure build dep. It's dropped by the solver.


Discussion:
I cannot reproduce this, so I think some bits that trigger the issue are missing? I installed `zlib-ng ^gmake@=4.4` and added:
```diff
diff --git a/var/spack/repos/builtin/packages/hdf5/package.py b/var/spack/repos/builtin/packages/hdf5/package.py
index a7f267d810..8ca927a879 100644
--- a/var/spack/repos/builtin/packages/hdf5/package.py
+++ b/var/spack/repos/builtin/packages/hdf5/package.py
@@ -114,6 +114,8 @@ class Hdf5(CMakePackage):
     depends_on("szip", when="+szip")
     depends_on("zlib-api")
 
+    depends_on("gmake@4.4.1:")
+
     # The compiler wrappers (h5cc, h5fc, etc.) run 'pkg-config'.
     # Skip this on Windows since pkgconfig is autotools
     for plat in ["cray", "darwin", "linux"]:
```
What I get is:
![Screenshot from 2024-03-19 13-40-36](https://github.com/spack/spack/assets/4199709/aa5178bd-9294-4043-a205-892f54dcc30a)



See updated top level comment.
Currently the case above fails because this assumption:

https://github.com/spack/spack/blob/e37870ff43514e79d7b520f930270bc41fc52e13/lib/spack/spack/spec.py#L3901-L3903 

is wrong, in the sense  that we might have a concrete dependency on the rhs.
This might be enough to fix the concretization:
```diff
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 7d8266400e..a06e761fcd 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -3910,6 +3910,9 @@ def satisfies(self, other: Union[str, "Spec"], deps: bool = True) -> bool:
             if not rhs_edge.virtuals:
                 continue
 
+            if rhs_edge.parent.concrete:
+                continue
+
             if not lhs_edges:
                 # Construct a map of the link/run subDAG + direct "build" edges,
                 # keyed by dependency name

```
Now I need to come up with a regression test.
Would be nice not to descend into the right hand side for concrete subbranches

Resolving patch:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -3898,9 +3898,13 @@ def satisfies(self, other: Union[str, "Spec"], deps: bool = True) -> bool:
         if not self._dependencies:
             return False
 
-        # If we arrived here, then rhs is abstract. At the moment we don't care about the edge
-        # structure of an abstract DAG, so we check if any edge could satisfy the properties
-        # we ask for.
+        # If we arrived here, the lhs root node satisfies the rhs root node. Now we need to check
+        # all the edges that have an abstract parent, and verify that they match some edge in the
+        # lhs.
+        #
+        # It might happen that the rhs brings in concrete sub-DAGs. For those we don't need to
+        # verify the edge properties, cause everything is encoded in the hash of the nodes that
+        # will be verified later.
         lhs_edges: Dict[str, Set[DependencySpec]] = collections.defaultdict(set)
         for rhs_edge in other.traverse_edges(root=False, cover="edges"):
             # If we are checking for ^mpi we need to verify if there is any edge
@@ -3910,6 +3914,10 @@ def satisfies(self, other: Union[str, "Spec"], deps: bool = True) -> bool:
             if not rhs_edge.virtuals:
                 continue
 
+            # Skip edges from a concrete sub-DAG
+            if rhs_edge.parent.concrete:
+                continue
+
             if not lhs_edges:
                 # Construct a map of the link/run subDAG + direct "build" edges,
                 # keyed by dependency name
diff --git a/var/spack/repos/builtin.mock/packages/dtbuild1/package.py b/var/spack/repos/builtin.mock/packages/dtbuild1/package.py
--- a/var/spack/repos/builtin.mock/packages/dtbuild1/package.py
+++ b/var/spack/repos/builtin.mock/packages/dtbuild1/package.py
@@ -16,6 +16,6 @@ class Dtbuild1(Package):
     version("1.0", md5="0123456789abcdef0123456789abcdef")
     version("0.5", md5="fedcba9876543210fedcba9876543210")
 
-    depends_on("dtbuild2", type="build")
+    depends_on("vdtbuild2", type="build")
     depends_on("dtlink2")
     depends_on("dtrun2", type="run")
diff --git a/var/spack/repos/builtin.mock/packages/dtbuild2/package.py b/var/spack/repos/builtin.mock/packages/dtbuild2/package.py
--- a/var/spack/repos/builtin.mock/packages/dtbuild2/package.py
+++ b/var/spack/repos/builtin.mock/packages/dtbuild2/package.py
@@ -13,3 +13,5 @@ class Dtbuild2(Package):
     url = "http://www.example.com/dtbuild2-1.0.tar.gz"
 
     version("1.0", md5="0123456789abcdef0123456789abcdef")
+
+    provides("vdtbuild2")


Test patch:
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -2448,6 +2448,20 @@ def _default_libc(self):
         s = Spec("a %gcc@=13.2.0").concretized()
         assert s.satisfies("%gcc@13.2.0")
 
+    @pytest.mark.regression("43267")
+    def test_spec_with_build_dep_from_json(self, tmp_path):
+        """Tests that we can correctly concretize a spec, when we express its dependency as a
+        concrete spec to be read from JSON.
+
+        The bug was triggered by missing virtuals on edges that were trimmed from pure build
+        dependencies.
+        """
+        build_dep = Spec("dttop").concretized()
+        json_file = tmp_path / "build.json"
+        json_file.write_text(build_dep.to_json())
+        s = Spec(f"dtuse ^{str(json_file)}").concretized()
+        assert s["dttop"].dag_hash() == build_dep.dag_hash()
+
 
 @pytest.fixture()
 def duplicates_test_repository():