diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9acfb84c..c4ab1700 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,6 +8,31 @@ on: - main jobs: + # Key-free mocked critical-path smoke gate (connect -> upsert -> query). + # Runs on every PR with NO PINECONE_API_KEY to prove the request/response + # plumbing works without a real backend. Keyed integration tests stay below. + smoke-mocked: + name: Smoke (mocked, no key) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: 8.5 + + # No PINECONE_API_KEY: proves the mocked critical path needs none. + # The smokeTest task throws GradleException if zero tests execute, + # so an accidentally emptied suite fails the job rather than passing silently. + - name: Run mocked critical-path smoke test + run: gradle smokeTest + build: strategy: fail-fast: false diff --git a/build.gradle b/build.gradle index 1c055651..29390add 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,14 @@ sourceSets { } resources.srcDir file('src/integration/resources') } + + smokeTest { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/smoke/java') + } + } } def grpcVersion = '1.60.2' @@ -122,6 +130,8 @@ tasks.register('generateJavadoc', Javadoc) { configurations { integrationTestImplementation.extendsFrom testImplementation integrationTestRuntimeOnly.extendsFrom testRuntimeOnly + smokeTestImplementation.extendsFrom testImplementation + smokeTestRuntimeOnly.extendsFrom testRuntimeOnly } java { @@ -151,6 +161,21 @@ task integrationTest(type: Test) { outputs.upToDateWhen { false } } +// Key-free mocked critical-path smoke gate (connect -> upsert -> query). +// Runs with no PINECONE_API_KEY; in-process gRPC + MockWebServer, no real backend. +task smokeTest(type: Test) { + useJUnitPlatform() + testClassesDirs = sourceSets.smokeTest.output.classesDirs + classpath = sourceSets.smokeTest.runtimeClasspath + outputs.upToDateWhen { false } + // Fail if zero tests run (silently passing empty suite must not happen). + afterSuite { desc, result -> + if (!desc.parent && result.testCount == 0) { + throw new GradleException("No smoke tests were executed — the suite may have been emptied or moved.") + } + } +} + // Configure Shadow JAR with relocations and transformers import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer diff --git a/src/smoke/README.md b/src/smoke/README.md new file mode 100644 index 00000000..bb419c1c --- /dev/null +++ b/src/smoke/README.md @@ -0,0 +1,45 @@ +# Smoke tests + +## Mocked critical-path gate (`MockedCriticalPathTest.java`) + +A key-free smoke gate that runs the critical **connect → upsert → query** path +against fully in-process mocks. It runs on **every pull request** (the +`Smoke (mocked, no key)` job in `.github/workflows/pr.yml`) with no +`PINECONE_API_KEY`, guarding the request/response plumbing against regressions +before any keyed suite runs. + +### What is mocked + +Mocks are injected at the transport layer, not at the SDK's public API: + +- **Control plane** (`describeIndex`) — REST (OkHttp); mocked with OkHttp + `MockWebServer` pointed at via `Pinecone.Builder.withHost(...)`. +- **Data plane** (`upsert` / `query`) — gRPC (Netty); mocked with an in-process + `io.grpc.Server` from `grpc-testing` implementing `VectorServiceGrpc.VectorServiceImplBase`. + The channel is injected via `PineconeConfig.setCustomManagedChannel`, then + passed directly to `PineconeConnection` + `Index`. + +Everything above the wire — config resolution, request building, proto +marshaling, response deserialization — is the real code path. This is the Java +analogue of the Python SDK's respx-backed gate +(`tests/smoke/test_mocked_critical_path_*.py`), the TS SDK's fetchApi-injected +gate (`src/smoke/mockedCriticalPath.test.ts`), and the Go SDK's in-process +`grpc.Server` gate (`smoke/mocked_critical_path_test.go`). + +### Run locally + +```sh +./gradlew smokeTest +``` + +### Zero-collection guard + +The `smokeTest` Gradle task is configured to throw `GradleException` if zero +tests execute (`result.testCount == 0`), so an accidentally emptied suite fails +the job instead of passing silently. + +### Real (keyed) suites + +The keyed integration tests live in `src/integration/` and run in the +`integration-test` job with `PINECONE_API_KEY` from secrets. They are gated +separately from this key-free gate. diff --git a/src/smoke/java/io/pinecone/smoke/MockedCriticalPathTest.java b/src/smoke/java/io/pinecone/smoke/MockedCriticalPathTest.java new file mode 100644 index 00000000..c3b043e3 --- /dev/null +++ b/src/smoke/java/io/pinecone/smoke/MockedCriticalPathTest.java @@ -0,0 +1,246 @@ +package io.pinecone.smoke; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.pinecone.clients.Index; +import io.pinecone.clients.Pinecone; +import io.pinecone.commons.IndexInterface; +import io.pinecone.configs.PineconeConfig; +import io.pinecone.configs.PineconeConnection; +import io.pinecone.proto.QueryRequest; +import io.pinecone.proto.QueryResponse; +import io.pinecone.proto.ScoredVector; +import io.pinecone.proto.UpsertRequest; +import io.pinecone.proto.UpsertResponse; +import io.pinecone.proto.VectorServiceGrpc; +import io.pinecone.unsigned_indices_model.QueryResponseWithUnsignedIndices; +import io.pinecone.unsigned_indices_model.ScoredVectorWithUnsignedIndices; +import io.pinecone.unsigned_indices_model.VectorWithUnsignedIndices; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openapitools.db_control.client.model.IndexModel; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Mocked critical-path smoke test — connect → upsert → query. + * + *
The key-free half of the Java SDK's smoke-test coverage. Unlike the keyed suites in + * {@code src/integration} (which hit a real backend and require {@code PINECONE_API_KEY}) and the + * localServer suite (which requires the Dockerized mock index), this test stands up its own + * in-process mocks and runs on every pull request with no API key. + * + *
It guards the three most critical use cases against a regression in the request/response + * plumbing: + *
Mocks are injected at the transport layer, not at the SDK's public API: + *
Everything above the wire — config resolution, request building, proto marshaling, and
+ * response deserialization — is the real code path. This is the Java analogue of the Python
+ * SDK's respx-backed gate and the Go SDK's in-process {@code grpc.Server} gate.
+ */
+class MockedCriticalPathTest {
+
+ private static final String INDEX_NAME = "mocked-critical-path";
+ private static final String NAMESPACE = "smoke-ns";
+
+ private MockWebServer controlPlane;
+ private Server dataPlane;
+ private ManagedChannel dataChannel;
+ private MockVectorService mockSvc;
+ private String serverName;
+
+ // ---------------------------------------------------------------------------
+ // Mock data-plane service
+ // ---------------------------------------------------------------------------
+
+ /**
+ * In-process implementation of the data-plane gRPC service. Records every call so the
+ * test can assert on-wire correctness.
+ */
+ static class MockVectorService extends VectorServiceGrpc.VectorServiceImplBase {
+ final AtomicInteger upsertCalls = new AtomicInteger(0);
+ final AtomicInteger queryCalls = new AtomicInteger(0);
+ final AtomicReference