Skip to content

Commit 8428b81

Browse files
authored
[OPIK-6891] [BE] test: guard the trace deletion-events bridge with ArchUnit (#7390)
Add an ArchUnit architectural test asserting that only TraceServiceImpl may call TraceDAO.delete, so a future refactor adding a new trace-delete path can't silently bypass the deletion-events bridge capture. Retention paths are separate methods and intentionally out of scope. Introduce ArchUnit (archunit-junit5 1.4.2 — bundles ASM with Java 25 support) as a test dependency.
1 parent 284f1ff commit 8428b81

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

apps/opik-backend/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@
451451
<artifactId>testcontainers-junit-jupiter</artifactId>
452452
<scope>test</scope>
453453
</dependency>
454+
<dependency>
455+
<groupId>com.tngtech.archunit</groupId>
456+
<artifactId>archunit-junit5</artifactId>
457+
<version>1.4.2</version>
458+
<scope>test</scope>
459+
</dependency>
454460
<dependency>
455461
<groupId>uk.co.jemos.podam</groupId>
456462
<artifactId>podam</artifactId>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.comet.opik.domain;
2+
3+
import com.tngtech.archunit.core.importer.ImportOption;
4+
import com.tngtech.archunit.junit.AnalyzeClasses;
5+
import com.tngtech.archunit.junit.ArchTest;
6+
import com.tngtech.archunit.lang.ArchRule;
7+
import io.r2dbc.spi.Connection;
8+
9+
import java.util.Set;
10+
import java.util.UUID;
11+
12+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
13+
14+
/**
15+
* Architectural guard for the trace deletion event capturing. User-initiated trace deletes must flow through
16+
* {@link TraceServiceImpl}, which records the deleted ids in {@code deletion_events_local} after the delete so
17+
* they survive the data-model migration's table copy. A new caller of
18+
* {@code TraceDAO.delete(Set, UUID, Connection)} would issue the lightweight delete without that capture,
19+
* silently bypassing the capturing, so this rule fails the build if one appears. Retention paths
20+
* ({@code deleteForRetention*}) are intentionally not captured and are separate methods, so they are not matched.
21+
*/
22+
@AnalyzeClasses(packages = "com.comet.opik", importOptions = ImportOption.DoNotIncludeTests.class)
23+
class TraceDeletionEventArchTest {
24+
25+
/**
26+
* allowEmptyShould: this is a negative rule; with zero violations (the healthy state) its should-clause
27+
* matches nothing, which ArchUnit otherwise rejects. It still fails if a bypassing caller is introduced.
28+
*/
29+
@ArchTest
30+
static final ArchRule trace_deletes_must_route_through_the_capturing_service = noClasses()
31+
.that().doNotBelongToAnyOf(TraceServiceImpl.class)
32+
.should().callMethod(TraceDAO.class, "delete", Set.class, UUID.class, Connection.class)
33+
.because("""
34+
trace deletes must go through TraceServiceImpl so the deletion-events bridge captures every
35+
deleted id; a new caller here would silently bypass the bridge
36+
""")
37+
.allowEmptyShould(true);
38+
}

0 commit comments

Comments
 (0)