Skip to content

feat(appkit): add testing kit - #530

Merged
IamGalymzhan merged 27 commits into
mainfrom
feat/testing-kit
Aug 20, 2026
Merged

feat(appkit): add testing kit#530
IamGalymzhan merged 27 commits into
mainfrom
feat/testing-kit

Conversation

@IamGalymzhan

@IamGalymzhan IamGalymzhan commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Testing Kit

  1. Made PluginContext telemetry injectable — the one production edit. It now takes an optional { telemetry }, defaulting to the real provider for testing.
  2. Created the @databricks/appkit/testing package — moved the fixtures out of tools/test-helpers.ts into packages/appkit/src/testing/
  3. Added the two headline helpers — createTestPluginContext() (wraps the real PluginContext with fake tool providers) and expectStream(...).toEmit(...).
  4. Shipped it to users + added docs page (docs/development/testing.md), a template example test.
  5. Added new tests for analytics and genie plugins using this kit

The testing kit needs to construct a real PluginContext without a live
OpenTelemetry pipeline. Add an optional constructor dependency for the
telemetry provider, defaulting to the shared "plugin-context" provider so
the production path is unchanged. This is the single production edit
required to wrap the real class in tests rather than reimplementing it.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Wire the testing kit as a published subpath and prove it against the first
of the two hand-rolled context stubs (the design gate):

- Add ./testing to both exports maps (dev + publishConfig) following the
  ./type-generator shape, add src/testing/index.ts to the tsdown entry, and
  declare vitest as an optional peerDependency. Build passes attw + publint;
  dist/testing/{index,mock-plugin-context,expect-stream,fixtures}.{js,d.ts}
  are emitted and vitest stays external to the main entry.
- Migrate dispatch-tool-call.test.ts: replace (plugin as any).context =
  { executeTool } with mockPluginContext. executeTool is now the REAL method,
  so the forwarded toolCallTimeoutMs is asserted through actual signal
  composition, the on-behalf-of (asUser) path is verified, and a new test
  proves the forwarded timeout actually aborts a slow toolkit tool end-to-end.

This is the primary win from the plan: executeTool's OBO and timeout paths
gain real assertions instead of a stub that proved nothing.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
…Context

Replace the second and final hand-rolled stub — (plugin as any).context =
{ addRoute } — with the real PluginContext from mockPluginContext. The kit's
route recorder captures raw handlers, so the alias assertion (both
/invocations and /responses mount the same handler reference) holds against
the real class, where forwardAsyncErrors wrapping would otherwise break
reference identity.

Both context stubs the plan identified are now migrated.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
- Add docs/docs/development/testing.md covering mockPluginContext(),
  expectStream(), and the fixture helpers, with a full end-to-end example.
  Cross-links to local-development, custom-plugins, and execution-context.
- Add template/server/example.test.ts: a self-contained, plugin-agnostic
  example that scaffolded apps ship with — it defines a tiny custom plugin
  and exercises both mockPluginContext (route recording) and expectStream
  (ordered event assertions), running with no workspace or network.

Ships the kit to users, satisfying the plan's acceptance criteria that a
docs page exists and the template carries at least one example test.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Validation by scaffolding a real app with `databricks apps init` surfaced
that the examples called the `analytics()`/`toPlugin()` factory and then
treated the result as a plugin instance — but a factory returns a
{ plugin, config, name } descriptor for createApp to construct, so
`.attachContext`/handler methods are absent.

Rewrite both the template example test and the docs "Full example" to
instantiate the plugin class directly (`new GreeterPlugin({})`), matching how
the migrated agents suites use the kit. The scaffolded app's `npm test` and
`tsc` both pass against the published `@databricks/appkit/testing` subpath
with no workspace or network.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
…pe error

Drop `undefined` from the static FakeToolValue union. `resolve()` treats an
undefined map entry as "unregistered tool" and throws, so allowing undefined
as a declared response made `{ query: undefined }` a confusing runtime error
instead of a compile error. A function returning undefined still works for the
rare "returns nothing" case. Add a test pinning that a null response is
returned as a value, not misread as a missing tool.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
…ting kit

The plan's step 5 was to MOVE the fixtures into the package, not copy them.
The shipped kit (src/testing/fixtures.ts) duplicated all 15 exports of
tools/test-helpers.ts, which would drift over time. Collapse the original
into a thin re-export of @databricks/appkit/testing so src/testing is the
single source of truth while the 18 existing @tools/test-helpers importers
keep working unchanged.

