From 2caed63efab6808bcb43897de2c0090aa3a5a658 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 15:43:28 -0700 Subject: [PATCH 1/2] test: move harness and live tests into e2e/ tests/ and live/ become e2e/, run by bun run test:e2e in CI; the live round trip still skips unless EMBEDDING_E2E_BASE_URL is set. Wire and bearer-header cases move from src/embed.test.ts to e2e/wire.test.ts. The empty-input and batchSize-schema units are dropped. --- .github/workflows/ci.yml | 1 + CONTRIBUTING.md | 18 +++++-- IMPLEMENTATION.md | 20 ++++---- {tests => e2e}/base64.test.ts | 0 live/embeddings.test.ts => e2e/live.test.ts | 0 {tests => e2e}/retry.test.ts | 0 e2e/wire.test.ts | 57 +++++++++++++++++++++ package.json | 4 +- src/embed.test.ts | 53 ------------------- tsconfig.json | 2 +- 10 files changed, 86 insertions(+), 69 deletions(-) rename {tests => e2e}/base64.test.ts (100%) rename live/embeddings.test.ts => e2e/live.test.ts (100%) rename {tests => e2e}/retry.test.ts (100%) create mode 100644 e2e/wire.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0b19db..c86c1d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - run: bunx tsc --noEmit - run: bun run lint - run: bun run test + - run: bun run test:e2e - run: bunx prettier --check . - run: bun run build - name: Import packed dist with Node diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eeb13b5..014a475 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,14 +7,17 @@ bun install bun run typecheck bun run lint bun run test +bun run test:e2e ``` -`bun run test` is hermetic. `bun run test:live` runs a `/v1/embeddings` round -trip and skips unless `EMBEDDING_E2E_BASE_URL` is set; point it at a remote -server rather than loading models locally: +`bun run test` runs the colocated units in `src/`. `bun run test:e2e` runs +`e2e/`: retry, wire and base64 cases on the `@intx/inference-testing` harness +and a local HTTP stub, plus a `/v1/embeddings` round trip that skips unless +`EMBEDDING_E2E_BASE_URL` is set; point it at a remote server rather than +loading models locally: ```bash -EMBEDDING_E2E_BASE_URL=http://100.113.184.123:11434/v1 bun run test:live +EMBEDDING_E2E_BASE_URL=http://100.113.184.123:11434/v1 bun run test:e2e ``` `EMBEDDING_E2E_MODEL` defaults to `nomic-embed-text`. @@ -22,3 +25,10 @@ EMBEDDING_E2E_BASE_URL=http://100.113.184.123:11434/v1 bun run test:live ## Versioning Semver. Releases run `bun run build && npm publish` with green CI. + +## Commit messages + +Commit subjects and PR titles follow [Conventional Commits](https://www.conventionalcommits.org): `feat`, `fix`, `refactor`, `test`, `docs`, `build`, `ci`, `perf`, and `chore(release): x.y.z` for releases. +Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`. +Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs. +Every PR links its issue with a `Closes ` line in the PR body. diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 49b28e6..674f667 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -101,14 +101,16 @@ package depends only on the published surface). ## Tests -`bun run test` runs `bun test ./src ./tests`. +`bun run test` runs `bun test ./src`; `bun run test:e2e` runs `bun test ./e2e`. +Both run in CI. - `src/embed.test.ts` — units on `@intx/inference-testing` fakes: index - placement across batches, short reply, base64 decode. -- `tests/retry.test.ts` — 429 retried once after `Retry-After` on the - harness clock; 401 rejects without retry. -- `tests/base64.test.ts` — `base64` and `float` agree over a real HTTP stub. -- `live/embeddings.test.ts` (`bun run test:live`, not in `test` or CI) — - round trip against a real `/v1/embeddings` (`EMBEDDING_E2E_BASE_URL`, - required; `EMBEDDING_E2E_MODEL`, default `nomic-embed-text`). - Skips when unset or unreachable. + placement across batches, short and duplicate replies, base64 decode, + non-JSON 200. +- `e2e/retry.test.ts` — 429 retried after `Retry-After` (seconds, HTTP-date, + custom extractor) on the harness clock; 401 rejects without retry; timeout. +- `e2e/wire.test.ts` — request URL, body fields and bearer header. +- `e2e/base64.test.ts` — `base64` and `float` agree over a real HTTP stub. +- `e2e/live.test.ts` — round trip against a real `/v1/embeddings` + (`EMBEDDING_E2E_BASE_URL`, required; `EMBEDDING_E2E_MODEL`, default + `nomic-embed-text`). Skips when unset or unreachable. diff --git a/tests/base64.test.ts b/e2e/base64.test.ts similarity index 100% rename from tests/base64.test.ts rename to e2e/base64.test.ts diff --git a/live/embeddings.test.ts b/e2e/live.test.ts similarity index 100% rename from live/embeddings.test.ts rename to e2e/live.test.ts diff --git a/tests/retry.test.ts b/e2e/retry.test.ts similarity index 100% rename from tests/retry.test.ts rename to e2e/retry.test.ts diff --git a/e2e/wire.test.ts b/e2e/wire.test.ts new file mode 100644 index 0000000..6a3ef77 --- /dev/null +++ b/e2e/wire.test.ts @@ -0,0 +1,57 @@ +import { afterEach, expect, test } from "bun:test"; +import { setupHarness, type Harness } from "@intx/inference-testing"; + +import { embedTexts, type EmbedConfig } from "../src/index"; + +const CONFIG: EmbedConfig = { + baseURL: "https://embed.example/v1", + model: "text-embedding-3-small", +}; + +let harness: Harness | undefined; +afterEach(() => { + harness?.dispose(); + harness = undefined; +}); + +async function run(h: Harness, pending: Promise): Promise { + await h.run(); + return await pending; +} + +test("sends only configured fields to {baseURL}/embeddings", async () => { + harness = setupHarness(); + const stream = harness.scenario.createStream(); + harness.scenario.whenRequestBodyMatches( + (body, req) => + req.url === "https://embed.example/v1/embeddings" && + !req.headers.has("authorization") && + body === JSON.stringify({ model: CONFIG.model, input: ["a"] }), + stream, + ); + stream.enqueueAll( + [new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')], + { startAt: 1 }, + ); + const pending = embedTexts(["a"], CONFIG, { deps: harness.deps }); + expect(await run(harness, pending)).toEqual([[1]]); +}); + +test("sends a bearer token only when apiKey is set", async () => { + harness = setupHarness(); + const stream = harness.scenario.createStream(); + harness.scenario.whenRequestMatches( + (req) => req.headers.get("authorization") === "Bearer sk-test", + stream, + ); + stream.enqueueAll( + [new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')], + { startAt: 1 }, + ); + const pending = embedTexts( + ["a"], + { ...CONFIG, apiKey: "sk-test" }, + { deps: harness.deps }, + ); + expect(await run(harness, pending)).toEqual([[1]]); +}); diff --git a/package.json b/package.json index 63cf4ff..b6a0348 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "prepack": "bun run build", "typecheck": "tsc --noEmit", "lint": "eslint .", - "test": "bun test ./src ./tests", - "test:live": "bun test ./live", + "test": "bun test ./src", + "test:e2e": "bun test ./e2e", "format": "prettier -w ." }, "dependencies": { diff --git a/src/embed.test.ts b/src/embed.test.ts index 9d9aea3..236a4a3 100644 --- a/src/embed.test.ts +++ b/src/embed.test.ts @@ -115,56 +115,3 @@ test("classifies a 200 that is not JSON instead of leaking a SyntaxError", async reason: { category: "protocol_mismatch" }, }); }); - -test("sends only configured fields to {baseURL}/embeddings", async () => { - harness = setupHarness(); - const stream = harness.scenario.createStream(); - harness.scenario.whenRequestBodyMatches( - (body, req) => - req.url === "https://embed.example/v1/embeddings" && - !req.headers.has("authorization") && - body === JSON.stringify({ model: CONFIG.model, input: ["a"] }), - stream, - ); - stream.enqueueAll( - [new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')], - { startAt: 1 }, - ); - const pending = embedTexts(["a"], CONFIG, { deps: harness.deps }); - expect(await run(harness, pending)).toEqual([[1]]); -}); - -test("sends a bearer token only when apiKey is set", async () => { - harness = setupHarness(); - const stream = harness.scenario.createStream(); - harness.scenario.whenRequestMatches( - (req) => req.headers.get("authorization") === "Bearer sk-test", - stream, - ); - stream.enqueueAll( - [new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')], - { startAt: 1 }, - ); - const pending = embedTexts( - ["a"], - { ...CONFIG, apiKey: "sk-test" }, - { deps: harness.deps }, - ); - expect(await run(harness, pending)).toEqual([[1]]); -}); - -test("returns [] for empty input without a request", async () => { - harness = setupHarness(); - const pending = embedTexts([], CONFIG, { deps: harness.deps }); - expect(await run(harness, pending)).toEqual([]); -}); - -test("rejects a non-positive batchSize before any request", async () => { - harness = setupHarness(); - for (const batchSize of [0, -1, 1.5]) { - await expect( - embedTexts(["a"], { ...CONFIG, batchSize }, { deps: harness.deps }), - ).rejects.toThrow(/batchSize/); - } - await harness.run(); -}); diff --git a/tsconfig.json b/tsconfig.json index 713b1f6..f64f20d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "lib": ["ESNext"] }, - "include": ["src/**/*.ts", "tests/**/*.ts", "live/**/*.ts"] + "include": ["src/**/*.ts", "e2e/**/*.ts"] } From 98e14ec6fa990e15378335a9d87f14f7a8613a50 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 08:13:59 -0700 Subject: [PATCH 2/2] chore(release): 0.2.0 Breaking changes: - @intx/inference and @intx/types (^0.4.0) are peer dependencies; install them alongside. - ModelRequestError is now EmbeddingRequestError; match on err.reason (InferenceError). - runJSONRequest, extractRetryAfterMs and RunRequestOptions are no longer exported. - Malformed replies throw EmbeddingRequestError instead of a plain Error. - An invalid config (e.g. batchSize) throws arktype's TraversalError instead of RangeError. EmbedConfig and wire behavior are unchanged, so existing configs keep working. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6a0348..1299cc6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@corbits/embedding", "description": "Embedding client for OpenAI-compatible /v1/embeddings endpoints, built on the Interchange inference primitives", - "version": "0.1.0", + "version": "0.2.0", "license": "LGPL-2.1-only", "author": "Sawyer Cutler ", "type": "module",