Repository: dbt-labs/dbt-core

Issue:
[Bug] Unit tests + compiled paths: report correct compiled path if the test fails, and don't overwrite input compiled paths with fixtures
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

Two problems:
1. Unit test failure includes link to yaml file where the unit test is defined, rather than the file where I could find the actual compiled code
2. In the process of running the unit test, we overwrite the compiled code for _the actual model being unit tested_ with its 

```
22:43:55  Failure in unit_test my_unit_test (models/my_model.yml)
22:43:55

actual differs from expected:

@@,id,renamed
→ ,1 ,red→blue


22:43:55
22:43:55    compiled Code at models/my_model.yml
```

### Expected Behavior

1. Unit test failure includes the actual compiled path of the unit test query, so I could copy-paste-rerun it if I so chose
2. Don't overwrite the compiled path for the input model(s) with fixture(s)

May be tricky to link what we're running with the definition of it. The dummy nodes and manifest go away at the end of execution.

### Steps To Reproduce

```sql
-- models/subfolder/model_a.sql
select
  1 as id, 'blue' as color
```
```sql
-- models/subfolder/model_b.sql
select
    id,
    color
from {{ ref('model_a')}}
```
```yml
# models/subfolder/my_model.yml
unit_tests:
  - name: my_unit_test
    model: model_b
    given:
      - input: ref('model_a')
        rows:
          - { id: 1, renamed: 'blue' }
    expect:
      rows:
        - { id: 1, renamed: 'red' }
```

Run `dbt build`, and see that:
1. The compiled code is `models/subfolder/my_model.yml` rather than `target/compiled/my_dbt_project/models/my_model.yml/models/my_unit_test.sql`
2. dbt has overwritten `target/compiled/my_dbt_project/models/model_a.sql` with its _fixture_ for the unit test, which I think we should avoid doing if possible:
```sql

-- Fixture for model_a
select 
    
    cast(1 as INTEGER)
 as id, 
    
    cast('blue' as character varying(256))
 as renamed
```

### Relevant log output

```shell
$ dbt build
22:47:45  Running with dbt=1.8.0-a1
22:47:45  Registered adapter: duckdb=1.7.0 ;)
22:47:46  Found 2 models, 515 macros, 1 unit_test
22:47:46
22:47:46  Concurrency: 1 threads (target='dev')
22:47:46
22:47:46  1 of 3 START sql view model dev.model_a ........................................ [RUN]
22:47:46  1 of 3 OK created sql view model dev.model_a ................................... [OK in 0.08s]
22:47:46  2 of 3 START unit_test model_b::my_unit_test ................................... [RUN]
22:47:46  2 of 3 FAIL 1 model_b::my_unit_test ............................................ [FAIL 1 in 0.38s]
22:47:46  3 of 3 SKIP relation dev.model_b ............................................... [SKIP]
22:47:46
22:47:46  Finished running 2 view models, 1 unit_test in 0 hours 0 minutes and 0.63 seconds (0.63s).
22:47:46
22:47:46  Completed with 1 error and 0 warnings:
22:47:46
22:47:46  Failure in unit_test my_unit_test (models/subfolder/my_model.yml)
22:47:46

actual differs from expected:

@@,id,color
→ ,1 ,red→blue


22:47:46
22:47:46    compiled Code at models/subfolder/my_model.yml
22:47:46
22:47:46  Done. PASS=1 WARN=0 ERROR=1 SKIP=1 TOTAL=3
```


### Environment

```markdown
- OS: 13.4.1
- Python: 3.10.11
- dbt: 1.8.0a1 (main)
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_
[Bug] Unit tests + compiled paths: report correct compiled path if the test fails, and don't overwrite input compiled paths with fixtures
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

Two problems:
1. Unit test failure includes link to yaml file where the unit test is defined, rather than the file where I could find the actual compiled code
2. In the process of running the unit test, we overwrite the compiled code for _the actual model being unit tested_ with its 

```
22:43:55  Failure in unit_test my_unit_test (models/my_model.yml)
22:43:55

actual differs from expected:

@@,id,renamed
→ ,1 ,red→blue


22:43:55
22:43:55    compiled Code at models/my_model.yml
```

### Expected Behavior

1. Unit test failure includes the actual compiled path of the unit test query, so I could copy-paste-rerun it if I so chose
2. Don't overwrite the compiled path for the input model(s) with fixture(s)

May be tricky to link what we're running with the definition of it. The dummy nodes and manifest go away at the end of execution.

### Steps To Reproduce

```sql
-- models/subfolder/model_a.sql
select
  1 as id, 'blue' as color
```
```sql
-- models/subfolder/model_b.sql
select
    id,
    color
