diff --git a/.gemini/rules/async-test-configuration.md b/.gemini/rules/async-test-configuration.md new file mode 100644 index 00000000..c3ed719a --- /dev/null +++ b/.gemini/rules/async-test-configuration.md @@ -0,0 +1,66 @@ + +# Rules for async-test-configuration + +## Locked Status + +### se.deversity.asynctest.DetectorType +- **Reason**: Each enum constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) both branches of AsyncTestConfig.build() (detectAll block + excludes block), and (5) DetectorRegistry constructor. Adding a value here in isolation breaks the system. + +## Mirrored — Keep In Sync + +### se.deversity.asynctest.DetectorType +- **Rule**: Free to change, but every mirror must change in the same commit. +- **Mirrors**: se.deversity.asynctest.AsyncTest, se.deversity.asynctest.AsyncTestConfig, se.deversity.asynctest.DetectorRegistry, se.deversity.asynctest.spi.LegacyDetectorFactories, META-INF/services/se.deversity.asynctest.spi.DetectorFactory +- **Reason**: A detector is only reachable from the public API when all of these agree. The enum constant is the name users type in @AsyncTest(excludes=...); the annotation attribute, the config field and its Builder default carry it through resolution; the registry constructor instantiates it; and the SPI factory plus its services entry are what detectAll loads. Adding the constant alone compiles and silently detects nothing. +- **Enforced by**: se.deversity.asynctest.spi.AllDetectorsSpiCoverageTest + +## Context & Focus + +### se.deversity.asynctest.AsyncTestConfig +- **Focus**: Maintain strict 1:1 mapping between @AsyncTest attributes, Builder fields, from(AsyncTest), build() logic, and DetectorRegistry +- **Avoid**: mutable state — this class must remain immutable after construction + +### se.deversity.asynctest.DetectorRegistry +- **Focus**: Each new detector requires exactly three steps in this class: (1) a final field declaration, (2) conditional construction in the constructor keyed on the config flag, (3) an analyzeAll() call in the correct phase block. All three steps must be added together. +- **Avoid**: partial patterns — a field without construction or analysis silently skips detection + +## Core Functionality + +### se.deversity.asynctest.AsyncTestConfig +- **Sensitivity**: Critical +- **Note**: Adding a new detector requires synchronized changes across six places: @AsyncTest attribute, AsyncTestConfig field, Builder default, from(AsyncTest) call chain, build() detectAll/excludes blocks, and DetectorRegistry constructor. + +## Immutable Type +- **Rule**: These types are immutable. Never introduce non-final fields, setters, or mutating methods. + +### se.deversity.asynctest.AsyncTestConfig +- **Note**: Immutable snapshot of @AsyncTest parameters to ensure thread safety. + +### se.deversity.asynctest.Preset +- **Note**: Enum constants — JVM guarantees structural immutability. Internal enabled-set is captured at class init. + +## Feature Flag Gate +- **Rule**: This code is gated behind a feature flag. Preserve the flag check. Never assume the flag is always active. + +### se.deversity.asynctest.AsyncTestConfig.enableBenchmarking +- **Flag**: 'async-test.benchmarking.enabled' (default: false) + +### se.deversity.asynctest.AsyncTestConfig.licenseMockMode +- **Flag**: 'license.mock.mode' (default: false) + +## Thread-Safety Guarantee + +### se.deversity.asynctest.DetectorRegistry +- **Strategy**: SYNCHRONIZED +- **Note**: Guards conditional access to internal detector initialization and phase blocks. + +## Contract-Frozen Signature + +### se.deversity.asynctest.AsyncTest +- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions. +- **Reason**: Public annotation API used directly in user test methods. Attribute names, types, and defaults are part of the stable public API — any change is a breaking change for all consumers. + +## Public API Surface Protection +- **Rule**: Exposes public API. Preserve signature, Javadoc, and behavior without breaking backwards or source compatibility. +- **Applies to**: `se.deversity.asynctest.AsyncTest`, `se.deversity.asynctest.Preset` + diff --git a/.gemini/rules/async-test-detectors.md b/.gemini/rules/async-test-detectors.md new file mode 100644 index 00000000..ff887c4f --- /dev/null +++ b/.gemini/rules/async-test-detectors.md @@ -0,0 +1,221 @@ + +# Rules for async-test-detectors + +## Performance Constraints + +### se.deversity.asynctest.diagnostics.SiteCapture +- **Rule**: Optimal complexity required. O(n^2) is forbidden on hot paths. +- **Constraint**: Called from detector recordAccess paths; do not allocate when a site is already captured for a given key. + +## Test-Driven Requirements +- **Rule**: Changes MUST be accompanied by a matching test update. +- **Coverage Goal**: 80% +- **Frameworks**: JUNIT_5 + +### se.deversity.asynctest.diagnostics.CompletableFutureBlockingCallbackDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/CompletableFutureBlockingCallbackDetectorTest.java + +### se.deversity.asynctest.diagnostics.CompletableFutureObtrudeDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/CompletableFutureObtrudeDetectorTest.java + +### se.deversity.asynctest.diagnostics.DaemonThreadHygieneDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/DaemonThreadHygieneDetectorTest.java + +### se.deversity.asynctest.diagnostics.FileChannelPositionRaceDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/FileChannelPositionRaceDetectorTest.java + +### se.deversity.asynctest.diagnostics.FinalFieldMutationDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/FinalFieldMutationDetectorTest.java + +### se.deversity.asynctest.diagnostics.HighContentionAtomicDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/HighContentionAtomicDetectorTest.java + +### se.deversity.asynctest.diagnostics.JdbcConnectionSharedDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/JdbcConnectionSharedDetectorTest.java + +### se.deversity.asynctest.diagnostics.LazyConstantMisuseDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/LazyConstantMisuseDetectorTest.java + +### se.deversity.asynctest.diagnostics.LockUpgradeDeadlockDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/LockUpgradeDeadlockDetectorTest.java + +### se.deversity.asynctest.diagnostics.NonAtomicConcurrentMapUpdateDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/NonAtomicConcurrentMapUpdateDetectorTest.java + +### se.deversity.asynctest.diagnostics.NotifyWithoutMonitorDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/NotifyWithoutMonitorDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedByteBufferDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedByteBufferDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedCharsetCoderDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedCharsetCoderDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedChecksumDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedChecksumDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedDeflaterDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedDeflaterDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedIteratorDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedIteratorDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedJsonMapperReconfigDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedJsonMapperReconfigDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedKdfDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedKdfDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedMessageDigestDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedMessageDigestDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedSecureRandomDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedSecureRandomDetectorTest.java + +### se.deversity.asynctest.diagnostics.SharedStatefulCryptoDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SharedStatefulCryptoDetectorTest.java + +### se.deversity.asynctest.diagnostics.SpuriousWakeupDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/SpuriousWakeupDetectorTest.java + +### se.deversity.asynctest.diagnostics.ThisEscapeDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/ThisEscapeDetectorTest.java + +### se.deversity.asynctest.diagnostics.ThreadLocalRandomMisuseDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/ThreadLocalRandomMisuseDetectorTest.java + +### se.deversity.asynctest.diagnostics.TryLockMisuseDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/TryLockMisuseDetectorTest.java + +### se.deversity.asynctest.diagnostics.WeakHashMapSharedDetector +- **Test Location**: src/test/java/se/deversity/asynctest/diagnostics/WeakHashMapSharedDetectorTest.java + +## Thread-Safety Guarantee + +### se.deversity.asynctest.diagnostics.CompletableFutureBlockingCallbackDetector +- **Strategy**: OTHER +- **Note**: ThreadLocal tracks active callbacks; ConcurrentHashMap stores violations. + +### se.deversity.asynctest.diagnostics.CompletableFutureObtrudeDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap stores state per CF instance. + +### se.deversity.asynctest.diagnostics.DaemonThreadHygieneDetector +- **Strategy**: OTHER +- **Note**: Per-thread access map is a ConcurrentHashMap; first-registration-wins via putIfAbsent. + +### se.deversity.asynctest.diagnostics.FileChannelPositionRaceDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet() and track only implicit-position accessors. + +### se.deversity.asynctest.diagnostics.FinalFieldMutationDetector +- **Strategy**: OTHER +- **Note**: Per-field state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.HighContentionAtomicDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; counters are LongAdder; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.JdbcConnectionSharedDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap-backed JDBC-resource tracking; per-resource State holds ConcurrentHashMap.newKeySet() for accessing threads. + +### se.deversity.asynctest.diagnostics.LazyConstantMisuseDetector +- **Strategy**: OTHER +- **Note**: Per-constant state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id sets are ConcurrentHashMap.newKeySet(); reports are synchronized lists. + +### se.deversity.asynctest.diagnostics.LockUpgradeDeadlockDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap tracks read lock ownership and violations. + +### se.deversity.asynctest.diagnostics.NonAtomicConcurrentMapUpdateDetector +- **Strategy**: OTHER +- **Note**: Per (map,key) state in a ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.NotifyWithoutMonitorDetector +- **Strategy**: SYNCHRONIZED +- **Note**: Attempts list mutated under a single intrinsic monitor on the list itself; sampling Thread.holdsLock requires no locking. + +### se.deversity.asynctest.diagnostics.SharedByteBufferDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name and operation sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedCharsetCoderDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedChecksumDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedDeflaterDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedIteratorDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedJsonMapperReconfigDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; using-thread sets are ConcurrentHashMap.newKeySet(); violating mutations recorded in a CopyOnWriteArrayList. + +### se.deversity.asynctest.diagnostics.SharedKdfDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedMessageDigestDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedSecureRandomDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with double-check (get-then-computeIfAbsent) hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SharedStatefulCryptoDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with double-check (get-then-computeIfAbsent) hot path; thread-id/name sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.SpuriousWakeupDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap stores state per monitor instance. + +### se.deversity.asynctest.diagnostics.ThisEscapeDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; escape descriptions and observer-thread sets are ConcurrentHashMap.newKeySet(); the completed flag is volatile. + +### se.deversity.asynctest.diagnostics.ThreadLocalRandomMisuseDetector +- **Strategy**: OTHER +- **Note**: Per-instance state in ConcurrentHashMap with get-then-computeIfAbsent hot path; misusing-thread sets are ConcurrentHashMap.newKeySet(). + +### se.deversity.asynctest.diagnostics.TryLockMisuseDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap tracks tryLock attempts, results, and unlock violations. + +### se.deversity.asynctest.diagnostics.WeakHashMapSharedDetector +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap-backed instance tracking; per-instance State holds ConcurrentHashMap.newKeySet() for thread ids/names. + +## Security-Critical Code +- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact. + +### se.deversity.asynctest.diagnostics.SharedMessageDigestDetector +- **Aspect**: cryptography (hash integrity / MAC / signature state) + +### se.deversity.asynctest.diagnostics.SharedSecureRandomDetector +- **Aspect**: cryptography (RNG quality) + +### se.deversity.asynctest.diagnostics.SharedStatefulCryptoDetector +- **Aspect**: cryptography (confidentiality / integrity / authenticity state) + +## Immutable Type + +### se.deversity.asynctest.diagnostics.SiteCapture.Site +- **Rule**: This type is immutable. Never introduce non-final fields, setters, or mutating methods. +- **Note**: Java record — fields are final by language; types are all primitives or String. + +## Public API Surface Protection + +### se.deversity.asynctest.diagnostics.SiteCapture.Site +- **Rule**: Exposes public API. Preserve signature, Javadoc, and behavior without breaking backwards or source compatibility. + diff --git a/.gemini/rules/async-test-instrumentation.md b/.gemini/rules/async-test-instrumentation.md new file mode 100644 index 00000000..5e2bfb36 --- /dev/null +++ b/.gemini/rules/async-test-instrumentation.md @@ -0,0 +1,15 @@ + +# Rules for async-test-instrumentation + +## Core Functionality + +### se.deversity.asynctest.agent.AsyncTestAgent +- **Sensitivity**: Critical +- **Note**: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave field accessors and double-count every access. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while the Advice stays a method-entry prologue that adds no fields, methods or interfaces. Nothing may throw out of premain — an exception there aborts JVM startup. The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar. + +## Contract-Frozen Signature + +### se.deversity.asynctest.agent.AgentOptions +- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions. +- **Reason**: The class is package-private but the agentArgs grammar it parses is public surface: users type it on the -javaagent: command line. Key names (includes/excludes/debug), the comma-or-semicolon separator, the bare-token continuation that lets one key carry several values, and case-insensitive key matching are all part of that contract — changing any of them breaks existing launch scripts silently. Parsing must stay total: it is called from premain, where a thrown exception aborts JVM startup, so unknown keys are ignored and malformed input degrades to the default instrument-everything behaviour rather than failing. + diff --git a/.gemini/rules/async-test-public-api.md b/.gemini/rules/async-test-public-api.md new file mode 100644 index 00000000..9c711595 --- /dev/null +++ b/.gemini/rules/async-test-public-api.md @@ -0,0 +1,78 @@ + +# Rules for async-test-public-api + +## Exclusion Rule + +### se.deversity.asynctest.NoopAsyncTestListener +This element is strictly excluded from AI context. Do not reference it. + +## Performance Constraints + +### se.deversity.asynctest.spi.adapters.LegacyDetectorAdapter +- **Rule**: Optimal complexity required. O(n^2) is forbidden on hot paths. +- **Constraint**: analyze() does Method.getMethod + invoke each call; only invoked once per round per detector, not on the hot recordAccess path. If profiling shows reflection overhead, cache the Method handles in the constructor. + +## Legacy Compatibility Bridge + +### se.deversity.asynctest.spi.adapters.LegacyDetectorAdapter +- **Rule**: Compatibility bridge. Do not attempt to modernize, elegant-ize, or refactor structural patterns. Only modify internal business logic as explicitly requested. + +## Contract-Frozen Signature +- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions. + +### se.deversity.asynctest.AsyncAssert +- **Reason**: Public assertion utility API for AsyncTest consumers. awaitUntil() and capture() are used directly in user test code — method signatures and semantics must not change without a major version bump. + +### se.deversity.asynctest.AsyncTestListener +- **Reason**: Public SPI interface for observing async-test lifecycle events. Method signatures are part of the stable API — implementors bind to these exact names and parameter types. + +### se.deversity.asynctest.AsyncTestListenerRegistry +- **Reason**: Public API for registering and unregistering AsyncTestListener instances. register(), unregister(), clearAll(), and fireXxx() methods are called by user code and infrastructure — signatures must not change. + +### se.deversity.asynctest.report.Formatter +- **Reason**: Public formatter SPI. format(List) signature must not change — built-in formatters and user-provided lambdas bind to this exact type. + +### se.deversity.asynctest.spi.Detector +- **Reason**: Public SPI interface. type(), analyze(), onTestStart(), and onTestEnd() signatures are part of the stable extension contract — implementors bind to these exact names and parameter types. + +### se.deversity.asynctest.spi.DetectorFactory +- **Reason**: Public SPI interface for ServiceLoader-based detector discovery. type(), isEnabledFor(), and create() signatures are part of the stable factory contract — implementors bind to these exact names and parameter types. + +## Public API Surface Protection +- **Rule**: Exposes public API. Preserve signature, Javadoc, and behavior without breaking backwards or source compatibility. +- **Applies to**: `se.deversity.asynctest.AsyncAssert`, `se.deversity.asynctest.AsyncTestListener`, `se.deversity.asynctest.AsyncTestListenerRegistry`, `se.deversity.asynctest.report.Formatter`, `se.deversity.asynctest.report.JsonFormatter`, `se.deversity.asynctest.report.MarkdownFormatter`, `se.deversity.asynctest.report.Violation`, `se.deversity.asynctest.spi.Detector`, `se.deversity.asynctest.spi.DetectorFactory`, `se.deversity.asynctest.spi.DetectorRegistry` + +## Idempotency Guarantee +- **Rule**: These operations are idempotent. Calling them multiple times must produce the same result as calling them once. + +### se.deversity.asynctest.AsyncTestListenerRegistry.Registration.close() +- **Reason**: Guarded by the `closed` volatile flag; second close() returns early before touching the registry. Covered by `registrationClose_isIdempotent` test. + +### se.deversity.asynctest.AsyncTestListenerRegistry.clearAll() +- **Reason**: List.clear() on an already-empty list is a no-op; repeated calls have identical observable effect (empty registry). + +### se.deversity.asynctest.AsyncTestListenerRegistry.unregister(se.deversity.asynctest.AsyncTestListener) +- **Reason**: Backed by List.remove which is a no-op when the listener is absent; second call returns false but produces no observable side effect. + +### se.deversity.asynctest.spi.DetectorRegistry.analyzeAll() +- **Reason**: Each Detector.analyze() must return the same violations for the same observed state (the SPI contract). Calling analyzeAll() N times on a quiescent registry yields N identical lists; do not introduce stateful side-effects in analyze(). + +## Polymorphic Extension Pattern +- **Pattern**: STRATEGY_PATTERN +- **Rule**: Open for extension, closed for modification. Use strategy or visitor subclasses instead of changing these files. +- **Applies to**: `se.deversity.asynctest.report.Formatter`, `se.deversity.asynctest.spi.Detector` + +## Immutable Type +- **Rule**: These types are immutable. Never introduce non-final fields, setters, or mutating methods. + +### se.deversity.asynctest.report.Violation +- **Note**: Java record — fields are final by language. Collection fields are deep-copied to immutable views in the canonical constructor. + +### se.deversity.asynctest.spi.DetectorRegistry +- **Note**: Effectively immutable after build() — the EnumMap is populated only in the private constructor and never mutated thereafter; safe to publish to multiple threads and read-only views over an EnumMap populated once at construction. + +## Input Sanitization +- **Target Filters**: XSS +- **Rule**: Run raw input strings through approved sanitizers. +- **Applies to**: `se.deversity.asynctest.report.JUnitXmlReportListener.onStructuredReport(java.lang.String,se.deversity.asynctest.diagnostics.IssueSeverity,java.lang.String)#report`, `se.deversity.asynctest.report.JsonReportListener.onStructuredReport(java.lang.String,se.deversity.asynctest.diagnostics.IssueSeverity,java.lang.String)#report` + diff --git a/.gemini/rules/async-test-runtime-core.md b/.gemini/rules/async-test-runtime-core.md new file mode 100644 index 00000000..f534d6ac --- /dev/null +++ b/.gemini/rules/async-test-runtime-core.md @@ -0,0 +1,78 @@ + +# Rules for async-test-runtime-core + +## Security Audit Requirements +When modifying these elements, audit for: +- Thread Safety issues +- **Applies to**: `se.deversity.asynctest.AsyncTestContext` + +### se.deversity.asynctest.runner.ConcurrencyRunner +- Resource Leaks + +## Core Functionality +- **Sensitivity**: Critical + +### se.deversity.asynctest.AsyncTestContext +- **Note**: ThreadLocal install/uninstall must always be symmetric. A leak propagates stale detector state across test invocations and causes false positives or missed detections. + +### se.deversity.asynctest.extension.AsyncTestInvocationInterceptor +- **Note**: invocation.skip() is intentional — ConcurrencyRunner owns the full N×M execution and must never call invocation.proceed(). Restoring proceed() would run the test body once outside the CyclicBarrier, bypassing all detectors. + +### se.deversity.asynctest.runner.ConcurrencyRunner +- **Note**: Core stress-test execution engine. The CyclicBarrier pattern forces maximum thread contention. Timeout logic and AsyncTestContext install/uninstall are carefully calibrated — subtle changes introduce flaky tests or missed detector activations. + +## Thread-Safety Guarantee + +### se.deversity.asynctest.AsyncTestContext +- **Strategy**: THREAD_LOCAL +- **Note**: CURRENT ThreadLocal maintains context per active test thread symmetrically. + +### se.deversity.asynctest.runner.ConcurrencyRunner +- **Strategy**: OTHER +- **Note**: Coordinates concurrency using CyclicBarrier to maximize thread contention. + +### se.deversity.asynctest.runner.LicenseGuard +- **Strategy**: OTHER +- **Note**: ConcurrentHashMap.computeIfAbsent guarantees at-most-once gate execution per fingerprint under contention; volatile announce flags collapse the GRANTED/CI banner to once-per-JVM. + +## Public API Surface Protection +- **Rule**: Exposes public API. Preserve signature, Javadoc, and behavior without breaking backwards or source compatibility. +- **Applies to**: `se.deversity.asynctest.AsyncTestContext`, `se.deversity.asynctest.AsyncTestContext.sharedCryptographyDetector()`, `se.deversity.asynctest.AsyncTestContext.sharedMessageDigestDetector()`, `se.deversity.asynctest.extension.AsyncTestExtension` + +## Idempotency Guarantee +- **Rule**: These operations are idempotent. Calling them multiple times must produce the same result as calling them once. + +### se.deversity.asynctest.AsyncTestContext.uninstall() +- **Reason**: ThreadLocal.remove() is documented as a no-op when the thread has no value set; the install/uninstall symmetry rule (CLAUDE.md) tolerates extra uninstalls. ConcurrencyRunner relies on this in its outermost-finally cleanup. + +### se.deversity.asynctest.runner.LicenseGuard.check(se.deversity.asynctest.AsyncTestConfig) +- **Reason**: ConcurrentHashMap.computeIfAbsent guarantees the underlying gate.check fires at most once per Fingerprint; repeat calls return immediately. Denied results consistently throw SecurityException for the same fingerprint. + +## Access Restrictions + +### se.deversity.asynctest.AsyncTestContext.install(se.deversity.asynctest.AsyncTestContext) +- **Allowed Callers**: [se.deversity.asynctest.runner.ConcurrencyRunner] + +## Load-Bearing Oddity +- **Rule**: This looks removable but is deliberate. Refactor only while the invariant holds. + +### se.deversity.asynctest.extension.AsyncTestInvocationInterceptor.interceptTestTemplateMethod(org.junit.jupiter.api.extension.InvocationInterceptor.Invocation,org.junit.jupiter.api.extension.ReflectiveInvocationContext,org.junit.jupiter.api.extension.ExtensionContext) +- **Invariant**: This method calls invocation.skip() and never invocation.proceed(). +- **Breaks if changed**: Someone 'fixes' the apparently-dropped invocation by calling proceed(). The test body then runs once on the JUnit thread, outside the CyclicBarrier and outside AsyncTestContext, so no detector observes it — and because that single run usually passes, the suite goes green while every concurrency check has silently stopped running. + +### se.deversity.asynctest.runner.ConcurrencyRunner.execute(org.junit.jupiter.api.extension.ReflectiveInvocationContext,se.deversity.asynctest.AsyncTestConfig) +- **Invariant**: The timeoutAlreadyReported flag, and the per-step guarded cleanup in the finally block, are both deliberate. A pre-round deadline check throws an error that has already been through timeoutError(), and each cleanup step is wrapped in its own try so one failure cannot suppress the next. +- **Breaks if changed**: The flag is removed as redundant — the catch block then sends the same error through timeoutError() a second time, producing two onTimeout callbacks and two copies of every report for one timeout. Or the cleanup steps are merged into one try, at which point a failing AsyncTestContext.uninstall() skips the livelock snapshot and leaks context into the next test. + +## Contract-Frozen Signature + +### se.deversity.asynctest.extension.AsyncTestExtension +- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions. +- **Reason**: JUnit 5 TestTemplateInvocationContextProvider SPI. The two overridden methods (supportsTestTemplate, provideTestTemplateInvocationContexts) must preserve their exact signatures as mandated by JUnit. + +## Security-Critical Code + +### se.deversity.asynctest.runner.LicenseGuard +- **Rule**: This code is security-critical. Do not weaken security properties. Every change must be explicitly reviewed for security impact. +- **Aspect**: authorization + diff --git a/.vibetags-roles b/.vibetags-roles index 9a2ae4f3..8792c51a 100644 --- a/.vibetags-roles +++ b/.vibetags-roles @@ -1,4 +1,4 @@ -# VibeTags role routing (vibetags 1.0.0-RC8, se.deversity.vibetags.processor.internal.RoleConfig). +# VibeTags role routing (vibetags 1.0.0-RC10, se.deversity.vibetags.processor.internal.RoleConfig). # # Presence of this file switches granular rule generation from "one file per annotated # class" to "one file per role". Without it, .claude/rules/ would hold ~130 files — one diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..8373d927 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,142 @@ +# GEMINI.md + +AI guardrails for Google Gemini, generated from source annotations by +[VibeTags](https://github.com/PIsberg/vibetags). The region between the +VIBETAGS-START and VIBETAGS-END markers is regenerated on every compile; +never hand-edit inside it. Per-element detail lives in `.gemini/rules/`, +indexed from the block below. + + + +# AUTO-GENERATED AI RULES +# Generated by VibeTags | https://github.com/PIsberg/vibetags +# Do not edit manually. + +## 🧠 CORE FUNCTIONALITY (CHANGE WITH EXTREME CAUTION) +The following elements are well-tested core components. Make changes with extreme caution. + +- `se.deversity.asynctest.agent.AsyncTestAgent`: Sensitivity: Critical. Note: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave field accessors and double-count every access. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while the Advice stays a method-entry prologue that adds no fields, methods or interfaces. Nothing may throw out of premain — an exception there aborts JVM startup. The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar. + +## Scoped Rules Index +Detailed per-element guardrails live in scoped rule files that load automatically when you open the matching source file. Consult the referenced file before modifying an element: + +- `se.deversity.asynctest.agent.AgentOptions` → `.gemini/rules/async-test-instrumentation.md` +- `se.deversity.asynctest.agent.AsyncTestAgent` → `.gemini/rules/async-test-instrumentation.md` + + +# AUTO-GENERATED AI RULES +# Generated by VibeTags | https://github.com/PIsberg/vibetags +# Do not edit manually. + +## 🧠 CORE FUNCTIONALITY (CHANGE WITH EXTREME CAUTION) +The following elements are well-tested core components. Make changes with extreme caution. + +- `se.deversity.asynctest.analysis.StaticPinningScanner`: Sensitivity: High. Note: The whole module is this one class plus ASM, and ArchitectureTest pins both directions: nothing here may reference the library, and asm may not leak out of here. Keep the analysis one-directional — if the scanner starts needing the runner or a detector, that is a design question, not a dependency to add. The asymmetry in the findings is deliberate and must be preserved: monitor depth is tracked within a single method body only, so cross-method synchronization yields false negatives, and MONITOREXIT on exception-handler edges may undercount depth. False negatives are acceptable here; a false positive is not, because the scanner runs without executing tests and has no way to confirm a site. + +## Scoped Rules Index +Detailed per-element guardrails live in scoped rule files that load automatically when you open the matching source file. Consult the referenced file before modifying an element: + +- `se.deversity.asynctest.analysis.StaticPinningScanner` → `.gemini/rules/async-test-instrumentation.md` + + +# AUTO-GENERATED AI RULES +# Generated by VibeTags | https://github.com/PIsberg/vibetags +# Do not edit manually. + +## LOCKED FILES (DO NOT EDIT) +- `se.deversity.asynctest.DetectorType`: Each enum constant requires synchronized changes in five places: (1) @AsyncTest attribute, (2) AsyncTestConfig field, (3) AsyncTestConfig.Builder default, (4) both branches of AsyncTestConfig.build() (detectAll block + excludes block), and (5) DetectorRegistry constructor. Adding a value here in isolation breaks the system. + +## 🛡️ MANDATORY SECURITY AUDITS +When proposing edits or writing code for the following files, you MUST perform a security review before outputting the final code. You must explicitly state in your response that you have audited the changes for the required vulnerabilities. + +File: `se.deversity.asynctest.AsyncTestContext` +Critical Vulnerabilities to Prevent: +- Thread Safety issues + +File: `se.deversity.asynctest.runner.ConcurrencyRunner` +Critical Vulnerabilities to Prevent: +- Thread Safety issues +- Resource Leaks + + +## 🚫 IGNORED ELEMENTS (EXCLUDE FROM CONTEXT) +Do not reference, suggest changes to, or include the following in completions or answers. + +- `se.deversity.asynctest.NoopAsyncTestListener` + +## 🧠 CORE FUNCTIONALITY (CHANGE WITH EXTREME CAUTION) +The following elements are well-tested core components. Make changes with extreme caution. + +- `se.deversity.asynctest.AsyncTestConfig`: Sensitivity: Critical. Note: Adding a new detector requires synchronized changes across six places: @AsyncTest attribute, AsyncTestConfig field, Builder default, from(AsyncTest) call chain, build() detectAll/excludes blocks, and DetectorRegistry constructor. +- `se.deversity.asynctest.AsyncTestContext`: Sensitivity: Critical. Note: ThreadLocal install/uninstall must always be symmetric. A leak propagates stale detector state across test invocations and causes false positives or missed detections. +- `se.deversity.asynctest.extension.AsyncTestInvocationInterceptor`: Sensitivity: Critical. Note: invocation.skip() is intentional — ConcurrencyRunner owns the full N×M execution and must never call invocation.proceed(). Restoring proceed() would run the test body once outside the CyclicBarrier, bypassing all detectors. +- `se.deversity.asynctest.runner.ConcurrencyRunner`: Sensitivity: Critical. Note: Core stress-test execution engine. The CyclicBarrier pattern forces maximum thread contention. Timeout logic and AsyncTestContext install/uninstall are carefully calibrated — subtle changes introduce flaky tests or missed detector activations. + +## 🔐 SECURITY-CRITICAL CODE +The following elements are security-critical. AI must not weaken security properties. Any change must be reviewed for security impact. + +- `se.deversity.asynctest.diagnostics.SharedMessageDigestDetector`: Security-critical code [cryptography (hash integrity / MAC / signature state)]. Do not weaken security properties. Flag any change for security review. +- `se.deversity.asynctest.diagnostics.SharedSecureRandomDetector`: Security-critical code [cryptography (RNG quality)]. Do not weaken security properties. Flag any change for security review. +- `se.deversity.asynctest.diagnostics.SharedStatefulCryptoDetector`: Security-critical code [cryptography (confidentiality / integrity / authenticity state)]. Do not weaken security properties. Flag any change for security review. +- `se.deversity.asynctest.runner.LicenseGuard`: Security-critical code [authorization]. Do not weaken security properties. Flag any change for security review. + +## Scoped Rules Index +Detailed per-element guardrails live in scoped rule files that load automatically when you open the matching source file. Consult the referenced file before modifying an element: + +- `se.deversity.asynctest.AsyncAssert` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.AsyncTest` → `.gemini/rules/async-test-configuration.md` +- `se.deversity.asynctest.AsyncTestConfig` → `.gemini/rules/async-test-configuration.md` +- `se.deversity.asynctest.AsyncTestContext` → `.gemini/rules/async-test-runtime-core.md` +- `se.deversity.asynctest.AsyncTestListener` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.AsyncTestListenerRegistry` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.AsyncTestListenerRegistry.Registration` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.DetectorRegistry` → `.gemini/rules/async-test-configuration.md` +- `se.deversity.asynctest.DetectorType` → `.gemini/rules/async-test-configuration.md` +- `se.deversity.asynctest.NoopAsyncTestListener` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.Preset` → `.gemini/rules/async-test-configuration.md` +- `se.deversity.asynctest.benchmark.BenchmarkComparator` → `.gemini/rules/async-test-instrumentation.md` +- `se.deversity.asynctest.benchmark.BenchmarkRecorder` → `.gemini/rules/async-test-instrumentation.md` +- `se.deversity.asynctest.diagnostics.CompletableFutureBlockingCallbackDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.CompletableFutureObtrudeDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.DaemonThreadHygieneDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.FileChannelPositionRaceDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.FinalFieldMutationDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.HighContentionAtomicDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.JdbcConnectionSharedDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.LazyConstantMisuseDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.LockUpgradeDeadlockDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.NonAtomicConcurrentMapUpdateDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.NotifyWithoutMonitorDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedByteBufferDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedCharsetCoderDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedChecksumDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedDeflaterDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedIteratorDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedJsonMapperReconfigDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedKdfDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedMessageDigestDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedSecureRandomDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SharedStatefulCryptoDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SiteCapture` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SiteCapture.Site` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.SpuriousWakeupDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.ThisEscapeDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.ThreadLocalRandomMisuseDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.TryLockMisuseDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.diagnostics.WeakHashMapSharedDetector` → `.gemini/rules/async-test-detectors.md` +- `se.deversity.asynctest.extension.AsyncTestExtension` → `.gemini/rules/async-test-runtime-core.md` +- `se.deversity.asynctest.extension.AsyncTestInvocationInterceptor` → `.gemini/rules/async-test-runtime-core.md` +- `se.deversity.asynctest.report.Formatter` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.report.JUnitXmlReportListener` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.report.JsonFormatter` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.report.JsonReportListener` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.report.MarkdownFormatter` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.report.Violation` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.runner.ConcurrencyRunner` → `.gemini/rules/async-test-runtime-core.md` +- `se.deversity.asynctest.runner.LicenseGuard` → `.gemini/rules/async-test-runtime-core.md` +- `se.deversity.asynctest.spi.Detector` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.spi.DetectorFactory` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.spi.DetectorRegistry` → `.gemini/rules/async-test-public-api.md` +- `se.deversity.asynctest.spi.adapters.LegacyDetectorAdapter` → `.gemini/rules/async-test-public-api.md` + + diff --git a/pom.xml b/pom.xml index e2976ec9..ab6dce0e 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 3.2.8 2.9.3 0.11.0 - 1.0.0-RC8 + 1.0.0-RC10 3.6.0 13.9.0 4.10.3.0