streamtrust 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,13 @@
4
4
 
5
5
  **[View StreamTrust on npm](https://www.npmjs.com/package/streamtrust)**
6
6
 
7
- Adapter-driven CI checks for streaming applications: compare observed output with authoritative state, exercise cancellation, and verify retry identity.
7
+ StreamTrust is a CI check for streaming applications such as AI chat. Its reusable checks compare what was displayed with what the application authoritatively saved and report disagreements as failed checks. It does not replace Playwright: Playwright drives the browser, and the bundled presets use it inside their adapters.
8
8
 
9
- **Status:** `0.2.0`, an early 0.x release for independent teams. APIs and report formats may change before 1.0. The CLI is dependency-free, ESM, and requires Node.js 22+. MIT licensed. Browser and database libraries belong to your adapter and are not installed by StreamTrust.
9
+ - **Three outcomes, kept apart.** "The app failed" (FAIL), "this capability is not supported" (UNSUPPORTED) and "we could not determine whether it failed" (HARNESS_ERROR) are reported separately, and harness errors exit with their own code.
10
+ - **Repeated runs.** Each scenario runs N times and the report shows every repetition, because cancellation and retry bugs are often intermittent.
11
+ - **Your contract, written down.** An adapter implements your app's start, stream, cancel, retry and saved-state operations; StreamTrust invokes the adapter scenarios and writes JSON, JUnit, Markdown and HTML reports.
12
+
13
+ **Status:** `0.3.0`, an early 0.x release for independent teams. APIs and report formats may change before 1.0. The CLI is dependency-free, ESM, and requires Node.js 22+. MIT licensed. Browser and database libraries belong to your adapter and are not installed by StreamTrust.
10
14
 
11
15
  ## Watch the 66-second demo
12
16
 
@@ -24,13 +28,19 @@ npx streamtrust init
24
28
  npx streamtrust verify --adapter ./streamtrust/adapter.mjs
25
29
  ```
26
30
 
27
- For reproducible CI, pin `streamtrust@0.2.0` and commit your lockfile. The generated adapter initially exercises the bundled demonstration; map it to your application before using its results as application evidence.
31
+ For reproducible CI, pin `streamtrust@0.3.0` and commit your lockfile. The generated adapter initially exercises the bundled demonstration; map it to your application before using its results as application evidence.
28
32
 
29
33
  ## LibreChat integration
30
34
 
31
35
  A LibreChat preset now exercises persistence, native Stop, regeneration and error-retry through its browser UI and disposable MongoDB. It uses reusable `streamtrust/checks` assertions and summary-only evidence; no chat text, screenshots or application logs are included in its reports.
32
36
 
33
- **Available in StreamTrust 0.2.0.** Install from npm using the [LibreChat quick start and CI guide](examples/librechat/README.md). It requires Node 24 and optional Playwright/MongoDB dependencies. An [integration workflow](.github/workflows/librechat.yml) exercises the packed CLI against the pinned upstream version. The default `init` demonstration remains available.
37
+ **Available since StreamTrust 0.2.0.** Install from npm using the [LibreChat quick start and CI guide](examples/librechat/README.md). It requires Node 24 and optional Playwright/MongoDB dependencies. An [integration workflow](.github/workflows/librechat.yml) exercises the packed CLI against the pinned upstream version. The default `init` demonstration remains available.
38
+
39
+ ## Vercel AI SDK integration
40
+
41
+ A `vercel-ai` preset runs a minimal Next.js chat built on the AI SDK (`useChat`, `streamText`) in production mode with a synthetic OpenAI-compatible provider, so it needs no API key. It covers the same four scenarios through the browser and the app's saved state, and can be pointed at your own built AI SDK app after you adjust its selectors and saved-state read. It is not an official Vercel integration.
42
+
43
+ **Available in StreamTrust 0.3.0.** Install from npm and generate it with `npx streamtrust init streamtrust-vercel-ai --preset vercel-ai`. See the [Vercel AI SDK quick start](examples/vercel-ai/README.md) for what it does and does not verify. It requires Node 22+ and Playwright in your verification project. An [integration workflow](.github/workflows/vercel-ai.yml) exercises the packed CLI against the reference app.
34
44
 
35
45
  ## Run from source
36
46
 
@@ -50,7 +60,7 @@ Build the tarball with `npm pack` in this repository.
50
60
  Put the supplied tarball in your project's `vendor/` directory, then:
51
61
 
52
62
  ```sh
53
- npm install --save-dev ./vendor/streamtrust-0.2.0.tgz
63
+ npm install --save-dev ./vendor/streamtrust-0.3.0.tgz
54
64
  npx streamtrust init
55
65
  npx streamtrust verify --adapter ./streamtrust/adapter.mjs
56
66
  ```
@@ -146,7 +156,7 @@ Add StreamTrust as a test step in your existing CI. For GitHub Actions, follow t
146
156
  1. Install StreamTrust and create the starting adapter:
147
157
 
148
158
  ```sh
149
- npm install --save-dev --save-exact streamtrust@0.2.0
159
+ npm install --save-dev --save-exact streamtrust@0.3.0
150
160
  npx streamtrust init
151
161
  ```
152
162
 
@@ -229,6 +239,6 @@ Use a unique output directory per concurrent job. The CLI takes an exclusive loc
229
239
 
230
240
  ## Scope and attribution
231
241
 
232
- StreamTrust checks what an adapter asserts. It does not prove exactly-once external effects, AI factuality, automatic recovery, production incident rates or adoption. The HTTP demo is synthetic. Separate development experiments have tested pinned Vercel Chatbot and LibreChat applications; the LibreChat adapter is now available as an npm preset with optional dependencies. The Vercel experiment remains separate.
242
+ StreamTrust checks what an adapter asserts. It does not prove exactly-once external effects, AI factuality, automatic recovery, production incident rates or adoption. The HTTP demo is synthetic. Separate development experiments have tested pinned Vercel Chatbot and LibreChat applications; the LibreChat adapter is now available as an npm preset with optional dependencies. The `vercel-ai` preset targets its own minimal reference app, not the Vercel Chatbot template; that experiment remains separate.
233
243
 
234
244
  Local preparation: `npm pack` in this package directory runs the package tests and creates the tarball. The public repository runs tests and a clean tarball installation on Node.js 22 and 24. This release is published interactively; npm build provenance is not configured.
@@ -4,11 +4,11 @@ This preset runs a **built local LibreChat checkout**, native browser UI and rea
4
4
 
5
5
  ## Installation
6
6
 
7
- Use Node.js 24 and npm 11. In a separate verification project, install StreamTrust 0.2.0 from npm:
7
+ Use Node.js 24 and npm 11. In a separate verification project, install StreamTrust 0.3.0 from npm:
8
8
 
9
9
  ```sh
10
10
  npm init -y
11
- npm install --save-dev --save-exact streamtrust@0.2.0
11
+ npm install --save-dev --save-exact streamtrust@0.3.0
12
12
  npm install --save-dev --save-exact playwright@1.64.1 mongodb@6.20.0 mongodb-memory-server@11.3.0
13
13
  npx playwright install chromium
14
14
  npx streamtrust init streamtrust-librechat --preset librechat
@@ -0,0 +1,108 @@
1
+ # Vercel AI SDK CI integration
2
+
3
+ This preset runs a **minimal Next.js chat built on the AI SDK** (`ai`, `@ai-sdk/react` `useChat`, `streamText`) in production mode, drives its UI with Playwright, and compares what the browser showed with what the app saved. A synthetic OpenAI-compatible HTTP provider supplies deterministic text, so no model key is needed. Four scenarios cover saved answers, Stop, regeneration branches, and retry after a provider failure. This is not an official Vercel integration and is not endorsed by Vercel.
4
+
5
+ ## Quick start (no API key)
6
+
7
+ Use Node.js 22 or newer (AI SDK 7 requires it). This preset is available in StreamTrust 0.3.0. In a separate verification project:
8
+
9
+ ```sh
10
+ npm init -y
11
+ npm install --save-dev --save-exact streamtrust@0.3.0 playwright@1.64.1
12
+ npx playwright install chromium
13
+ npx streamtrust init streamtrust-vercel-ai --preset vercel-ai
14
+ node streamtrust-vercel-ai/setup.mjs
15
+ npx streamtrust verify --adapter streamtrust-vercel-ai/adapter.mjs \
16
+ --output reports/vercel-ai --repetitions 3
17
+ ```
18
+
19
+ `setup.mjs` installs and builds `streamtrust-vercel-ai/reference-app` (Next.js 16, AI SDK 7, pinned exact versions). The bundled `npm-shrinkwrap.json` pins transitive dependencies and setup uses `npm ci`. Commit the generated preset including this lockfile, and ignore `reference-app/node_modules/` and `reference-app/.next/`. Build and verification time depend on the machine. A cold install needs network access for npm and the Chromium download.
20
+
21
+ The verifier starts the built app on a loopback-only ephemeral port with a fresh temporary chat store, starts the provider and a headless browser, and tears all of them down afterwards. It does not forward your environment to the app (only `PATH`, `HOME` and temp-directory variables), and refuses an app directory containing `.env`, `.env.local`, `.env.production` or `.env.production.local`, because Next.js would load those files.
22
+
23
+ ## What each scenario checks
24
+
25
+ The adapter maps observations onto the shared `streamtrust/checks` lifecycle evaluator:
26
+
27
+ | Scenario | Action | Key assertions |
28
+ |---|---|---|
29
+ | `persistence` | Send, let the provider finish | Exactly one saved answer equals the streamed text; it survives reload; reload does not call the model again |
30
+ | `cancellation` | Click Stop after the first chunk | The provider connection is aborted before completion; the partial answer is saved as unfinished; a late completion cannot be delivered |
31
+ | `regeneration` | Finish, then click Regenerate | The new answer is saved as a sibling of the original (same parent, distinct id); the original is kept; only the new answer is shown after reload |
32
+ | `error-retry` | Provider drops mid-stream, reload, Regenerate | The failure is saved and still visible after reload; the retry is saved as a complete answer; the failed partial is no longer shown |
33
+
34
+ The reference app is a fixture for validating the verifier, not evidence of an upstream defect. Negative controls and their observed results are recorded in the release notes.
35
+
36
+ Two semantics are specific to the AI SDK and worth knowing:
37
+
38
+ - **Stop is a client-side fetch abort.** There is no server stop endpoint, so `cancel.server-acknowledged` means "the server persisted the aborted state", observed in the store, not a response to a stop request.
39
+ - **Regeneration is checked as branching.** The reference app keeps the old answer as a sibling. Many AI SDK apps instead save the client's `messages` array in `onFinish`, which drops the old answer on regenerate. Against such an app the regeneration branch checks fail by design. If replacing history is your intended behavior, return `{ checks: [], evidence: {}, unsupported: 'Regenerate replaces history' }` for that scenario instead of weakening the check.
40
+
41
+ ## Verify your own AI SDK app
42
+
43
+ The reference app is a demonstration: **its passing says nothing about your application.** To test your app, point the adapter at a built checkout:
44
+
45
+ ```sh
46
+ export STREAMTRUST_VERCEL_AI_ROOT=/path/to/your/next-app
47
+ # Build your isolated checkout using its usual build command.
48
+ npx streamtrust verify --adapter streamtrust-vercel-ai/adapter.mjs --output reports/vercel-ai
49
+ ```
50
+
51
+ Then edit `adapter.mjs` where your app differs. Expect HARNESS_ERROR, not FAIL, until the mapping matches:
52
+
53
+ 1. **Provider wiring.** The adapter passes `MODEL_BASE_URL`, `MODEL_API_KEY` and `MODEL_NAME` to the app. Your route must create its model from those, for example `createOpenAICompatible({ name, baseURL: process.env.MODEL_BASE_URL, apiKey: process.env.MODEL_API_KEY })`, at least in test builds. Without this the app calls a real provider or fails to start.
54
+ 2. **Authoritative read.** `saved(id)` reads the reference app's JSON store from `CHAT_STORE_DIR`. Replace it with a read of your database, mapping each assistant message to `{ id, parentId, text, status }` where status is `complete`, `streaming`, `aborted` or `error`.
55
+ 3. **UI selectors.** The adapter types into the textbox named `Message` and presses Enter, then uses the buttons named `Stop` and `Regenerate`, the `data-testid="messages"` container, the URL `/chat/<id>`, and the failure text `The model provider failed.`. Change them to match your UI.
56
+ 4. **Other services.** Start any database, auth or migrations your app needs in the adapter factory, and register their cleanup.
57
+
58
+ Optional settings:
59
+
60
+ | Variable | Purpose |
61
+ |---|---|
62
+ | `STREAMTRUST_VERCEL_AI_ROOT` | Built app directory; defaults to the bundled `reference-app` |
63
+ | `STREAMTRUST_VERCEL_AI_PORT` | Fixed loopback port; defaults to an ephemeral port |
64
+ | `STREAMTRUST_BROWSER_CHANNEL` | Installed browser channel such as `chrome`; defaults to Playwright Chromium |
65
+ | `STREAMTRUST_VERCEL_AI_DEBUG` | `1` prints raw adapter errors and app stderr to the console (never into reports) |
66
+
67
+ ## CI
68
+
69
+ Run the installation steps once, then commit your verification project package.json, lockfile, and generated preset. Use a job with `permissions: { contents: read }`, `timeout-minutes: 20`, and no production secrets. These steps run the bundled reference app; adapt the adapter and build step to verify your own application.
70
+
71
+ ```yaml
72
+ - uses: actions/checkout@v4
73
+ - uses: actions/setup-node@v4
74
+ with:
75
+ node-version: '22'
76
+ cache: npm
77
+ - run: npm ci
78
+ - run: npx --no-install playwright install --with-deps chromium
79
+ - run: node streamtrust-vercel-ai/setup.mjs
80
+ - name: Verify AI SDK app
81
+ run: npx --no-install streamtrust verify --adapter streamtrust-vercel-ai/adapter.mjs --output reports/vercel-ai --repetitions 3
82
+ - uses: actions/upload-artifact@v4
83
+ if: always()
84
+ with:
85
+ name: vercel-ai-verification
86
+ path: |
87
+ reports/vercel-ai/report.json
88
+ reports/vercel-ai/report.md
89
+ reports/vercel-ai/junit.xml
90
+ reports/vercel-ai/index.html
91
+ retention-days: 7
92
+ ```
93
+
94
+ Exit 0 means all declared checks passed; 1 means failed or unsupported checks; 2 means setup, timeout or harness error. Keep the exit status: do not use `continue-on-error`.
95
+
96
+ ## Evidence and limits
97
+
98
+ Reports use `summary-only-v1`: stable check names, booleans, message counts and provider-call counts. Chat text, message ids, URLs and file paths stay in memory. Reports replace browser and app exceptions with fixed messages even in debug mode. Debug mode prints raw errors to the console; do not enable it in CI that retains logs. This is data minimization, not a sandbox: the adapter and the app under test are trusted code.
99
+
100
+ What this does **not** verify: real model behavior or output quality, multiple server instances, resumable streams (`resume: true`), server crashes or redeploys mid-stream, tool calls, the AI SDK's own correctness in general, or production reliability. The model is synthetic, and the reference app's file store is a stand-in for a database. The fixture has no authentication or concurrency control and must not be deployed as a production chat app. A single matching saved answer is not proof of exactly-once side effects.
101
+
102
+ ## Release validation
103
+
104
+ For 0.3.0, a clean tarball consumer on macOS with Node 22.22.3 and Chromium 156.0.8078.4 passed all four scenarios with three repetitions (12 executions, 99 assertions). The 17 package tests passed on Node 22 and 24. The locked fixture dependencies reported no known vulnerabilities in `npm audit` at review time.
105
+
106
+ Three isolated mutations were each detected as FAIL with exit 1: removing request-abort propagation (`cancel.provider-aborted`), deleting the original regeneration branch (`retry.original-branch-preserved`), and skipping persistence for failed outcomes (`error.saved-before-retry`). These are deliberately broken copies of our fixture, not bugs attributed to Vercel or another team's app. Run `node test/vercel-ai-controls.mjs /path/to/consumer preset-directory` from the source repository to reproduce them against a disposable generated preset; the script restores the route and rebuilds afterwards.
107
+
108
+ The [Linux GitHub Actions run](https://github.com/santoshsibyala/streamtrust/actions/runs/40817581908) also passed the clean install/build, all 12 baseline executions, all three negative controls, and report artifact upload. Successful report checks found none of the tested synthetic text, local-path, or key markers; this is not a guarantee against arbitrary sensitive data in customized adapters.
@@ -0,0 +1,333 @@
1
+ import { assertIsolatedCheckout, normalizeAssistant } from './config.mjs';
2
+ import { existsSync } from 'node:fs';
3
+ import { evaluateLifecycle } from 'streamtrust/checks';
4
+ import { createRequire } from 'node:module';
5
+ import { spawn } from 'node:child_process';
6
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { createServer } from 'node:net';
9
+ import { join, resolve } from 'node:path';
10
+ import { createHash, randomUUID } from 'node:crypto';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ import { startProvider } from './provider.mjs';
14
+
15
+ // Visible text the app renders for a failed generation; change it for your app.
16
+ const FAILED = 'The model provider failed.';
17
+ const debug = process.env.STREAMTRUST_VERCEL_AI_DEBUG === '1';
18
+ export async function until(check, label, ms = 20000) {
19
+ const end = Date.now() + ms;
20
+ while (Date.now() < end) {
21
+ if (await check()) return;
22
+ await new Promise((r) => setTimeout(r, 100));
23
+ }
24
+ throw new Error(`Timeout: ${label}`);
25
+ }
26
+ let phase = 'preflight';
27
+ async function createAdapter() {
28
+ if (Number(process.versions.node.split('.')[0]) < 22)
29
+ throw new Error('vercel-ai preset requires Node.js 22+');
30
+ const root = resolve(
31
+ process.env.STREAMTRUST_VERCEL_AI_ROOT ||
32
+ fileURLToPath(new URL('./reference-app', import.meta.url)),
33
+ );
34
+ assertIsolatedCheckout(root);
35
+ if (!existsSync(resolve(root, '.next/BUILD_ID')))
36
+ throw new Error('App is not built; run setup.mjs first');
37
+ const configPort = Number(process.env.STREAMTRUST_VERCEL_AI_PORT || 0);
38
+ if (!Number.isInteger(configPort) || configPort < 0 || configPort > 65535)
39
+ throw new Error('Invalid app port');
40
+ const port =
41
+ configPort ||
42
+ (await new Promise((ok, fail) => {
43
+ const server = createServer();
44
+ server.once('error', fail);
45
+ server.listen(0, '127.0.0.1', () => {
46
+ const value = server.address().port;
47
+ server.close(() => ok(value));
48
+ });
49
+ }));
50
+ const baseUrl = `http://127.0.0.1:${port}`;
51
+ const require = createRequire(resolve(process.cwd(), 'package.json'));
52
+ const { chromium } = require('playwright');
53
+ const appRequire = createRequire(resolve(root, 'package.json'));
54
+ const lockPath = ['npm-shrinkwrap.json', 'package-lock.json'].map((name) => resolve(root, name)).find(existsSync);
55
+ const versions = Object.fromEntries(
56
+ ['ai', 'next', '@ai-sdk/react'].map((name) => {
57
+ try {
58
+ return [name, appRequire(`${name}/package.json`).version];
59
+ } catch {
60
+ return [name, 'unknown'];
61
+ }
62
+ }),
63
+ );
64
+ const cleanups = [];
65
+ let cleaned = false;
66
+ const cleanup = async () => {
67
+ if (cleaned) return;
68
+ cleaned = true;
69
+ let cleanupFailed = false;
70
+ for (const close of cleanups.reverse()) {
71
+ try {
72
+ await close();
73
+ } catch {
74
+ cleanupFailed = true;
75
+ }
76
+ }
77
+ if (cleanupFailed) throw new Error('Vercel AI preset cleanup failed');
78
+ };
79
+ try {
80
+ const provider = await startProvider();
81
+ cleanups.push(() => provider.close());
82
+ phase = 'disposable store';
83
+ const store = await mkdtemp(join(tmpdir(), 'streamtrust-vercel-ai-'));
84
+ cleanups.unshift(() => rm(store, { recursive: true, force: true }));
85
+ const runtime = process.execPath;
86
+ const env = {
87
+ // Do not forward provider keys, database URLs or application secrets.
88
+ ...Object.fromEntries(
89
+ ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'SystemRoot']
90
+ .filter((k) => process.env[k])
91
+ .map((k) => [k, process.env[k]]),
92
+ ),
93
+ NODE_ENV: 'production',
94
+ NEXT_TELEMETRY_DISABLED: '1',
95
+ CHAT_STORE_DIR: store,
96
+ MODEL_BASE_URL: provider.url,
97
+ MODEL_API_KEY: 'fixture-not-a-real-key',
98
+ MODEL_NAME: 'fixture-model',
99
+ };
100
+ phase = 'application startup';
101
+ const app = spawn(
102
+ runtime,
103
+ [
104
+ appRequire.resolve('next/dist/bin/next'),
105
+ 'start',
106
+ '--hostname',
107
+ '127.0.0.1',
108
+ '--port',
109
+ String(port),
110
+ ],
111
+ {
112
+ cwd: root,
113
+ env,
114
+ stdio: ['ignore', 'ignore', debug ? 'inherit' : 'ignore'],
115
+ },
116
+ );
117
+ let appError = false;
118
+ app.on('error', () => {
119
+ appError = true;
120
+ });
121
+ cleanups.push(async () => {
122
+ if (!appError && app.exitCode === null && app.signalCode === null) {
123
+ const stopped = new Promise((r) => app.once('exit', () => r()));
124
+ app.kill('SIGTERM');
125
+ const timer = setTimeout(() => app.kill('SIGKILL'), 10000);
126
+ await stopped;
127
+ clearTimeout(timer);
128
+ }
129
+ });
130
+ phase = 'browser startup';
131
+ const browser = await chromium.launch(
132
+ !process.env.STREAMTRUST_BROWSER_CHANNEL
133
+ ? { headless: true }
134
+ : { channel: process.env.STREAMTRUST_BROWSER_CHANNEL, headless: true },
135
+ );
136
+ cleanups.push(() => browser.close());
137
+ phase = 'application readiness';
138
+ await until(
139
+ async () => {
140
+ if (appError || app.exitCode !== null)
141
+ throw new Error('App startup failed; check the build and port');
142
+ try {
143
+ return (await fetch(baseUrl, { signal: AbortSignal.timeout(2000) }))
144
+ .ok;
145
+ } catch {
146
+ return false;
147
+ }
148
+ },
149
+ 'app ready',
150
+ 60000,
151
+ );
152
+ const context = await browser.newContext();
153
+ // Authoritative read. For your app, replace this with a query against its database.
154
+ async function saved(id) {
155
+ try {
156
+ return JSON.parse(await readFile(join(store, `${id}.json`), 'utf8'))
157
+ .messages;
158
+ } catch (error) {
159
+ if (error.code === 'ENOENT') return [];
160
+ throw error;
161
+ }
162
+ }
163
+ const assistants = async (id) =>
164
+ (await saved(id)).filter((m) => m.role === 'assistant');
165
+ const observed = async (check) => {
166
+ const deadline = Date.now() + 15000;
167
+ while (Date.now() < deadline) {
168
+ if (await check()) return true;
169
+ await new Promise((r) => setTimeout(r, 100));
170
+ }
171
+ return false;
172
+ };
173
+ const visible = (p) => p.getByTestId('messages').innerText();
174
+ const stopButton = (p) =>
175
+ p.getByRole('button', { name: 'Stop', exact: true });
176
+ const settled = (p, label) =>
177
+ until(async () => !(await stopButton(p).count()), label);
178
+ const reload = async (p) => {
179
+ await p.reload();
180
+ await p.getByRole('textbox', { name: 'Message' }).waitFor();
181
+ };
182
+ async function waitGeneration(p, index) {
183
+ await until(
184
+ () => provider.generations.length > index,
185
+ 'provider request',
186
+ );
187
+ const g = provider.generations[index];
188
+ await until(
189
+ async () => (await visible(p)).includes(g.prefix),
190
+ 'first rendered chunk',
191
+ );
192
+ return g;
193
+ }
194
+ return {
195
+ name: 'Vercel AI SDK reference app',
196
+ scenarios: ['persistence', 'cancellation', 'regeneration', 'error-retry'],
197
+ async metadata() {
198
+ return {
199
+ evidencePolicy: 'summary-only-v1',
200
+ checkVersion: 1,
201
+ app: process.env.STREAMTRUST_VERCEL_AI_ROOT
202
+ ? 'configured application'
203
+ : 'bundled reference app',
204
+ versions,
205
+ node: process.version,
206
+ browser: browser.version(),
207
+ lockSha256: lockPath
208
+ ? createHash('sha256')
209
+ .update(await readFile(lockPath))
210
+ .digest('hex')
211
+ : 'none',
212
+ scope:
213
+ 'Real Next.js production server, useChat UI, AI SDK route and the app-owned store. Only the OpenAI-compatible model responses are synthetic. Single server process; no multi-instance, resumable-stream, server-crash or production coverage. Stop is a client fetch abort: there is no server stop endpoint, so acknowledgement means the server persisted the aborted state.',
214
+ };
215
+ },
216
+ async run(scenario, { repetition }) {
217
+ const p = await context.newPage();
218
+ let first;
219
+ let second;
220
+ try {
221
+ const id = randomUUID();
222
+ await p.goto(`${baseUrl}/chat/${id}`);
223
+ const input = p.getByRole('textbox', { name: 'Message' });
224
+ await input.waitFor();
225
+ const before = provider.generations.length;
226
+ await input.fill(
227
+ `StreamTrust ${scenario} ${repetition}. Write two sentences.`,
228
+ );
229
+ await input.press('Enter');
230
+ first = await waitGeneration(p, before);
231
+ const cancellation = {};
232
+ const failure = {};
233
+ let expected = first.prefix + first.suffix;
234
+ if (scenario === 'cancellation') {
235
+ await stopButton(p).click();
236
+ await observed(() => first.closed);
237
+ expected = first.prefix;
238
+ cancellation.acknowledged = await observed(async () =>
239
+ (await assistants(id)).some((m) => m.status === 'aborted'),
240
+ );
241
+ cancellation.providerClosed = first.closed;
242
+ cancellation.providerFinished = first.finished;
243
+ cancellation.lateCompletionRejected = !provider.finish(first);
244
+ } else if (scenario === 'error-retry') {
245
+ provider.fail(first);
246
+ } else provider.finish(first);
247
+ await settled(p, 'generation settles');
248
+ if (scenario === 'error-retry') {
249
+ failure.saved = await observed(async () =>
250
+ (await assistants(id)).some((m) => m.status === 'error'),
251
+ );
252
+ await reload(p);
253
+ failure.visibleAfterReload = await observed(async () =>
254
+ (await visible(p)).includes(FAILED),
255
+ );
256
+ }
257
+ if (scenario === 'regeneration' || scenario === 'error-retry') {
258
+ const nextIndex = provider.generations.length;
259
+ await p
260
+ .getByRole('button', { name: 'Regenerate', exact: true })
261
+ .click();
262
+ second = await waitGeneration(p, nextIndex);
263
+ provider.finish(second);
264
+ expected = second.prefix + second.suffix;
265
+ await settled(p, 'retry settles');
266
+ }
267
+ await observed(async () =>
268
+ (await assistants(id)).some(
269
+ (m) =>
270
+ m.text.trim() === expected.trim() && m.status !== 'streaming',
271
+ ),
272
+ );
273
+ const durable = await assistants(id);
274
+ const beforeReload = await visible(p);
275
+ const callsBeforeReload = provider.generations.length;
276
+ await reload(p);
277
+ await observed(async () => (await visible(p)).includes(expected));
278
+ const reloaded = await visible(p);
279
+ return evaluateLifecycle({
280
+ scenario,
281
+ expected,
282
+ original: first.prefix + first.suffix,
283
+ previousPrefix: first.prefix,
284
+ beforeReload,
285
+ reloaded,
286
+ messages: durable.map(normalizeAssistant),
287
+ callsBeforeReload,
288
+ callsAfterReload: provider.generations.length,
289
+ cancellation:
290
+ scenario === 'cancellation' ? cancellation : undefined,
291
+ failure: scenario === 'error-retry' ? failure : undefined,
292
+ });
293
+ } finally {
294
+ if (first && !first.closed) provider.finish(first);
295
+ if (second && !second.closed) provider.finish(second);
296
+ await p.close();
297
+ }
298
+ },
299
+ close: cleanup,
300
+ };
301
+ } catch (error) {
302
+ await cleanup();
303
+ throw error;
304
+ }
305
+ }
306
+
307
+ // Browser/app exceptions can contain prompts, URLs and file paths; reports get fixed messages.
308
+ // STREAMTRUST_VERCEL_AI_DEBUG=1 prints raw errors and app stderr to the console, never to reports.
309
+ const withheld = (message) => (error) => {
310
+ if (debug) console.error(error);
311
+ throw new Error(message);
312
+ };
313
+ export default async function safeFactory() {
314
+ phase = 'preflight';
315
+ const adapter = await createAdapter().catch((error) =>
316
+ withheld(
317
+ `Vercel AI preset setup failed during ${phase}. Check Node 22+, Playwright, a built app without .env files, and the port. Raw details withheld.`,
318
+ )(error),
319
+ );
320
+ const run = adapter.run.bind(adapter);
321
+ adapter.run = (...args) =>
322
+ run(...args).catch(
323
+ withheld(
324
+ 'Vercel AI scenario could not complete (details withheld by summary-only policy)',
325
+ ),
326
+ );
327
+ const metadata = adapter.metadata.bind(adapter);
328
+ adapter.metadata = () =>
329
+ metadata().catch(
330
+ withheld('Vercel AI metadata unavailable (details withheld)'),
331
+ );
332
+ return adapter;
333
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ export function assertIsolatedCheckout(root) {
5
+ // Next.js production builds and servers load all four of these files.
6
+ if (['.env', '.env.local', '.env.production', '.env.production.local']
7
+ .some((name) => existsSync(resolve(root, name))))
8
+ throw new Error('Use an app checkout without environment files');
9
+ }
10
+
11
+ export function normalizeAssistant(m) {
12
+ if (!['complete', 'streaming', 'aborted', 'error'].includes(m.status))
13
+ throw new Error('Unknown saved assistant status');
14
+ return { id: m.id, parentId: m.parentId, text: m.text, assistant: true,
15
+ unfinished: m.status === 'aborted' || m.status === 'streaming',
16
+ error: m.status === 'error' };
17
+ }
@@ -0,0 +1,79 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ export async function startProvider() {
4
+ const generations = [];
5
+ const chunk = (g, delta, finish = null) =>
6
+ g.response.write(
7
+ `data: ${JSON.stringify({ id: `chatcmpl-fixture-${g.id}`, object: 'chat.completion.chunk', created: 1, model: 'fixture-model', choices: [{ index: 0, delta: { content: delta }, finish_reason: finish }] })}\n\n`,
8
+ );
9
+ const server = createServer(async (req, res) => {
10
+ let body = '';
11
+ for await (const part of req) body += part;
12
+ if (req.method !== 'POST') {
13
+ res.setHeader('content-type', 'application/json');
14
+ res.end(
15
+ JSON.stringify({ data: [{ id: 'fixture-model', object: 'model' }] }),
16
+ );
17
+ return;
18
+ }
19
+ const input = JSON.parse(body);
20
+ if (!input.stream) {
21
+ res.setHeader('content-type', 'application/json');
22
+ res.end(
23
+ JSON.stringify({
24
+ choices: [
25
+ {
26
+ message: { role: 'assistant', content: 'Fixture conversation' },
27
+ finish_reason: 'stop',
28
+ },
29
+ ],
30
+ }),
31
+ );
32
+ return;
33
+ }
34
+ const id = generations.length + 1;
35
+ const g = {
36
+ id,
37
+ prefix: `Response ${id} begins.`,
38
+ suffix: ` Response ${id} completes.`,
39
+ closed: false,
40
+ finished: false,
41
+ failed: false,
42
+ path: req.url,
43
+ response: res,
44
+ };
45
+ generations.push(g);
46
+ res.on('close', () => {
47
+ g.closed = true;
48
+ });
49
+ res.writeHead(200, {
50
+ 'content-type': 'text/event-stream',
51
+ 'cache-control': 'no-cache',
52
+ });
53
+ chunk(g, g.prefix);
54
+ });
55
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
56
+ return {
57
+ url: `http://127.0.0.1:${server.address().port}/v1`,
58
+ generations,
59
+ finish(g) {
60
+ if (g.closed) return false;
61
+ chunk(g, g.suffix);
62
+ chunk(g, '', 'stop');
63
+ g.finished = true;
64
+ g.response.end('data: [DONE]\n\n');
65
+ return true;
66
+ },
67
+ fail(g) {
68
+ g.failed = true;
69
+ g.response.destroy();
70
+ },
71
+ observations() {
72
+ return generations.map(({ response, ...g }) => g);
73
+ },
74
+ async close() {
75
+ server.closeAllConnections();
76
+ await new Promise((r) => server.close(() => r()));
77
+ },
78
+ };
79
+ }