from {{ ref('model_a')}}
```
```yml
# models/subfolder/my_model.yml
unit_tests:
  - name: my_unit_test
    model: model_b
    given:
      - input: ref('model_a')
        rows:
          - { id: 1, renamed: 'blue' }
    expect:
      rows:
        - { id: 1, renamed: 'red' }
```

Run `dbt build`, and see that:
1. The compiled code is `models/subfolder/my_model.yml` rather than `target/compiled/my_dbt_project/models/my_model.yml/models/my_unit_test.sql`
2. dbt has overwritten `target/compiled/my_dbt_project/models/model_a.sql` with its _fixture_ for the unit test, which I think we should avoid doing if possible:
```sql

-- Fixture for model_a
select 
    
    cast(1 as INTEGER)
 as id, 
    
    cast('blue' as character varying(256))
 as renamed
```

### Relevant log output

```shell
$ dbt build
22:47:45  Running with dbt=1.8.0-a1
22:47:45  Registered adapter: duckdb=1.7.0 ;)
22:47:46  Found 2 models, 515 macros, 1 unit_test
22:47:46
22:47:46  Concurrency: 1 threads (target='dev')
22:47:46
22:47:46  1 of 3 START sql view model dev.model_a ........................................ [RUN]
22:47:46  1 of 3 OK created sql view model dev.model_a ................................... [OK in 0.08s]
22:47:46  2 of 3 START unit_test model_b::my_unit_test ................................... [RUN]
22:47:46  2 of 3 FAIL 1 model_b::my_unit_test ............................................ [FAIL 1 in 0.38s]
22:47:46  3 of 3 SKIP relation dev.model_b ............................................... [SKIP]
22:47:46
22:47:46  Finished running 2 view models, 1 unit_test in 0 hours 0 minutes and 0.63 seconds (0.63s).
22:47:46
22:47:46  Completed with 1 error and 0 warnings:
22:47:46
22:47:46  Failure in unit_test my_unit_test (models/subfolder/my_model.yml)
22:47:46

actual differs from expected:

@@,id,color
→ ,1 ,red→blue


22:47:46
22:47:46    compiled Code at models/subfolder/my_model.yml
22:47:46
22:47:46  Done. PASS=1 WARN=0 ERROR=1 SKIP=1 TOTAL=3
```


### Environment

```markdown
- OS: 13.4.1
- Python: 3.10.11
- dbt: 1.8.0a1 (main)
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_


Discussion:

Resolving patch:
diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py
--- a/core/dbt/contracts/graph/nodes.py
+++ b/core/dbt/contracts/graph/nodes.py
@@ -954,16 +954,6 @@ class UnitTestDefinition(NodeInfoMixin, GraphNode, UnitTestDefinitionResource):
     def resource_class(cls) -> Type[UnitTestDefinitionResource]:
         return UnitTestDefinitionResource
 
-    @property
-    def build_path(self):
-        # TODO: is this actually necessary?
-        return self.original_file_path
-
-    @property
-    def compiled_path(self):
-        # TODO: is this actually necessary?
-        return self.original_file_path
-
     @property
     def depends_on_nodes(self):
         return self.depends_on.nodes
