Skip to content

Commit 6258b0c

Browse files
authored
test: add published SDK parity E2E (#70)
Generated SDK now owns public API behavior, but runtime regressions can still pass unit tests while diverging from latest npm release. - compares candidate package with npm `latest` across all 16 public operations - validates request paths and bodies, JSON parsing, SSE streams, binary responses, async APIs, browser sessions, embeddings, search, chat, and responses - keeps published SDK as test-only npm alias - runs typed `node:test` suites directly with Node 26 strip-types - runs mock parity in dedicated PR-safe workflow - runs live chat contract parity in separate functional workflow with `PPLX_API_TOKEN` - marks both Bazel targets `manual`, keeping non-hermetic tests out of `//...`
1 parent 1680837 commit 6258b0c

7 files changed

Lines changed: 708 additions & 0 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: SDK E2E
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: sdk-e2e-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
e2e:
19+
timeout-minutes: 15
20+
runs-on: ubuntu-24.04
21+
steps:
22+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
24+
- name: Set up Bazel
25+
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86
26+
with:
27+
bazelisk-cache: true
28+
disk-cache: ${{ github.workflow }}-e2e
29+
repository-cache: true
30+
31+
- name: Set up Node
32+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
33+
with:
34+
node-version: '26'
35+
36+
- name: Enable pnpm
37+
run: corepack enable pnpm
38+
39+
- name: Install dependencies
40+
run: pnpm install --frozen-lockfile
41+
42+
- name: Compare candidate with published SDK
43+
run: bazel test //e2e:sdk_parity
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Functional tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
schedule:
8+
- cron: '17 9 * * 1'
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: functional-tests-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
functional-tests:
20+
if: github.ref == 'refs/heads/main'
21+
timeout-minutes: 15
22+
runs-on: ubuntu-24.04
23+
steps:
24+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
26+
- name: Set up Bazel
27+
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86
28+
with:
29+
bazelisk-cache: true
30+
disk-cache: ${{ github.workflow }}-functional-tests
31+
repository-cache: true
32+
33+
- name: Set up Node
34+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
35+
with:
36+
node-version: '26'
37+
38+
- name: Enable pnpm
39+
run: corepack enable pnpm
40+
41+
- name: Install dependencies
42+
run: pnpm install --frozen-lockfile
43+
44+
- name: Refresh published SDK
45+
run: pnpm update published-sdk --latest
46+
47+
- name: Run functional tests
48+
env:
49+
PPLX_API_TOKEN: ${{ secrets.PPLX_API_TOKEN }}
50+
run: bazel test //e2e:live --test_env=PPLX_API_TOKEN

e2e/BUILD.bazel

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
load("@aspect_rules_js//js:defs.bzl", "js_test")
2+
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
3+
load("//:ts.bzl", "ts_library")
4+
load("//:tsconfig.bzl", "BASE_COMPILER_OPTIONS")
5+
6+
package(default_visibility = ["//visibility:private"])
7+
8+
js_test(
9+
name = "sdk_parity",
10+
data = [
11+
":typecheck",
12+
"//:node_modules/@perplexity-ai/perplexity_ai",
13+
"//:node_modules/published-sdk",
14+
],
15+
entry_point = "sdk-parity.ts",
16+
tags = ["manual"],
17+
)
18+
19+
js_test(
20+
name = "live",
21+
data = [
22+
":typecheck",
23+
"//:node_modules/@perplexity-ai/perplexity_ai",
24+
"//:node_modules/published-sdk",
25+
],
26+
entry_point = "live.ts",
27+
tags = ["manual"],
28+
)
29+
30+
ts_project(
31+
name = "typecheck",
32+
srcs = [
33+
"live.ts",
34+
"sdk-parity.ts",
35+
],
36+
no_emit = True,
37+
tags = ["manual"],
38+
tsc = "//:tsc",
39+
tsconfig = {
40+
"compilerOptions": dict(
41+
BASE_COMPILER_OPTIONS,
42+
module = "ESNext",
43+
moduleResolution = "Bundler",
44+
types = ["node"],
45+
),
46+
},
47+
deps = [
48+
"//:node_modules/@perplexity-ai/perplexity_ai",
49+
"//:node_modules/@types/node",
50+
"//:node_modules/published-sdk",
51+
],
52+
)
53+
54+
ts_library(
55+
name = "e2e",
56+
srcs = [
57+
"live.ts",
58+
"sdk-parity.ts",
59+
],
60+
tsconfig_types = ["node"],
61+
visibility = ["//visibility:public"],
62+
deps = [
63+
"//:node_modules/@types/node",
64+
"//:node_modules/published-sdk",
65+
],
66+
)

e2e/live.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
// oxlint-disable-next-line no-restricted-imports -- Verify packaged public entrypoint.
4+
import { Perplexity as Candidate } from '@perplexity-ai/perplexity_ai';
5+
import { Perplexity as Published } from 'published-sdk';
6+
7+
interface CompletionResponse {
8+
choices: Array<{
9+
index: number;
10+
message: {
11+
content: string;
12+
role: string;
13+
};
14+
[key: string]: unknown;
15+
}>;
16+
id: string;
17+
model: string;
18+
[key: string]: unknown;
19+
}
20+
21+
function completionContract(response: unknown) {
22+
assert.ok(typeof response === 'object' && response !== null);
23+
const completion = response as CompletionResponse;
24+
25+
assert.equal(typeof completion.id, 'string');
26+
assert.equal(typeof completion.model, 'string');
27+
assert.ok(Array.isArray(completion.choices));
28+
assert.ok(completion.choices.length > 0);
29+
30+
const choice = completion.choices[0];
31+
assert.ok(choice);
32+
assert.equal(typeof choice.index, 'number');
33+
assert.equal(typeof choice.message?.content, 'string');
34+
assert.equal(choice.message?.role, 'assistant');
35+
36+
return {
37+
choiceKeys: Object.keys(choice).sort(),
38+
messageKeys: Object.keys(choice.message).sort(),
39+
responseKeys: Object.keys(completion).sort(),
40+
};
41+
}
42+
43+
const request = {
44+
max_tokens: 16,
45+
messages: [{ content: 'Reply with only the word pong.', role: 'user' as const }],
46+
model: 'sonar',
47+
temperature: 0,
48+
};
49+
50+
describe('live API parity', () => {
51+
it('matches published chat completion response contract', async () => {
52+
const apiKey = process.env['PPLX_API_TOKEN'];
53+
assert.ok(apiKey, 'PPLX_API_TOKEN must be set');
54+
55+
const [candidateResponse, publishedResponse] = await Promise.all([
56+
new Candidate({ apiKey, maxRetries: 0 }).chat.completions.create(request),
57+
new Published({ apiKey, maxRetries: 0 }).chat.completions.create(request),
58+
]);
59+
60+
assert.deepStrictEqual(completionContract(candidateResponse), completionContract(publishedResponse));
61+
});
62+
});

0 commit comments

Comments
 (0)