test(e2e): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@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): add SeedClient — seed resources by writing canonical documents to etcd - #750

Merged
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client
Jul 11, 2026
Merged

test(e2e): add SeedClient — seed resources by writing canonical documents to etcd#750
moonming merged 1 commit into
mainfrom
feat/e2e-seed-client

Conversation

@moonming

@moonmingmoonming commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

  • New harness client SeedClient: seeds provider_keys / models / api_keys / observability_exporters by writing the canonical resource document (the schemas/resources/ shapes) straight to etcd under <prefix>/<kind>/<id>, instead of POSTing to the Admin API. The interface mirrors AdminClient's create methods — {id, value} return, generated id, the same provider/adapter defaulting — so call sites can migrate mechanically (admin.createModel({...})seed.createModel({...})).
  • New characterization case seed-vs-admin-characterization-e2e.test.ts, pinning the equivalence the migration relies on through two lenses:
    • Behavior — chat completes through fully seed-created resources, and allowed_models authz rejects a seeded caller on a non-allowed model with 403, exactly like an admin-created key. The positive probes double as propagation gates for both front doors.
    • Shape — read back through the store's serde round-trip (admin GET), a seeded sparse document is field-identical to the admin-created one, modulo identity fields and cross-references; identity fields themselves are pinned byte-exact. api_keys are additionally compared as raw stored bytes (via a new EtcdClient.get), because their GET view is a public projection that omits attribution fields — without the raw lens, handler-side enrichment of the stored document could hide behind the projection.

Why

Writing documents directly is the same front door the control plane uses in managed mode, so cases seeded this way exercise the production write path rather than the Admin API, which only standalone deployments use for writes. The pattern already existed in the harness for resources the Admin API doesn't expose (rate_limit_policies in team-member-ratelimit-e2e); this generalizes it so any case can seed without the Admin API in the write path.

Follow-ups land separately: the mechanical sweep of existing cases' seeding, then a wait-condition audit.

Verification

  • New case: 2/2 green, including the raw-bytes lens with optional fields (rate_limit, expires_at) carried on both sides.
  • Full local suite against etcd v3.5.15 + redis:7-alpine and a freshly built debug binary: 133 files / 269 tests passed (~220s), re-run after review fixes.
  • tsc --noEmit: zero errors in the added files (the 5 pre-existing TS18048 warnings in untouched cases remain untouched).

Review notes