diff --git a/core/dbt/task/printer.py b/core/dbt/task/printer.py
--- a/core/dbt/task/printer.py
+++ b/core/dbt/task/printer.py
@@ -105,7 +105,7 @@ def print_run_result_error(result, newline: bool = True, is_warning: bool = Fals
         else:
             fire_event(RunResultErrorNoMessage(status=result.status))
 
-        if result.node.build_path is not None:
+        if result.node.compiled_path is not None:
             with TextOnly():
                 fire_event(Formatting(""))
             fire_event(SQLCompiledPath(path=result.node.compiled_path))


Test patch:
diff --git a/core/dbt/parser/unit_tests.py b/core/dbt/parser/unit_tests.py
--- a/core/dbt/parser/unit_tests.py
+++ b/core/dbt/parser/unit_tests.py
@@ -120,10 +120,12 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
             original_input_node = self._get_original_input_node(
                 given.input, tested_node, test_case.name
             )
+            input_name = original_input_node.name
 
             common_fields = {
                 "resource_type": NodeType.Model,
-                "original_file_path": original_input_node.original_file_path,
+                # root directory for input and output fixtures
+                "original_file_path": unit_test_node.original_file_path,
                 "config": ModelConfig(materialized="ephemeral"),
                 "database": original_input_node.database,
                 "alias": original_input_node.identifier,
@@ -131,6 +133,10 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
                 "fqn": original_input_node.fqn,
                 "checksum": FileHash.empty(),
                 "raw_code": self._build_fixture_raw_code(given.rows, None),
+                "package_name": original_input_node.package_name,
+                "unique_id": f"model.{original_input_node.package_name}.{input_name}",
+                "name": input_name,
+                "path": f"{input_name}.sql",
             }
 
             if original_input_node.resource_type in (
@@ -138,14 +144,7 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
                 NodeType.Seed,
                 NodeType.Snapshot,
             ):
-                input_name = original_input_node.name
-                input_node = ModelNode(
-                    **common_fields,
-                    package_name=original_input_node.package_name,
-                    unique_id=f"model.{original_input_node.package_name}.{input_name}",
-                    name=input_name,
-                    path=original_input_node.path or f"{input_name}.sql",
-                )
+                input_node = ModelNode(**common_fields)
                 if (
                     original_input_node.resource_type == NodeType.Model
                     and original_input_node.version
@@ -156,13 +155,8 @@ def parse_unit_test_case(self, test_case: UnitTestDefinition):
                 # We are reusing the database/schema/identifier from the original source,
                 # but that shouldn't matter since this acts as an ephemeral model which just
                 # wraps a CTE around the unit test node.
-                input_name = original_input_node.name
                 input_node = UnitTestSourceDefinition(
                     **common_fields,
-                    package_name=original_input_node.package_name,
-                    unique_id=f"model.{original_input_node.package_name}.{input_name}",
-                    name=original_input_node.name,  # must be the same name for source lookup to work
-                    path=input_name + ".sql",  # for writing out compiled_code
                     source_name=original_input_node.source_name,  # needed for source lookup
                 )
                 # Sources need to go in the sources dictionary in order to create the right lookup
diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py
--- a/core/dbt/task/test.py
+++ b/core/dbt/task/test.py
@@ -7,12 +7,18 @@
 from dbt_common.events.format import pluralize
 from dbt_common.dataclass_schema import dbtClassMixin
 import threading
-from typing import Dict, Any, Optional, Union, List, TYPE_CHECKING
+from typing import Dict, Any, Optional, Union, List, TYPE_CHECKING, Tuple
 
 from .compile import CompileRunner
 from .run import RunTask
 
-from dbt.contracts.graph.nodes import TestNode, UnitTestDefinition, UnitTestNode
+from dbt.contracts.graph.nodes import (
+    TestNode,
+    UnitTestDefinition,
+    UnitTestNode,
+    GenericTestNode,
+    SingularTestNode,
+)
 from dbt.contracts.graph.manifest import Manifest
 from dbt.artifacts.schemas.results import TestStatus
 from dbt.artifacts.schemas.run import RunResult
@@ -180,7 +186,7 @@ def build_unit_test_manifest_from_test(
 
     def execute_unit_test(
         self, unit_test_def: UnitTestDefinition, manifest: Manifest
-    ) -> UnitTestResultData:
+    ) -> Tuple[UnitTestNode, UnitTestResultData]:
 
         unit_test_manifest = self.build_unit_test_manifest_from_test(unit_test_def, manifest)
 
@@ -190,6 +196,7 @@ def execute_unit_test(
 
         # Compile the node
         unit_test_node = self.compiler.compile_node(unit_test_node, unit_test_manifest, {})
+        assert isinstance(unit_test_node, UnitTestNode)
 
         # generate_runtime_unit_test_context not strictly needed - this is to run the 'unit'
         # materialization, not compile the node.compiled_code
@@ -243,18 +250,21 @@ def execute_unit_test(
                 rendered=rendered,
             )
 
-        return UnitTestResultData(
+        unit_test_result_data = UnitTestResultData(
             diff=diff,
             should_error=should_error,
             adapter_response=adapter_response,
         )
 
-    def execute(self, test: Union[TestNode, UnitTestDefinition], manifest: Manifest):
+        return unit_test_node, unit_test_result_data
+
+    def execute(self, test: Union[TestNode, UnitTestNode], manifest: Manifest):
         if isinstance(test, UnitTestDefinition):
-            unit_test_result = self.execute_unit_test(test, manifest)
-            return self.build_unit_test_run_result(test, unit_test_result)
+            unit_test_node, unit_test_result = self.execute_unit_test(test, manifest)
+            return self.build_unit_test_run_result(unit_test_node, unit_test_result)
         else:
             # Note: manifest here is a normal manifest
+            assert isinstance(test, (SingularTestNode, GenericTestNode))
             test_result = self.execute_data_test(test, manifest)
             return self.build_test_run_result(test, test_result)
 
@@ -293,7 +303,7 @@ def build_test_run_result(self, test: TestNode, result: TestResultData) -> RunRe
         return run_result
 
     def build_unit_test_run_result(
-        self, test: UnitTestDefinition, result: UnitTestResultData
+        self, test: UnitTestNode, result: UnitTestResultData
     ) -> RunResult:
         thread_id = threading.current_thread().name
 
@@ -306,7 +316,7 @@ def build_unit_test_run_result(
             failures = 1
 
         return RunResult(
-            node=test,  # type: ignore
+            node=test,
             status=status,
             timing=[],
             thread_id=thread_id,
diff --git a/tests/functional/unit_testing/test_unit_testing.py b/tests/functional/unit_testing/test_unit_testing.py
--- a/tests/functional/unit_testing/test_unit_testing.py
+++ b/tests/functional/unit_testing/test_unit_testing.py
@@ -5,6 +5,9 @@
     run_dbt,
     write_file,
     get_manifest,
+    run_dbt_and_capture,
+    read_file,
+    file_exists,
 )
 from dbt.contracts.results import NodeStatus
 from dbt.exceptions import DuplicateResourceNameError, ParsingError
@@ -30,6 +33,7 @@
     test_my_model_incremental_yml_wrong_override,
     test_my_model_incremental_yml_no_this_input,
 )
+from tests.unit.utils import normalize
 
 
 class TestUnitTests:
@@ -442,3 +446,60 @@ def test_unit_test_ext_nodes(
         run_dbt(["run"], expect_pass=True)
         results = run_dbt(["test", "--select", "valid_emails"], expect_pass=True)
         assert len(results) == 1
+
+
+subfolder_model_a_sql = """select 1 as id, 'blue' as color"""
+
+subfolder_model_b_sql = """
+select
+    id,
+    color
+from {{ ref('model_a') }}
+"""
+
+subfolder_my_model_yml = """
+unit_tests:
+  - name: my_unit_test
+    model: model_b
+    given:
+      - input: ref('model_a')
+        rows:
+          - { id: 1, color: 'blue' }
+    expect:
+      rows:
+        - { id: 1, color: 'red' }
+"""
+
+
+class TestUnitTestSubfolderPath:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "subfolder": {
+                "model_a.sql": subfolder_model_a_sql,
+                "model_b.sql": subfolder_model_b_sql,
+                "my_model.yml": subfolder_my_model_yml,
+            }
+        }
+
+    def test_subfolder_unit_test(self, project):
+        results, output = run_dbt_and_capture(["build"], expect_pass=False)
+
+        # Test that input fixture doesn't overwrite the original model
+        assert (
+            read_file("target/compiled/test/models/subfolder/model_a.sql").strip()
+            == subfolder_model_a_sql.strip()
+        )
+
+        # Test that correct path is written in logs
+        assert (
+            normalize(
+                "target/compiled/test/models/subfolder/my_model.yml/models/subfolder/my_unit_test.sql"
+            )
+            in output
+        )
+        assert file_exists(
+            normalize(
+                "target/compiled/test/models/subfolder/my_model.yml/models/subfolder/my_unit_test.sql"
+            )
+        )
diff --git a/tests/functional/unit_testing/test_ut_names.py b/tests/functional/unit_testing/test_ut_names.py
--- a/tests/functional/unit_testing/test_ut_names.py
+++ b/tests/functional/unit_testing/test_ut_names.py
@@ -27,33 +27,37 @@ def test_duplicate_test_names_across_models(self, project):
         # Select duplicate tests
         results, log_output = run_dbt_and_capture(["test"], expect_pass=True)
         assert len(results) == 2
-        assert ["my_model_a", "my_model_b"] == sorted([result.node.model for result in results])
+        assert {"model.test.my_model_a", "model.test.my_model_b"} == {
+            result.node.tested_node_unique_id for result in results
+        }
         assert "my_model_a::my_test_name" in log_output
         assert "my_model_b::my_test_name" in log_output
 
         # Test select duplicates by by test name
         results = run_dbt(["test", "--select", "test_name:my_test_name"])
         assert len(results) == 2
-        assert ["my_model_a", "my_model_b"] == sorted([result.node.model for result in results])
+        assert {"model.test.my_model_a", "model.test.my_model_b"} == {
+            result.node.tested_node_unique_id for result in results
+        }
         assert "my_model_a::my_test_name" in log_output
         assert "my_model_b::my_test_name" in log_output
 
         results = run_dbt(["test", "--select", "my_model_a,test_name:my_test_name"])
         assert len(results) == 1
-        assert results[0].node.model == "my_model_a"
+        assert results[0].node.tested_node_unique_id == "model.test.my_model_a"
 
         results = run_dbt(["test", "--select", "my_model_b,test_name:my_test_name"])
         assert len(results) == 1
-        assert results[0].node.model == "my_model_b"
+        assert results[0].node.tested_node_unique_id == "model.test.my_model_b"
 
         # Test select by model name
         results = run_dbt(["test", "--select", "my_model_a"])
         assert len(results) == 1
-        assert results[0].node.model == "my_model_a"
+        assert results[0].node.tested_node_unique_id == "model.test.my_model_a"
 
         results = run_dbt(["test", "--select", "my_model_b"])
         assert len(results) == 1
-        assert results[0].node.model == "my_model_b"
+        assert results[0].node.tested_node_unique_id == "model.test.my_model_b"
 
 
 class TestUnitTestDuplicateTestNamesWithinModel: