test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151) - #161

Merged
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings
May 9, 2026
Merged

test(e2e): C7 /v1/embeddings dispatch + response passthrough (#151)#161
moonming merged 2 commits into
mainfrom
test/e2e-c7-embeddings

Conversation

@moonming

@moonmingmoonming commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

First endpoint covered from #151's C7 row. Embeddings is one of the two most-used LLM API surfaces (every RAG / semantic-search app hits it heavily). Prior to this PR the gateway had zero e2e coverage on `/v1/embeddings`.

This PR ships one of the two originally-planned cases. The single-string input case surfaced a real product bug while the audit ran — held back as #162, same pattern as #153 / #154 / #159:

What's pinned

CaseUser journeyAsserts
Array input`client.embeddings.create({input: [s1, s2, s3]})`N embeddings returned in the SAME ORDER as input; each `data[i].object: "embedding"`, `data[i].index === i`, vector equals upstream's emitted vector at the same index; usage counts byte-for-byte; gateway hits `/v1/embeddings` exactly once with `Bearer sk-mock` and translated `model_name`; full input array reached upstream verbatim

Why this matters

The two real-world failure modes that prior coverage couldn't catch:

  • A regression that mis-routed embeddings through `/v1/chat/completions` would crash differently against real upstreams (chat schema rejects `input`) but not show up without a path assertion.
  • A regression that re-ordered or deduplicated the input array would silently corrupt every RAG caller's `{document: vector}` map. The strict per-index assertion catches this.

Source-blind discipline

Every assertion derives from external contracts:

No internal Rust paths or struct field names referenced.

Independent audit

Per CLAUDE.md §8, an independent audit agent reviewed the initial commit (87cb959). Resolution log:

FindingSeverityResolution
Loose "accept either input shape" assertion masked the gateway's own docs-vs-behavior contract divergence (`docs/api-proxy.md` §4.4 says "both pass through")HIGHFiled #162; removed the single-string case from this PR; held back to be added once product + docs are aligned (fb784db)
Probe was "doesn't throw"; could pass on a half-propagated 200-with-malformed-bodyMEDIUMProbe now shape-checks `object: "list"` + `data` array (fb784db)
Per-element `object: "embedding"` only asserted on first elementLOWAll three elements now pinned (fb784db)

The HIGH finding was the most important: my initial PR loosened a wire-shape assertion to "accept either string or single-element array" with the rationale "OpenAI's spec accepts both". The audit checked `docs/api-proxy.md` §4.4 — which I had not — and found the gateway's own published contract is narrower than the OpenAI spec ("both pass through"). The loose assertion was masking a real product-vs-docs gap. Per principle #2 ("test failure = code bug"), the strict assertion was correct and my loosening was incorrect.

Test plan

  • `npm test` (full e2e suite) — 28/28 passing locally (was 27)
  • No mock-data-only paths: each case still exercises the real `aisix` binary, real etcd config propagation, and real OpenAI Node SDK `client.embeddings.create()` reverse-call
  • CI green

Refs #151.

First endpoint covered from #151 C7. Embeddings is one of the two
most-used LLM API surfaces (every RAG / semantic-search app hits
it heavily). Prior to this file the gateway had **zero** e2e
coverage on /v1/embeddings.
Two user journeys pinned:
1. Single-string input — `client.embeddings.create({input: "..."})`
returns one embedding vector matching the upstream's exact
output, with usage counts byte-for-byte preserved.
2. Array input — `client.embeddings.create({input: [s1, s2, s3]})`
returns N embeddings in the SAME ORDER as the input array.
A regression that re-ordered, deduplicated, or batched-out-
of-order would silently corrupt every batched embedding
caller's index→vector mapping.
Each case also pins the upstream-side wire shape:
- Gateway hits `/v1/embeddings` exactly once (path + method)
- `Authorization: Bearer sk-mock` header reaches upstream
- Body is OpenAI-shape with display name → upstream model_name
translation
- Caller's input semantically reaches upstream
Note on input-shape normalisation: the OpenAI Embeddings API
accepts `input` as EITHER a string OR an array of strings. The
gateway is observed to normalise single-string into single-element
array on the upstream wire — which is spec-compliant per
<https://platform.openai.com/docs/api-reference/embeddings/create>.
The single-string test accepts either shape (`"hello"` or
`["hello"]`) — a test that pinned only one form would over-
specify against an implementation choice the spec leaves open.
References:
- OpenAI Embeddings API spec
<https://platform.openai.com/docs/api-reference/embeddings/create>
- OpenAI Node SDK embeddings client source
Refs #151
CopilotAI review requested due to automatic review settings May 9, 2026 13:20
@coderabbitai

coderabbitaiBot commented May 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@moonming has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf10957f-515e-425a-bb4d-833a629785c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee7642 and fb784db.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/embeddings-e2e.test.ts

Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds source-blind end-to-end coverage for the gateway’s OpenAI-compatible /v1/embeddings endpoint, exercising real SDK calls against a real spawned aisix process + etcd-propagated config, while pinning upstream dispatch behavior and response passthrough.

Changes:

  • Introduces a new e2e test suite covering embeddings for both single-string and arrayinput shapes via the official openai Node SDK.
  • Asserts strict passthrough for embedding vectors + usage counters, plus upstream dispatch invariants (path, auth header, model translation, and input preservation / ordering).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Audit (per CLAUDE.md §8) found one HIGH and two MEDIUM:
HIGH H1 (the "loose accept either input shape" assertion masked a
gateway-vs-docs contract divergence):
The audit checked `docs/api-proxy.md` §4.4 — which I had not —
and found the gateway's own published contract reads "input may
be a single string or an array; **both pass through**." The
gateway today normalises single-string `input` to single-element
array on the upstream wire, contradicting its own docs. My
loosening to "either shape OK because OpenAI's spec accepts
both" was wrong: spec-compliance ≠ scenario invalidation when
the gateway has published a narrower contract of its own.
Per principle #2 ("test failure = code bug"), the strict
assertion was correct and the loosening was masking a real
product-vs-docs gap. Resolution per the same pattern as #153 /
#154 / #159:
- Removed the single-string case from this PR (it would fail
against today's gateway behavior).
- Filed #162 — `/v1/embeddings` single-string normalisation
contradicts docs §4.4 — with the strict-assertion test
attached as the regression repro.
- Single-string case will be added back to the suite once
product + docs are aligned (either A: fix the gateway to
preserve shape, or B: update docs to document the
normalisation, then test pins the documented shape strictly).
MEDIUM M1 (probe shape-check):
Tightened the array-input readiness probe from "doesn't throw"
to "returns an OpenAI-shape `{object: 'list', data: [...]}`
body". A regression that returned 200 with a malformed body
would no longer falsely report ready. Matches the discipline
in anthropic-upstream-e2e.test.ts.
LOW L1 (per-element `object: "embedding"` asserts):
The array test now pins `data[i].object === "embedding"` for
all three elements, not just the first. Symmetrises with the
OpenAI Embeddings spec (each data element carries the literal).
Refs #151
@moonming
moonming merged commit 2d0f188 into mainMay 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-c7-embeddings branch May 9, 2026 13:35
moonming added a commit that referenced this pull request May 9, 2026
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
moonming added a commit that referenced this pull request May 9, 2026
… (#169)
* test(harness): raise waitConfigPropagation budget 5s → 10s (#157)
Closes#157 (test-infra concern, not a product bug).
Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.
Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.
The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.
Fix: raise the deadline to 10s. This:
- Eliminates the recurring rerun churn on feature PRs
- Preserves the "fail loudly on a genuinely stuck snapshot"
property — 10s is still a generous floor; a real bug where
propagation hangs indefinitely would still fail clearly
- Doesn't change the happy-path latency at all (the helper
polls every 50ms and returns as soon as the condition is
met — bumping the deadline only affects the sad path)
This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.
If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
* test(harness): also reduce maxForks 4→2 (#157 fallback escalation)
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.
Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.
Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".
The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.
Combined this PR now does:
1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
2. maxForks 4 → 2 (`vitest.config.ts`)
The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@moonming