Skip to content

Commit 8c173ca

Browse files
tombeckenhamclaude
andcommitted
ci: type-check examples/** and testing/** to catch call-site regressions (#820)
`test:pr`/`test:ci` excluded examples/** and testing/** from every target, so `test:types` never checked the apps where the library is actually consumed — call-site type regressions (e.g. a provider summarize adapter not assignable to `summarize()`) slipped through CI. Because those surfaces were never type-checked, they had also accumulated ~80 latent type errors. CI wiring: - test:pr / test:ci now run a second pass — `test:types` over examples/** and testing/** — after the existing packages-only run. Heavy targets (build/lib/...) stay excluded; only the cheap, high-value type check is added. - Add `test:types:examples` convenience script and document the gate in CLAUDE.md. Coverage: - Add a `test:types` target to every example/testing project that lacked one (e2e, panel, react-native-chat, react-native-smoke). ts-angular-chat type-checks templates via `ngc`, resolved from an existing @angular/build peer (no new dep, no lockfile change). Fixes (examples/testing drift from the current API; nothing under packages/**): - ts-react-chat, ts-solid-chat, ts-code-mode-web: MediaPrompt, maxTokens→modelOptions, ContentPart[] narrowing, transport XOR. - testing/e2e: drop poisoning `as never` model casts, fix adapter/tool typings, OpenRouter summarize httpClient header injection. - testing/panel: AnyAdapter factory maps, AG-UI EventType migration, remove dead app.config.ts (Vinxi-era), rewrite createEventRecording to emit AG-UI StreamChunks. Guard: - packages/ai-grok/tests/summarize-callsite-type-safety.test.ts asserts the grokSummarize → summarize() contract in an included package (@ts-expect-error tracks the known #821 options-shape bug; flips positive when #835 lands). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 33acdd4 commit 8c173ca

47 files changed

Lines changed: 665 additions & 477 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,16 @@ The single canonical command is:
230230
pnpm test:pr
231231
```
232232

233-
This runs the exact target set the `PR` workflow runs in CI (`nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**`).
233+
This runs the exact target set the `PR` workflow runs in CI: first `nx affected --targets=test:sherif,test:knip,test:docs,test:kiira,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**` (the heavy targets, packages only), then a second `nx affected --targets=test:types --projects=examples/**,testing/**` pass that **does** type-check the example apps and `testing/` packages. The example/testing surfaces are deliberately excluded from the heavy targets (build/lib/etc.) but included for `test:types`, because call-site type regressions often only manifest where the library is actually consumed (see issue #820). Use `pnpm test:types:examples` to run just that second pass locally.
234234

235235
If you can't run `test:pr` (e.g. it's too slow on your machine), at minimum run each of these and confirm they're green before pushing:
236236

237237
- `pnpm test:sherif` — workspace consistency
238238
- `pnpm test:knip` — unused dependencies
239239
- `pnpm test:docs` — doc link verification
240240
- `pnpm test:eslint` — lint
241-
- `pnpm test:types` — typecheck
241+
- `pnpm test:types` — typecheck (packages)
242+
- `pnpm test:types:examples` — typecheck the example apps + `testing/` packages
242243
- `pnpm test:lib` — unit tests
243244
- `pnpm test:build` — build artifact verification
244245
- `pnpm build` — build all affected packages

examples/ts-angular-chat/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "vite build",
9-
"preview": "vite preview"
9+
"preview": "vite preview",
10+
"test:types": "node scripts/typecheck.mjs"
1011
},
1112
"dependencies": {
1213
"@angular/common": "^21.2.0",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Type-check this Angular example, including template type-checking.
2+
//
3+
// Angular template type-checking (`strictTemplates`) requires the Angular
4+
// compiler (`ngc`), not plain `tsc`. `ngc` ships in `@angular/compiler-cli`,
5+
// which is a *peer* dependency of `@angular/build` (a direct devDependency of
6+
// this example). We resolve it from there instead of declaring it directly so
7+
// CI's frozen lockfile install stays unchanged.
8+
import { createRequire } from 'node:module'
9+
import { spawnSync } from 'node:child_process'
10+
import { dirname, join } from 'node:path'
11+
12+
const require = createRequire(import.meta.url)
13+
14+
// `@angular/compiler-cli` is resolvable from `@angular/build`'s location.
15+
const buildPkg = require.resolve('@angular/build/package.json')
16+
const cliPkgPath = require.resolve('@angular/compiler-cli/package.json', {
17+
paths: [buildPkg],
18+
})
19+
const cliPkg = require(cliPkgPath)
20+
const ngc = join(dirname(cliPkgPath), cliPkg.bin.ngc)
21+
22+
const { status } = spawnSync(
23+
process.execPath,
24+
[ngc, '-p', 'tsconfig.app.json', '--noEmit'],
25+
{ stdio: 'inherit' },
26+
)
27+
28+
process.exit(status ?? 1)

examples/ts-angular-chat/src/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export class AppComponent {
187187

188188
/** A message is worth rendering if it has visible text or a tool call. */
189189
isRenderable(message: {
190-
parts: ReadonlyArray<{ type: string; content?: string }>
190+
parts: ReadonlyArray<{ type: string; content?: unknown }>
191191
}): boolean {
192192
return message.parts.some(
193193
(part) =>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { UIMessage } from '@tanstack/ai-react'
2+
3+
type ToolResultPart = Extract<
4+
UIMessage['parts'][number],
5+
{ type: 'tool-result' }
6+
>
7+
8+
/** `string | Array<ContentPart>` — a tool result's raw content. */
9+
type ToolResultContent = ToolResultPart['content']
10+
11+
type ContentPartItem = Exclude<ToolResultContent, string>[number]
12+
13+
/**
14+
* Reduce a tool-result part's `content` to a plain string for rendering.
15+
*
16+
* Tool results carry `string | Array<ContentPart>` (multimodal results are
17+
* normalized to an array of content parts upstream). These demos render tool
18+
* results as plain strings, so array content is flattened to the concatenation
19+
* of its text parts; non-text parts (image, audio, video, document) have no
20+
* string form here and are skipped.
21+
*/
22+
export function toolResultContentToString(content: ToolResultContent): string {
23+
if (typeof content === 'string') return content
24+
return content
25+
.filter(
26+
(part): part is Extract<ContentPartItem, { type: 'text' }> =>
27+
part.type === 'text',
28+
)
29+
.map((part) => part.content)
30+
.join('')
31+
}

examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@/components/reports/useReportSSE'
1919
import ChatInput from '@/components/ChatInput'
2020
import { Header } from '@/components'
21+
import { toolResultContentToString } from '@/lib/tool-result-content'
2122
import { applyUIEvent, applyUIUpdates } from '@/lib/reports/apply-event'
2223
import type {
2324
RefreshResult,
@@ -159,7 +160,7 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
159160
for (const p of message.parts) {
160161
if (p.type === 'tool-result') {
161162
toolResults.set(p.toolCallId, {
162-
content: p.content,
163+
content: toolResultContentToString(p.content),
163164
state: p.state,
164165
error: p.error,
165166
})

examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { AnyTextAdapter, ServerTool, StreamChunk } from '@tanstack/ai'
1717
import type { IsolateDriver } from '@tanstack/ai-code-mode'
1818

1919
import { databaseTools, getSchemaInfoTool } from '@/lib/tools/database-tools'
20+
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
2021

2122
type Provider = 'anthropic' | 'openai' | 'gemini'
2223

@@ -284,7 +285,7 @@ export const Route = createFileRoute(
284285
systemPrompts,
285286
agentLoopStrategy: maxIterations(15),
286287
abortController,
287-
maxTokens: 8192,
288+
modelOptions: maxTokensModelOptions(rawAdapter, 8192),
288289
})
289290

290291
const instrumentedStream = wrapWithTimingEvents(stream, rawAdapter)

examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { VMEvent } from '@/components'
2323
import { CodeBlock, ExecutionResult, JavaScriptVM, Header } from '@/components'
2424
import ChatInput from '@/components/ChatInput'
2525
import { formatDuration } from '@/lib/efficiency'
26+
import { toolResultContentToString } from '@/lib/tool-result-content'
2627

2728
export const Route = createFileRoute('/_database-demo/database-demo' as any)({
2829
component: DatabaseDemoPage,
@@ -378,7 +379,7 @@ function Messages({
378379
for (const p of message.parts) {
379380
if (p.type === 'tool-result') {
380381
toolResults.set(p.toolCallId, {
381-
content: p.content,
382+
content: toolResultContentToString(p.content),
382383
state: p.state,
383384
error: p.error,
384385
})

examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { openaiText } from '@tanstack/ai-openai'
55
import { geminiText } from '@tanstack/ai-gemini'
66
import type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'
77
import { productTools } from '@/lib/tools/product-tools'
8+
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
89

910
type Provider = 'anthropic' | 'openai' | 'gemini'
1011

@@ -90,7 +91,7 @@ export const Route = createFileRoute('/_home/api/product-regular')({
9091
],
9192
agentLoopStrategy: maxIterations(30),
9293
abortController,
93-
maxTokens: 8192,
94+
modelOptions: maxTokensModelOptions(adapter, 8192),
9495
})
9596

9697
const requestStartTimeMs = Date.now()

examples/ts-code-mode-web/src/routes/_home/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { parsePartialJSON } from '@tanstack/ai'
1818
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
1919
import type { VMEvent } from '@/components'
2020
import { CodeBlock, ExecutionResult, JavaScriptVM, Header } from '@/components'
21+
import { toolResultContentToString } from '@/lib/tool-result-content'
2122

2223
export const Route = createFileRoute('/_home/')({
2324
component: ProductDemoPage,
@@ -792,7 +793,7 @@ function CodeModePanel({
792793
for (const p of message.parts) {
793794
if (p.type === 'tool-result') {
794795
toolResults.set(p.toolCallId, {
795-
content: p.content,
796+
content: toolResultContentToString(p.content),
796797
state: p.state,
797798
error: p.error,
798799
})
@@ -1088,7 +1089,7 @@ function RegularToolsPanel({
10881089
for (const part of message.parts) {
10891090
if (part.type === 'tool-result') {
10901091
toolResults.set(part.toolCallId, {
1091-
content: part.content,
1092+
content: toolResultContentToString(part.content),
10921093
state: part.state,
10931094
error: part.error,
10941095
})

0 commit comments

Comments
 (0)