Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,32 @@
- 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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
strategy:
fail-fast: false
matrix:
Expand Down
25 changes: 25 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -122,6 +130,8 @@ tasks.register('generateJavadoc', Javadoc) {
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
smokeTestImplementation.extendsFrom testImplementation
smokeTestRuntimeOnly.extendsFrom testRuntimeOnly
}

java {
Expand Down Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions src/smoke/README.md
Original file line number Diff line number Diff line change
@@ -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.
246 changes: 246 additions & 0 deletions src/smoke/java/io/pinecone/smoke/MockedCriticalPathTest.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>It guards the three most critical use cases against a regression in the request/response
* plumbing:
* <ol>
* <li>connect — construct a client and resolve an index host via {@code describeIndex}</li>
* <li>upsert — write vectors to the data plane</li>
* <li>query — read them back by vector similarity</li>
* </ol>
*
* <p>Mocks are injected at the transport layer, not at the SDK's public API:
* <ul>
* <li>The <em>control plane</em> ({@code describeIndex}) is REST (OkHttp), mocked with
* OkHttp {@code MockWebServer} set as the client's base path.</li>
* <li>The <em>data plane</em> ({@code upsert} / {@code query}) is gRPC, mocked with an
* in-process {@code io.grpc.Server} from {@code grpc-testing} implementing
* {@code VectorServiceGrpc.VectorServiceImplBase}. The channel is injected via
* {@code PineconeConfig.setCustomManagedChannel}.</li>
* </ul>
*
* <p>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<UpsertRequest> lastUpsert = new AtomicReference<>();
final AtomicReference<QueryRequest> lastQuery = new AtomicReference<>();

@Override
public void upsert(UpsertRequest req, StreamObserver<UpsertResponse> resp) {
upsertCalls.incrementAndGet();
lastUpsert.set(req);
resp.onNext(UpsertResponse.newBuilder().setUpsertedCount(req.getVectorsCount()).build());
resp.onCompleted();
}

@Override
public void query(QueryRequest req, StreamObserver<QueryResponse> resp) {
queryCalls.incrementAndGet();
lastQuery.set(req);
resp.onNext(QueryResponse.newBuilder()
.addMatches(ScoredVector.newBuilder().setId("v1").setScore(0.99f).build())
.addMatches(ScoredVector.newBuilder().setId("v2").setScore(0.87f).build())
.build());
resp.onCompleted();
}
}

// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------

@BeforeEach
void setUp() throws IOException {
// Start the REST control-plane mock.
controlPlane = new MockWebServer();
controlPlane.start();

// Start the in-process gRPC data-plane mock.
serverName = InProcessServerBuilder.generateName();
mockSvc = new MockVectorService();
dataPlane = InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(mockSvc)
.build()
.start();

dataChannel = InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build();
}

@AfterEach
void tearDown() throws IOException {
controlPlane.shutdown();
dataPlane.shutdownNow();
dataChannel.shutdownNow();
}

// ---------------------------------------------------------------------------
// Helper: build describe-index JSON response
// ---------------------------------------------------------------------------

/**
* Returns a {@code MockResponse} shaped like a {@code GET /indexes/{name}} response.
* The {@code host} field is intentionally left as an opaque string because the
* in-process channel is obtained from {@code PineconeConfig.setCustomManagedChannel},
* which bypasses host-based dialing entirely.
*/
private MockResponse describeIndexResponse() {
String body = "{"
+ "\"name\":\"" + INDEX_NAME + "\","
+ "\"dimension\":3,"
+ "\"metric\":\"cosine\","
+ "\"host\":\"mocked-data-host.pinecone.io\","
+ "\"spec\":{\"serverless\":{"
+ "\"cloud\":\"gcp\",\"region\":\"us-east1\","
+ "\"read_capacity\":{\"mode\":\"OnDemand\",\"status\":{\"state\":\"Ready\"}}"
+ "}},"
+ "\"status\":{\"ready\":true,\"state\":\"Ready\"},"
+ "\"vector_type\":\"dense\""
+ "}";
return new MockResponse()
.setResponseCode(200)
.addHeader("Content-Type", "application/json")
.setBody(body);
}

// ---------------------------------------------------------------------------
// Test
// ---------------------------------------------------------------------------

/**
* Drives connect → upsert → query against fully in-process mocks with no
* {@code PINECONE_API_KEY}. The dummy API key below only satisfies the constructor;
* the mocks never validate it.
*/
@Test
void testConnectUpsertQueryAgainstMockedBackend() throws Exception {
// Queue the describe-index response so the control-plane call succeeds.
controlPlane.enqueue(describeIndexResponse());

// 1. connect: build the Pinecone client against the mocked control plane
// and resolve the data-plane host via describeIndex.
String controlPlaneUrl = controlPlane.url("").toString();
Pinecone pc = new Pinecone.Builder("mocked-key")
.withHost(controlPlaneUrl)
.build();

IndexModel idx = pc.describeIndex(INDEX_NAME);
assertEquals(INDEX_NAME, idx.getName(), "describeIndex should return our index name");
assertNotNull(idx.getHost(), "resolved host must be non-null");

// Verify the control-plane request was a GET to /indexes/{name}.
RecordedRequest ctrlReq = controlPlane.takeRequest();
assertEquals("GET", ctrlReq.getMethod());
assertTrue(ctrlReq.getPath().endsWith("/indexes/" + INDEX_NAME),
"control-plane path should end with /indexes/" + INDEX_NAME);

// 2. Build the data-plane Index directly with the in-process channel,
// bypassing host-based dialing so no real network is used.
PineconeConfig dataCfg = new PineconeConfig("mocked-key", null);
dataCfg.setCustomManagedChannel(dataChannel);
PineconeConnection connection = new PineconeConnection(dataCfg, INDEX_NAME);
Index index = new Index(dataCfg, connection, INDEX_NAME);

// 3. upsert: write vectors to the mocked data plane.
List<VectorWithUnsignedIndices> vectors = Arrays.asList(
IndexInterface.buildUpsertVectorWithUnsignedIndices("v1", Arrays.asList(0.1f, 0.2f, 0.3f), null, null, null),
IndexInterface.buildUpsertVectorWithUnsignedIndices("v2", Arrays.asList(0.4f, 0.5f, 0.6f), null, null, null),
IndexInterface.buildUpsertVectorWithUnsignedIndices("v3", Arrays.asList(0.7f, 0.8f, 0.9f), null, null, null));
UpsertResponse upsertResp = index.upsert(vectors, NAMESPACE);
assertEquals(3, upsertResp.getUpsertedCount(),
"mock should echo back the number of vectors upserted");

// 4. query: read them back by vector similarity.
QueryResponseWithUnsignedIndices queryResp = index.query(
2,
Arrays.asList(0.1f, 0.2f, 0.3f),
null, null, null, NAMESPACE, null,
false, false);
List<ScoredVectorWithUnsignedIndices> matches = queryResp.getMatchesList();
assertEquals(2, matches.size(), "mock returns two matches");
assertEquals("v1", matches.get(0).getId());
assertEquals(0.99f, matches.get(0).getScore(), 1e-6f);
assertEquals("v2", matches.get(1).getId());

// Verify every leg of the critical path was exercised at the transport level.
assertEquals(1, mockSvc.upsertCalls.get(), "upsert should hit the data plane exactly once");
assertEquals(1, mockSvc.queryCalls.get(), "query should hit the data plane exactly once");

UpsertRequest sentUpsert = mockSvc.lastUpsert.get();
assertEquals(NAMESPACE, sentUpsert.getNamespace(), "namespace must reach the wire for upsert");
assertEquals(3, sentUpsert.getVectorsCount(), "all three vectors must reach the wire");

QueryRequest sentQuery = mockSvc.lastQuery.get();
assertEquals(NAMESPACE, sentQuery.getNamespace(), "namespace must reach the wire for query");
assertEquals(2, sentQuery.getTopK(), "topK must reach the wire");
assertEquals(Arrays.asList(0.1f, 0.2f, 0.3f), sentQuery.getVectorList(),
"query vector must reach the wire");
}
}
Loading