An independent review of the first push was applied before re-push: the api_keys shape assertion was made non-vacuous (raw-bytes comparison + optional-field fixtures — its GET view is a projection), the round-trip comment now states precisely which lens covers what (store serde round-trip vs the loader's additional JSON-Schema validation, covered by the behavioral probes), and identity fields are pinned byte-exact. One known limitation kept as-is by design: the characterization covers the four AdminClient kinds; other kinds get their own coverage when their seeding migrates.

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moonming, you've reached your PR review limit, so we couldn't start this review.

Next review available in:37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 877a0348-e1ae-41c8-a632-9d7be69845ca

📥 Commits

Reviewing files that changed from the base of the PR and between fa31f8a and 38b2676.

📒 Files selected for processing (4)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/etcd.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts
📝 Walkthrough

Walkthrough

Adds a SeedClient for direct etcd resource creation and a characterization suite comparing seeded resources with Admin API-created resources through runtime requests and Admin GET round-trips.

Changes

Seed versus Admin characterization

Layer / File(s)Summary
Direct etcd seeding client
tests/e2e/src/harness/seed.ts
Adds SeedClient methods for writing models, API keys, provider keys, and observability exporters to etcd with generated ids.
Characterization test bootstrap
tests/e2e/src/harness/index.ts, tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Exports SeedClient and initializes parallel seeded and Admin-created resource sets alongside the app and upstream services.
Runtime and round-trip comparisons
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
Verifies chat readiness, authorization boundaries, and equality of seeded and Admin-created resources after Admin GET round-trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant SeedClient
participant AdminClient
participant etcd
participant AISIXApp
participant OpenAIClients
SeedClient->>etcd: Write resource documents
AdminClient->>AISIXApp: Create resources
AISIXApp->>etcd: Persist Admin resources
OpenAIClients->>AISIXApp: Send chat requests
AISIXApp-->>OpenAIClients: Return completion or 403
AdminClient->>AISIXApp: GET stored resources
AISIXApp->>etcd: Read resource documents
AISIXApp-->>AdminClient: Return resource entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR does not implement the linked CI fix for #39; it adds an e2e SeedClient and characterization test instead.Rename AISIX_REDIS_URL to CACHE_TEST_REDIS_URL in the Redis test, CI workflow, docs, and related comments as requested by #39.
Out of Scope Changes check⚠️ WarningThe SeedClient and characterization test are unrelated to the linked #39 env-var rename and appear out of scope.Limit this PR to the #39 Redis env-var rename and move the SeedClient/test work to a separate PR.
✅ Passed checks (4 passed)
Check nameStatusExplanation
E2e Test Quality Review✅ PassedThe suite exercises real etcd/admin/proxy/upstream paths, has clear propagation gates and readable assertions, and the new SeedClient matches the AdminClient surface cleanly.
Security Check✅ PassedNo new logging, authz bypass, or secret-handling regression: SeedClient is test-only, mirrors AdminClient shapes, and writes canonical docs only.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly describes the main change: adding a SeedClient that seeds resources by writing canonical documents to etcd.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-seed-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts (1)

192-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated fetch-find-normalize-compare pattern.

The four comparison blocks follow an identical pattern. A helper would reduce duplication and make the test's intent clearer.

♻️ Optional helper extraction
 type Entry = { id: string; value: Record<string, unknown> };
+async function assertRoundTripEqual(+ admin: AdminClient,+ path: string,+ seedEntry: Entry,+ adminEntry: Entry,+ varied: string[],+ label: string,+) {+ const entries = await admin.json<Entry[]>("GET", path);+ const seedVal = normalize(find(entries, seedEntry.id, `seed ${label}`).value, varied);+ const adminVal = normalize(find(entries, adminEntry.id, `admin ${label}`).value, varied);+ expect(seedVal).toEqual(adminVal);+}+
describe("seed-vs-admin characterization: direct etcd writes ≡ Admin API writes", () => {

Then the test body becomes:

- const pks = await admin.json<Entry[]>("GET", "/admin/v1/provider_keys");- expect(- normalize(find(pks, seedPk.id, "seed provider_key").value, ["display_name"]),- ).toEqual(- normalize(find(pks, adminPk.id, "admin provider_key").value, ["display_name"]),- );-- const models = await admin.json<Entry[]>("GET", "/admin/v1/models");- expect(- normalize(find(models, seedModel.id, "seed model").value, [- "display_name",- "provider_key_id",- ]),- ).toEqual(- normalize(find(models, adminModel.id, "admin model").value, [- "display_name",- "provider_key_id",- ]),- );-- const keys = await admin.json<Entry[]>("GET", "/admin/v1/apikeys");- expect(- normalize(find(keys, seedKey.id, "seed api_key").value, [- "key_hash",- "allowed_models",- ]),- ).toEqual(- normalize(find(keys, adminKey.id, "admin api_key").value, [- "key_hash",- "allowed_models",- ]),- );-- const exporters = await admin.json<Entry[]>("GET", "/admin/v1/observability_exporters");- expect(- normalize(find(exporters, seedExporter.id, "seed exporter").value, ["name"]),- ).toEqual(- normalize(find(exporters, adminExporter.id, "admin exporter").value, ["name"]),- );+ await assertRoundTripEqual(admin, "/admin/v1/provider_keys", seedPk, adminPk, ["display_name"], "provider_key");+ await assertRoundTripEqual(admin, "/admin/v1/models", seedModel, adminModel, ["display_name", "provider_key_id"], "model");+ await assertRoundTripEqual(admin, "/admin/v1/apikeys", seedKey, adminKey, ["key_hash", "allowed_models"], "api_key");+ await assertRoundTripEqual(admin, "/admin/v1/observability_exporters", seedExporter, adminExporter, ["name"], "exporter");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts` around lines
192 - 230, Extract the repeated fetch, lookup, normalization, and equality
assertion into a reusable helper near the test setup. Have the helper accept the
endpoint, seed and admin IDs, descriptive labels, and fields to ignore, then use
it for provider keys, models, API keys, and observability exporters in place of
the duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts`:
- Around line 192-230: Extract the repeated fetch, lookup, normalization, and
equality assertion into a reusable helper near the test setup. Have the helper
accept the endpoint, seed and admin IDs, descriptive labels, and fields to
ignore, then use it for provider keys, models, API keys, and observability
exporters in place of the duplicated blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc470314-7dba-469b-b81d-b036602e86ae

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddd6ad and fa31f8a.

📒 Files selected for processing (3)
  • tests/e2e/src/cases/seed-vs-admin-characterization-e2e.test.ts
  • tests/e2e/src/harness/index.ts
  • tests/e2e/src/harness/seed.ts

…ents to etcd
Add a harness client that seeds provider_keys / models / api_keys /
observability_exporters by writing the canonical resource document
straight to etcd under `<prefix>/<kind>/<id>` — the same write path the
control plane uses in managed mode — instead of going through the Admin
API. The interface mirrors AdminClient's create methods ({id, value}
return, generated id, same provider/adapter defaulting), so existing
call sites can migrate mechanically.
A characterization case pins the equivalence this relies on, through
both lenses:
- behavior: chat succeeds through fully seed-created resources, and
allowed_models authz rejects a seeded caller on a non-allowed model
with 403, exactly like admin-created keys;
- shape: after the store's serde round-trip (admin GET), a seeded
sparse document reads back identical to the admin-created one, field
for field, modulo identity fields and cross-references; api_keys are
additionally compared as raw stored bytes, because their GET view is
a public projection that omits attribution fields; identity fields
are pinned byte-exact.
The direct-write pattern already existed in the harness for resources
the Admin API doesn't expose (rate_limit_policies); this generalizes it
so any case can seed without the Admin API in the write path.
EtcdClient gains a single-key `get` to support the raw-bytes lens.
@moonming

Copy link
Copy Markdown
MemberAuthor

Applied the independent review of the first push (force-pushed 38b2676):

  • api_keys shape assertion was vacuous — its GET view is a public projection that omits attribution fields, and with the varied fields stripped the comparison reduced to {} == {}. Fixed: the pair now carries optional fields (rate_limit, expires_at) so the projection lens has residue, and the raw stored bytes are compared via a new EtcdClient.get — handler-side enrichment of the stored document can no longer hide behind the projection.
  • Round-trip comment overstated its lens — admin GET is the store's serde round-trip only; the proxy loader additionally applies JSON-Schema validation (covered by the behavioral 200-probes for traffic-bearing kinds). Comment and the find() error hint now say exactly that.
  • Identity fields pinned byte-exact (display_name), so a hypothetical canonicalization can't hide behind normalize().
  • Wording in comments/PR body tightened to plain architecture terms.

Kept as-is by design: the propagation probe's opaque-timeout behavior follows the existing harness convention (waitConfigPropagation), and the characterization intentionally covers the four AdminClient kinds — other kinds get their own coverage when their seeding migrates.

Re-verified after the fixes: new case 2/2, full local suite 133 files / 269 tests green.

@moonming
moonmingforce-pushed the feat/e2e-seed-client branch from fa31f8a to 38b2676CompareJuly 10, 2026 09:28
@moonming
moonming merged commit db4a7eb into mainJul 11, 2026
11 checks passed
@moonming
moonming deleted the feat/e2e-seed-client branch July 11, 2026 12:00
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.

1 participant

@moonming