The re-exported mockServiceContext is now synchronous; every call site either
awaits it (no-op on a non-promise) or reads it through
Awaited<ReturnType<...>>, so all suites pass unchanged (full appkit suite:
3117 passed, 1 pre-existing skip).

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
…ing docs

Code review follow-ups:

- expectStream's parseSSEBody split frames on \n\n, so a spec-compliant SSE
  stream delimited by \r\n\r\n (from a real server) collapsed into one event.
  AppKit's own writer uses \n\n so existing tests were unaffected, but
  expectStream is public API that accepts any Response. Normalize CRLF to LF
  before splitting; add a CRLF regression test.
- Docs: instantiate the plugin CLASS in the attach() snippet (the factory
  returns a descriptor, not an instance), and note that the cache attach()
  seeds is a per-process singleton shared by tests within a file.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 873 KB (+33 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 892 KB (+23 KB) 312 KB (+8.6 KB)
Type declarations 340 KB (+25 KB) 118 KB (+9.1 KB)
Source maps 1.7 MB (+57 KB) 586 KB (+20 KB)
Other 11 KB 3.7 KB
Total 3.0 MB (+106 KB) 1020 KB (+38 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 88 KB (+15 B) 2.5 KB 91 KB (+15 B) external 288 KB (+46 B)
./beta 49 KB 457 B 49 KB external 143 KB
./testing 17 KB 0 B 17 KB external 50 KB
./type-generator 21 KB 0 B 21 KB external 61 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 84 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 33 KB
./beta stream-manager.js initial 5.8 KB
./beta wide-event-emitter.js initial 3.2 KB
./beta databricks.js initial 3.0 KB
./beta configuration.js initial 2.1 KB
./beta service-context.js initial 1.3 KB
./beta client.js initial 434 B
./beta client-options.js initial 220 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 142 B
./beta index.js lazy 123 B
./testing index.js initial 17 KB
./type-generator index.js initial 21 KB

@databricks/appkit-ui

npm tarball (packed): 342 KB (-291 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 390 KB 130 KB (+1 B)
Type declarations 228 KB 83 KB
Source maps 752 KB (-334 B) 247 KB (-197 B)
CSS 16 KB (-462 B) 3.2 KB (-90 B)
Total 1.4 MB (-796 B) 464 KB (-286 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 432 KB (+127 B) 49 KB 480 KB (+127 B) 1.3 MB 175 KB
./react/beta 1.0 KB 0 B 1.0 KB 0 B 1.9 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 20 B
./react index.js initial 430 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 1.0 KB

@IamGalymzhan IamGalymzhan changed the title Feat/testing kit feat(test-kit) Aug 11, 2026
@IamGalymzhan IamGalymzhan changed the title feat(test-kit) feat(appkit): add testing kit Aug 11, 2026
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 32364946014 -R databricks/appkit -n appkit-template-0.61.1-pr.7a2cd6c-feat-testing-kit-530 -D appkit-pr-530 \
  && unzip -o "appkit-pr-530/appkit-template-0.61.1-pr.7a2cd6c-feat-testing-kit-530.zip" -d "appkit-pr-530" \
  && databricks apps init --template "appkit-pr-530"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

IamGalymzhan and others added 7 commits August 11, 2026 11:28
CI's "Lint & Type Check" job runs `pnpm run check` over the whole repo, so a
pre-existing lint error unrelated to this branch failed the build:

- remote-tunnel-controller.test.ts had two `afterEach` hooks in one describe
  (lint/suspicious/noDuplicateTestHooks, error severity). Merge them into one —
  behavior preserved (env reset + console-spy clear both still run after each
  test). This file is byte-identical to main; the error predated the branch and
  only surfaced because CI lints the entire tree.

Also drop two dead `biome-ignore lint/suspicious/noExplicitAny` suppressions in
the testing kit (fixtures.ts, expect-stream.test.ts): `noExplicitAny` is turned
off repo-wide in biome.json, so the comments had no effect (suppressions/unused
warnings). The invalid-source test now casts through `unknown as never`.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Verified and fixed the findings from an independent code review:

- #1 (correctness) expectStream dropped the wire `event:` name when the JSON
  payload carried its own `type` (spread ran after the assignment). Spread the
  payload first, then set `type = name ?? parsed.type`, so a frame like
  `event: error` + `data: {"type":"result"}` reports `error`. Regression test added.
- #2 (contract) `@databricks/appkit/testing` eagerly loads vitest via fixtures
  even for `expectStream`, so vitest is a real requirement. Drop the "optional"
  peerDependenciesMeta and correct the docs sentence.
- #6 (OBO fidelity) the fake `asUser` recorded `asUser: true` unconditionally.
  Enforce the real `Plugin.asUser` token precondition: a request without
  `x-forwarded-access-token` throws `missingToken` (missing user id throws too),
  and the resolved `userId` is recorded on each tool call. Tests now assert both
  directions (well-formed request vs token-less).
- #3 (fidelity) attach() now mirrors AppKit core: registerPlugin plus
  registerToolProvider for real tool providers, without clobbering injected
  fakes. getPlugins()/getPluginNames()/hasPlugin() behave as in production.
- #12 unknown-tool lookup used `tools[name] === undefined`, so a tool named
  "constructor"/"toString" hit Object.prototype. Use Object.hasOwn.
- #5 drop data-less named SSE frames (real clients ignore them).
- #7 re-export the PluginContext type from the testing barrel so
  MockPluginContext.ctx is nameable through the exports map.
- #13 correct the docs: mock.telemetry captures the context's executeTool spans,
  not plugin-level spans (attachContext rebuilds the plugin's own telemetry).
- #4 parseSSEResponse now delegates to the same parseSSEBody as expectStream —
  one parser, no divergence. All 3 analytics.integration call sites still pass.
- #8 reformat template/server/example.test.ts with the template's Prettier so a
  scaffolded app's `npm run format` passes.
- #10 fix the package-doc @example (agentsPlugin._handleStream does not exist).
- #11 add kit tests that exercise attach() end-to-end (cache seed, isReady,
  registration, fake-not-clobbered).

Build passes attw + publint; full appkit suite 3125 passed / 1 pre-existing skip.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
… dep

With vitest declared as a (non-optional) peerDependency, knip recognizes it as
used, so the earlier ignoreDependencies entry is unnecessary. This reverts
knip.json to its original state.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
A required peerDependency has no per-subpath scope: it applied to the whole
@databricks/appkit package, so every production consumer that never imports
the testing kit got an unsatisfied peer (npm 7+ auto-installs vitest into
their tree; pnpm warns) — a wider blast radius than the eager-import bug it
was meant to fix.

Follow appkit's own precedent instead: `vite` backs the ./type-generator
subpath as a normal `dependency`, installed for everyone but loaded only by
importers of that subpath. Do the same for `vitest` and ./testing. vitest is
referenced solely by dist/testing/fixtures.js, never by the main/plugin/core
entry, so a consumer importing createApp never loads it.

Verified end-to-end: scaffolded an app whose own vitest (4.1.9) differs in
major from appkit's dependency (3.2.4), forcing a nested second copy. The
testing kit's vi.fn()/vi.spyOn() mocks and expect(...).toHaveBeenCalled()
assertions work across the two instances (vi spies carry their own call
state), and npm install emits no peer-dep warning. Build passes attw + publint.
Also fold in the template example's Prettier formatting (template uses Prettier,
not Biome) so a scaffolded app's `npm run format` passes.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
The helper builds the REAL PluginContext with faked edges — it does not mock
the context — so the name was misleading. Rename to createTestPluginContext
(and the MockPluginContext type to TestPluginContext), matching the
create*-for-tests convention, and rename the files to test-plugin-context.ts.
Pre-merge and unreleased, so no external consumers are affected.

Also finish the #13 doc-accuracy fix in the shipped JSDoc (not just the docs
page): the telemetry field comment now states it captures the context's spans
(executeTool), not plugin-internal spans — attachContext rebuilds the plugin's
this.telemetry from the real TelemetryManager. These comments ship in
dist/testing/*.d.ts, so IntelliSense previously showed the unqualified claim.

Build passes attw + publint; full appkit suite 3125 passed / 1 pre-existing skip.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Behavior-preserving cleanups in the testing kit:

- createMockRequest reuses createMockWorkspaceClient() instead of an inline
  copy of the same mock client (verified identical).
- createMockServiceContext / createMockUserContext / mockServiceContext inline
  the createMockWorkspaceClient() call into the `||` fallback, so the mock
  client is built only when the caller did not supply one.
- The fake asUser view spreads `...base` and overrides executeAgentTool rather
  than re-declaring getAgentTools.
- expectStream's isSubsequence breaks once the expected sequence is fully
  matched.

No semantic change; typecheck clean and all kit + migrated tests pass.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
@IamGalymzhan
IamGalymzhan marked this pull request as ready for review August 12, 2026 12:40
@IamGalymzhan
IamGalymzhan requested a review from a team as a code owner August 12, 2026 12:40
- #1 (P1) The docs called vitest a peer dependency, but the manifest ships it
  under `dependencies` (the decision we landed on, matching how appkit ships
  `vite` for ./type-generator). Correct the docs to match: appkit installs
  vitest for you, and it loads only when you import ./testing. Manifest and
  docs now agree.
- #2 (P2) expectStream buffered the source eagerly with no bound, so a
  non-terminating stream hung until the runner's own timeout. Add an optional
  `{ timeout }` that fails fast with a clear, kit-specific error; document it
  and cover both directions with tests.
- #3 (P2) The fake asUser replicates asUser's token precondition but not the
  real dev-mode `DEV_OBO_FALLBACK_KEY` OTel marker (a module-private telemetry
  detail). Narrow the docs and JSDoc to say so and point users at the recorded
  asUser/userId fields instead of isDevOboFallback().

Build passes attw + publint; full appkit suite 3141 passed / 1 pre-existing skip.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Exercise @databricks/appkit/testing against real core plugins to validate it
beyond the two agent proof sites and produce usage references:

- analytics.kit.test.ts: cross-plugin executeTool via createTestPluginContext —
  OBO identity (asUser/userId), token-precondition rejection, and per-call
  timeout abort. Needs only the kit (no workspace/ServiceContext).
- genie.kit.test.ts: drives the real _handleSendMessage SSE stream and asserts
  event order with expectStream(...).toEmit(...).

Both add genuinely new coverage (streamed SSE order + OBO dispatch identity were
untested). Full appkit suite 3145 passed / 1 pre-existing skip.

Developer-experience notes (kit wins + friction, e.g. createMockResponse doesn't
compose with expectStream) captured in internal/ for the milestone review.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>

@pkosiec pkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Testing kit - review

I tried this kit two ways: read it closely, then used it to write real tests for the analytics and genie plugins. Short version: I'd adopt it. Wrapping the real PluginContext and faking only the edges (telemetry, tool providers, routes) gives you things a hand-rolled { executeTool } stub can't - real timeout composition, the real asUser token check, real route recording. attach() and OBO both worked cleanly in a fresh test.

One gap stood out in real use, and a few smaller things are worth a look. Details are in the inline comments; here's the map:

Biggest lever

  • The kit tests generators well, but you can't assert a plugin's real SSE route - createMockResponse throws the written bytes away. Fixing this is the single change that makes plugin testing feel complete. (see comment on fixtures.ts)

Worth deciding before wide use

  • vitest as a plain runtime dependency ships a test framework into every production install. An optional peer avoids that. (see package.json)

Convenience

  • An obo option on createMockRequest (+ show it in the docs example).
  • A resetTestCache() so the documented cache advice is actually followable.

Polish

  • The shipped example test uses an any-cast instead of the documented attach().
  • Two service-context helpers differ only by a create prefix.
  • A useServiceContextMock() hook could remove the repeated beforeEach/afterEach block.

Comment thread packages/appkit/src/testing/fixtures.ts Outdated
Comment thread packages/appkit/package.json Outdated
Comment thread packages/appkit/src/testing/fixtures.ts
Comment thread docs/docs/development/testing.md Outdated
Comment thread packages/appkit/src/plugins/agents/tests/dispatch-tool-call.test.ts Outdated
Comment thread packages/appkit/src/testing/fixtures.ts Outdated
Comment thread packages/appkit/src/testing/test-plugin-context.ts
Comment thread packages/appkit/src/testing/fixtures.ts
Comment thread docs/docs/plugins/testing.md

@pkosiec pkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 2 - the new dogfood tests

Thanks for adding these. Both pass, and the analytics one adds real coverage that was missing (cross-plugin executeTool OBO identity).

Two asks:

  1. Don't keep them as separate *.kit.test.ts files. The testing kit should be the default way to test a plugin, not a parallel track. Fold these into analytics.test.ts / genie.test.ts and drop the .kit split. For genie, that also means merging with the existing (presence-only) SSE test into one ordered assertion on the real event names.
  2. Trim the comments. Both files carry a lot of explanatory narration that reads as slop; a /deslop pass when merging would cut it to what's needed.

Bonus signal: these files independently re-hit round 1 - a hand-rolled capturing response (C1), a re-invented service-context mock (C8), and hand-rolled OBO headers (C3). Two authors needing the same workarounds is the strongest case yet to land those kit ergonomics.

Comment thread packages/appkit/src/plugins/analytics/tests/analytics.kit.test.ts Outdated
Comment thread packages/appkit/src/plugins/genie/tests/genie.kit.test.ts Outdated
Comment thread packages/appkit/src/plugins/genie/tests/genie.kit.test.ts Outdated
Comment thread packages/appkit/src/plugins/genie/tests/genie.kit.test.ts Outdated
Comment thread packages/appkit/src/plugins/genie/tests/genie.kit.test.ts Outdated
Comment thread packages/appkit/src/plugins/analytics/tests/analytics.kit.test.ts Outdated
Resolve the eight review comments on the testing kit:

- createMockResponse now captures written SSE bytes and exposes
  sseResponse(); expectStream reads a captured mock response directly, so
  streaming-route tests no longer need a hand-rolled bridge.
- Ship vitest as an optional peer dependency (+ devDependency) instead of a
  plain runtime dependency, keeping the test framework out of production
  installs and deduping to the app's own copy. Ignore it in knip.
- Add an obo option to createMockRequest so on-behalf-of tests set the
  forwarded identity headers with one flag.
- Add resetTestCache() to clear the shared cache singleton between tests.
- Use the documented attach() instead of an any-cast in the agents
  dispatch tests.
- Drop the unused createMockServiceContext/createMockUserContext builders
  from the public surface; keep the service-context builder internal.
- Pin the previously untested edges: the Object.hasOwn tool-lookup guard,
  the dev-mode asUser branch, and parseSSEBody's non-object data values.
- Add useServiceContextMock() to register the mock lifecycle in one line,
  returning a live accessor.

Dogfood the new helpers in the analytics, genie, and serving suites, and
document them in the testing guide.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
The testing kit is entirely plugin-scoped (createTestPluginContext,
attach(plugin), plugin route/tool/SSE assertions), and the page's own
cross-links already pointed into plugins/. Move it next to custom-plugins
and fix the relative links. Keep the heading as 'Testing'; the Plugins
section supplies the context.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Address round-2 review: the kit should be the default way to test a
plugin, not a parallel '*.kit.test.ts' track.

- Fold the three cross-plugin executeTool OBO tests into analytics.test.ts
  and delete analytics.kit.test.ts.
- Upgrade genie.test.ts's SSE test to assert event ORDER via
  expectStream on genie's real event names (message_start, status,
  message_result, query_result), replacing brittle write.mock.calls
  substring checks, and delete genie.kit.test.ts.
- Trim the heavy comment narration from the folded-in tests.
- Re-export createTestPluginContext and expectStream from the test-helpers
  shim.
- Finish the testing-guide move under plugins/ (sidebar position + links).

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
The dogfood fold trimmed expect(mock.toolCalls).toHaveLength(1), so a
double-dispatch would no longer fail the happy-path test — and it was
inconsistent with the token-less sibling that kept toHaveLength(0).
Restore it.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
The toEmit swap pinned event order but dropped the payload values the old
substring checks covered (conversationId=new-conv-id, status=ASKING_AI),
which aren't asserted elsewhere. Restore them structurally via collect() +
toMatchObject — keeping the ordering guarantee without brittle substrings.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
…equest

createMockRequest returned userWorkspaceClient, serviceWorkspaceClient,
getWarehouseId and getWorkspaceId — fields no production code reads
(plugins resolve those through getWorkspaceClient()/getWarehouseId() from
src/context, which mockServiceContext stands in for). Publishing them via
@databricks/appkit/testing would make four inert fields a permanent public
promise.

The two warehouse cold-start tests (analytics + metric) overrode
mockReq.serviceWorkspaceClient.warehouses.get, which the route never reads
— so they passed on the default RUNNING client without exercising the
warehouse path at all. Route the warehouse client through
mockServiceContext (the real seam) so the tests are live, and drop the
'mock WorkspaceClient' claim from the testing guide.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
@IamGalymzhan

Copy link
Copy Markdown
Collaborator Author

@pkosiec All comments are fixed

Brings feat/testing-kit up to v0.60.0. Three conflicts, all resolved in favour
of this branch:

- tools/test-helpers.ts — main only reformatted the old implementation; this
  branch replaced it with a re-export shim over packages/appkit/src/testing.
- agents/tests/route-handler-errors.test.ts and dispatch-tool-call.test.ts —
  this branch migrated both onto createTestPluginContext, a superset of main's
  raw-stub versions (dispatch-tool-call keeps an extra timeout-abort test).

Main migrated Biome -> oxlint+oxfmt, so this commit also reconciles the branch
with the new toolchain: the 64 dead biome-ignore comments are dropped from the
two conflicted test files, and the shipped testing-kit sources are reformatted
under oxfmt's import grouping.

Ignore **/.claude in knip, oxlint, and oxfmt. Agent worktrees live under
.claude/worktrees/, so every tool was analysing a second full copy of the repo:
knip reported hundreds of phantom unused exports and failed the pre-commit
hook outright, and a repo-root `oxfmt` would have rewritten another branch's
working tree.

Co-authored-by: Isaac
Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
Both were swept into the oxlint merge from a dirty working tree and corrected
later on the branch; folding those corrections in here keeps them out of the
follow-up PR.

The knip `.claude/**` entry and the `**/.claude` ignorePatterns in oxfmt/oxlint
were never needed — nothing in the repo lints or formats that directory. The
`packages/appkit` vitest ignoreDependencies entry stays: vitest is a real
dependency of the testing entry.

apps/dev-playground/client/package-lock.json is restored to origin/main
byte-for-byte; npm had run in that directory and pruned its `extraneous: true`
entries.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
@IamGalymzhan

Copy link
Copy Markdown
Collaborator Author

Split into a stack. This PR is now scoped to the testing-kit foundation only — the five items in the description above — at 22 files, +2753/−627.

The follow-up work (createTestApp, the never-crash mock WorkspaceClient, app.close(), re-bootable singletons) moved to #540, which is based on this branch. Review this one first; #540 will retarget to main automatically when it merges.

@pkosiec pkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, great work! Please address one comment before merge.

Comment thread packages/appkit/src/testing/fixtures.ts Outdated
`createMockRequest` stored header keys exactly as given while `header()`
lowercased the lookup, so a mixed-case override was unreachable:

  createMockRequest({ obo: { userId: "alice" }, headers: { "X-Forwarded-User": "bob" } });
  // header("x-forwarded-user") === "alice"

Both keys were kept — ["x-forwarded-access-token", "x-forwarded-user",
"X-Forwarded-User"] — and the lowercase one obo seeded still answered, which
contradicted the "an explicit override wins" contract documented right above it.

Keys are now lowercased on the way in, matching what Node's parser hands
Express. Thanks @pkosiec.

The existing override test passed because it used a lowercase key, so it is now
parametrised over both casings, and the case-insensitivity test additionally
pins that every stored key is lowercase. Reverting the fix fails both.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
IamGalymzhan added a commit that referenced this pull request Aug 20, 2026
Same root cause as the `createMockRequest` fix on the parent branch (#530,
found by @pkosiec), and worse here. `Object.assign(headers, reqOptions.headers)`
kept case variants as separate keys, and `Headers` **comma-joins** duplicates
rather than replacing them:

  new Headers({ "x-forwarded-user": "alice", "X-Forwarded-User": "bob" })
  // -> x-forwarded-user: "alice, bob"

So a mixed-case override did not merely lose, it corrupted the value the server
received — the "caller headers last" contract broken in a way that produces a
plausible-looking string rather than an error.

Keys are lowercased before assignment. The existing `/headers` table gained a
mixed-case row rather than a new test; reverting the fix fails it.

Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
@IamGalymzhan
IamGalymzhan merged commit 4ea97a4 into main Aug 20, 2026
10 checks passed
@IamGalymzhan
IamGalymzhan deleted the feat/testing-kit branch August 20, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants