streamtrust 0.3.0 → 0.4.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
@@ -10,7 +10,7 @@ StreamTrust is a CI check for streaming applications such as AI chat. Its reusab
10
10
  - **Repeated runs.** Each scenario runs N times and the report shows every repetition, because cancellation and retry bugs are often intermittent.
11
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
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.
13
+ **Status:** `0.4.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.
14
14
 
15
15
  ## Watch the 66-second demo
16
16
 
@@ -28,7 +28,31 @@ npx streamtrust init
28
28
  npx streamtrust verify --adapter ./streamtrust/adapter.mjs
29
29
  ```
30
30
 
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.
31
+ For reproducible CI, pin `streamtrust@0.4.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.
32
+
33
+ ## GitHub Action
34
+
35
+ Run StreamTrust through the [official composite GitHub Action](https://github.com/santoshsibyala/streamtrust-action), listed as [StreamTrust CI Verifier on GitHub Marketplace](https://github.com/marketplace/actions/streamtrust-ci-verifier):
36
+
37
+ ```yaml
38
+ - uses: santoshsibyala/streamtrust-action@v1
39
+ with:
40
+ adapter: ./streamtrust/adapter.mjs
41
+ node-version: "22"
42
+ artifact-name: streamtrust-report
43
+ ```
44
+
45
+ Check out your repository, install adapter dependencies, and start your test services first. The Action runs the pinned StreamTrust CLI, adds a job summary, and uploads diagnostic reports even when verification fails. By default, failed or unsupported checks and harness errors fail the job. See the [complete workflow, inputs, outputs, and failure policies](https://github.com/santoshsibyala/streamtrust-action/blob/main/README.md).
46
+
47
+ Use `@v1.0.0` to pin this Action release; `@v1` follows compatible Action updates. The generated HTTP adapter initially tests a bundled demo: adapt it to your application before treating its results as application coverage. Adapters run trusted repository code; use disposable test data and keep secrets out of uploaded reports.
48
+
49
+ ### Published HTTP/SSE demo
50
+
51
+ StreamTrust uses the published Action in its own CI. This [successful main-branch run](https://github.com/santoshsibyala/streamtrust/actions/runs/42171351265) used Action v1.0.0 and CLI 0.3.0 on Node 22: persistence, cancellation, and retry each passed three repetitions (9 PASS).
52
+
53
+ [![Published HTTP/SSE demo: StreamTrust Action job summary showing 9 PASS, 0 FAIL, 0 UNSUPPORTED, and 0 HARNESS_ERROR](docs/images/published-action-http-sse-summary.png)](https://github.com/santoshsibyala/streamtrust/actions/runs/42171351265#summary-127739661923)
54
+
55
+ This run checks the bundled HTTP/SSE teaching demo. The existing CLI, LibreChat, NextChat, and Vercel AI SDK workflows separately test the package built from the current checkout.
32
56
 
33
57
  ## LibreChat integration
34
58
 
@@ -40,7 +64,11 @@ A LibreChat preset now exercises persistence, native Stop, regeneration and erro
40
64
 
41
65
  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
66
 
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.
67
+ **Available since 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.
68
+
69
+ ## NextChat integration
70
+
71
+ Run `npx streamtrust init nextchat` to generate a Playwright adapter for a separate pinned NextChat checkout. It checks saved replies, Stop, regeneration and retry after a provider disconnect against the chat history stored in the browser. The local provider needs no model key. See the [NextChat setup and CI guide](examples/nextchat/README.md) and [integration workflow](.github/workflows/nextchat.yml).
44
72
 
45
73
  ## Run from source
46
74
 
@@ -60,7 +88,7 @@ Build the tarball with `npm pack` in this repository.
60
88
  Put the supplied tarball in your project's `vendor/` directory, then:
61
89
 
62
90
  ```sh
63
- npm install --save-dev ./vendor/streamtrust-0.3.0.tgz
91
+ npm install --save-dev ./vendor/streamtrust-0.4.0.tgz
64
92
  npx streamtrust init
65
93
  npx streamtrust verify --adapter ./streamtrust/adapter.mjs
66
94
  ```
@@ -156,7 +184,7 @@ Add StreamTrust as a test step in your existing CI. For GitHub Actions, follow t
156
184
  1. Install StreamTrust and create the starting adapter:
157
185
 
158
186
  ```sh
159
- npm install --save-dev --save-exact streamtrust@0.3.0
187
+ npm install --save-dev --save-exact streamtrust@0.4.0
160
188
  npx streamtrust init
161
189
  ```
162
190
 
@@ -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.3.0 from npm:
7
+ Use Node.js 24 and npm 11. In a separate verification project, install StreamTrust 0.4.0 from npm:
8
8
 
9
9
  ```sh
10
10
  npm init -y
11
- npm install --save-dev --save-exact streamtrust@0.3.0
11
+ npm install --save-dev --save-exact streamtrust@0.4.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
@@ -74,6 +74,31 @@ Use a fresh report directory for each job. Restrict workflow permissions to `con
74
74
 
75
75
  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`. Inspect reports before calling a failure an upstream defect.
76
76
 
77
+ ## Run in CI
78
+
79
+ To use the published GitHub Action, keep the checkout, dependency installation, Playwright installation, and build steps in the CI example above. Replace only its verify step with:
80
+
81
+ ```yaml
82
+ - name: Verify LibreChat with StreamTrust Action
83
+ uses: santoshsibyala/streamtrust-action@v1
84
+ with:
85
+ adapter: streamtrust-librechat/adapter.mjs
86
+ version: '0.4.0'
87
+ node-version: '24'
88
+ output: reports/librechat
89
+ repetitions: '3'
90
+ timeout-ms: '600000'
91
+ artifact-name: librechat-action-report
92
+ upload-report: 'false'
93
+ fail-on: failures
94
+ env:
95
+ STREAMTRUST_LIBRECHAT_ROOT: ${{ runner.temp }}/librechat
96
+ ```
97
+
98
+ Keep the existing upload step: `upload-report: 'false'` disables the Action's directory upload so CI continues uploading only the four listed report files. The Action still adds a job summary and fails the job on failed or unsupported checks and harness errors. Use a fresh report directory for each invocation.
99
+
100
+ `@v1` receives compatible Action updates; `@v1.0.0` pins the exact Action release. The CLI version is pinned separately by `version`. See the [Action README](https://github.com/santoshsibyala/streamtrust-action/blob/main/README.md) for setup, inputs, outputs, and failure policies. This uses the published CLI; retain direct CLI calls when testing a locally built, unreleased StreamTrust package.
101
+
77
102
  ## Evidence and limits
78
103
 
79
104
  `summary-only-v1` returns stable check identifiers, booleans, message counts and provider-call counts. Raw chat text, database records, user identifiers, network URLs and tokens remain in memory and are not returned as report evidence. The preset does not write screenshots, browser traces or application logs. Browser/database exceptions are replaced with fixed errors. App subprocess output is discarded. Upload only the four report files, not the entire working directory.
@@ -0,0 +1,51 @@
1
+ # NextChat CI integration
2
+
3
+ This preset drives a separate [NextChat checkout](https://github.com/ChatGPTNextWeb/NextChat/tree/1d3dc331512929a4b1d3e82354e72acce33b5c7a) with Playwright and a loopback OpenAI-compatible provider. NextChat is MIT licensed. Its source and dependencies stay in the checkout.
4
+
5
+ ## Setup
6
+
7
+ In a verification project with Node 22 (the install command selects Yarn Classic 1.22.22):
8
+
9
+ ```sh
10
+ npm init -y
11
+ npm install --save-dev --save-exact streamtrust@0.4.0 playwright@1.64.1
12
+ npx playwright install chromium
13
+ npx streamtrust init nextchat
14
+ export STREAMTRUST_NEXTCHAT_ROOT="$PWD/.cache/NextChat"
15
+ git clone https://github.com/ChatGPTNextWeb/NextChat.git "$STREAMTRUST_NEXTCHAT_ROOT"
16
+ git -C "$STREAMTRUST_NEXTCHAT_ROOT" checkout --detach 1d3dc331512929a4b1d3e82354e72acce33b5c7a
17
+ (cd "$STREAMTRUST_NEXTCHAT_ROOT" && npx --yes --package=yarn@1.22.22 -- yarn install --frozen-lockfile --ignore-scripts)
18
+ npx streamtrust verify --adapter nextchat/adapter.mjs --output reports/nextchat --repetitions 3 --timeout-ms 900000
19
+ ```
20
+
21
+ Use a dedicated checkout with clean tracked files and no development dotenv files. The adapter starts NextChat in development mode on loopback, waits for HTTP readiness, and closes the app, provider and browser after verification. Requests use a synthetic key. Each repetition starts in a fresh browser context. To choose another output directory for the template, use `streamtrust init <directory> --preset nextchat`. To use installed Chrome, set `STREAMTRUST_BROWSER_CHANNEL=chrome`.
22
+
23
+ ## What it checks
24
+
25
+ | Scenario | Expected outcome after reload |
26
+ |---|---|
27
+ | Persistence | One assistant answer matches the complete provider response, with one generation. |
28
+ | Cancellation | Clicking Stop after the first chunk preserves only the partial answer. Late completion text stays absent even if the provider can finish. |
29
+ | `regeneration.replace-turn` | Before Retry, the completed first answer is saved. After reload, the original assistant ID is absent and the sole assistant answer matches the second response. |
30
+ | `error-retry.replace-turn-same-session` | After a mid-stream disconnect, the first partial answer is saved before Retry. The replacement survives reload and the original assistant ID is absent. |
31
+
32
+ NextChat intentionally replaces the previous turn on Retry, so these NextChat-specific checks verify replacement in the active session; the shared evaluator checks branch preservation on regeneration and a saved error visible after reload before retry.
33
+
34
+ The adapter reads the selected session in the persisted Zustand record `chat-next-web-store` (IndexedDB database `keyval-store`, object store `keyval`; NextChat also supports localStorage fallback). It compares assistant text from storage with the rendered page and counts streaming provider calls. This tests browser-local persistence in one session. The Stop check covers visible and saved output. It does not establish server-side cancellation. The retry check covers recovery in the same session, without requiring a durable error flag or sibling branches. The pre-Retry read checks partial text for error retry. Whether a failure flag survives a reload before Retry is outside this scenario. Per-run evidence contains only booleans. Message text and IDs stay in memory. A changed selector or unreadable storage can produce a harness error.
35
+
36
+ ## Run in CI
37
+
38
+ After installing the locked consumer dependencies, installing Chromium, and preparing the pinned checkout above, export `STREAMTRUST_NEXTCHAT_ROOT` for the job. Commit the generated adapter and helper. With CLI 0.4.0 published, use the [StreamTrust Action](https://github.com/santoshsibyala/streamtrust-action):
39
+
40
+ ```yaml
41
+ - name: Verify NextChat
42
+ uses: santoshsibyala/streamtrust-action@v1
43
+ with:
44
+ adapter: nextchat/adapter.mjs
45
+ version: '0.4.0'
46
+ node-version: '22'
47
+ repetitions: '3'
48
+ timeout-ms: '900000'
49
+ artifact-name: nextchat-safe-reports
50
+ fail-on: failures
51
+ ```
@@ -0,0 +1,316 @@
1
+ import { chromium } from "playwright";
2
+ import { spawn, execFileSync } from "node:child_process";
3
+ import { createServer } from "node:net";
4
+ import { existsSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ import { startProvider, until } from "./provider.mjs";
8
+ const commit = "1d3dc331512929a4b1d3e82354e72acce33b5c7a";
9
+
10
+ async function availablePort() {
11
+ const socket = createServer();
12
+ await new Promise((ready, reject) => {
13
+ socket.once("error", reject);
14
+ socket.listen(0, "127.0.0.1", ready);
15
+ });
16
+ const port = socket.address().port;
17
+ await new Promise((done) => socket.close(done));
18
+ return port;
19
+ }
20
+
21
+ async function create() {
22
+ if (!process.env.STREAMTRUST_NEXTCHAT_ROOT)
23
+ throw Error(
24
+ "Set STREAMTRUST_NEXTCHAT_ROOT to the isolated NextChat checkout",
25
+ );
26
+ const root = resolve(process.env.STREAMTRUST_NEXTCHAT_ROOT);
27
+ const git = (...args) =>
28
+ execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
29
+ if (
30
+ git("rev-parse", "HEAD") !== commit ||
31
+ git("status", "--porcelain", "--untracked-files=no")
32
+ )
33
+ throw Error("Expected clean pinned NextChat source");
34
+ for (const file of [
35
+ ".env",
36
+ ".env.local",
37
+ ".env.development",
38
+ ".env.development.local",
39
+ ]) {
40
+ if (existsSync(resolve(root, file)))
41
+ throw Error("Remove dotenv files from the isolated checkout");
42
+ }
43
+ const port = await availablePort();
44
+ const url = `http://127.0.0.1:${port}`;
45
+ const provider = await startProvider();
46
+ const app = spawn(
47
+ process.execPath,
48
+ [
49
+ resolve(root, "node_modules/next/dist/bin/next"),
50
+ "dev",
51
+ "-H",
52
+ "127.0.0.1",
53
+ "-p",
54
+ String(port),
55
+ ],
56
+ {
57
+ cwd: root,
58
+ env: {
59
+ PATH: process.env.PATH,
60
+ HOME: process.env.HOME,
61
+ TMPDIR: process.env.TMPDIR,
62
+ NEXT_TELEMETRY_DISABLED: "1",
63
+ BASE_URL: provider.url,
64
+ OPENAI_API_KEY: "local-synthetic-only",
65
+ DEFAULT_MODEL: "gpt-4o-mini",
66
+ CUSTOM_MODELS: "-all,+gpt-4o-mini",
67
+ },
68
+ stdio: "ignore",
69
+ },
70
+ );
71
+ let stopped = false;
72
+ const exited = new Promise((done) => {
73
+ const onExit = () => {
74
+ stopped = true;
75
+ done();
76
+ };
77
+ app.once("error", onExit);
78
+ app.once("exit", onExit);
79
+ });
80
+ let browser;
81
+ const close = async () => {
82
+ try {
83
+ await browser?.close();
84
+ } finally {
85
+ app.kill("SIGTERM");
86
+ try {
87
+ await provider.close();
88
+ } finally {
89
+ await exited;
90
+ }
91
+ }
92
+ };
93
+ try {
94
+ await until(
95
+ async () => {
96
+ if (stopped) throw Error("NextChat exited before readiness");
97
+ try {
98
+ return (await fetch(url)).ok;
99
+ } catch {
100
+ return false;
101
+ }
102
+ },
103
+ "NextChat readiness",
104
+ 120000,
105
+ );
106
+ browser = await chromium.launch({
107
+ channel: process.env.STREAMTRUST_BROWSER_CHANNEL,
108
+ });
109
+ } catch (e) {
110
+ await close();
111
+ throw e;
112
+ }
113
+ return {
114
+ name: "NextChat local UI and IndexedDB",
115
+ metadata: async () => ({
116
+ evidencePolicy: "summary-only-v1",
117
+ provider: "local synthetic",
118
+ browser: browser.version(),
119
+ commit,
120
+ }),
121
+ scenarios: [
122
+ "persistence",
123
+ "cancellation",
124
+ "regeneration.replace-turn",
125
+ "error-retry.replace-turn-same-session",
126
+ ],
127
+ close,
128
+ async run(scenario, { repetition }) {
129
+ const context = await browser.newContext({ locale: "en-US" }),
130
+ page = await context.newPage();
131
+ page.setDefaultTimeout(10000);
132
+ await context.route(
133
+ /^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
134
+ (r) => r.abort(),
135
+ );
136
+ const since = provider.calls.length;
137
+ const calls = () =>
138
+ provider.calls.slice(since).filter((x) => typeof x.send === "function");
139
+ const text = async () => page.locator("body").innerText();
140
+ const read = () =>
141
+ page.evaluate(async () => {
142
+ let db, raw;
143
+ try {
144
+ db = await new Promise((ok, fail) => {
145
+ let r = indexedDB.open("keyval-store");
146
+ r.onsuccess = () => ok(r.result);
147
+ r.onerror = () => fail(r.error);
148
+ });
149
+ raw = await new Promise((ok, fail) => {
150
+ let r = db
151
+ .transaction("keyval")
152
+ .objectStore("keyval")
153
+ .get("chat-next-web-store");
154
+ r.onsuccess = () => ok(r.result);
155
+ r.onerror = () => fail(r.error);
156
+ });
157
+ } catch {
158
+ // NextChat falls back to localStorage when idb-keyval is unavailable.
159
+ } finally {
160
+ db?.close();
161
+ }
162
+ const state = JSON.parse(
163
+ raw || localStorage.getItem("chat-next-web-store") || "{}",
164
+ ).state;
165
+ return state?.sessions[state.currentSessionIndex]?.messages || [];
166
+ });
167
+ const regenerate = scenario === "regeneration.replace-turn";
168
+ const retryError = scenario === "error-retry.replace-turn-same-session";
169
+ const retry = regenerate || retryError;
170
+ let original;
171
+ const evidence = {};
172
+ const checks = [];
173
+ const check = (name, passed) => checks.push({ name, passed: !!passed });
174
+ try {
175
+ await page.goto(url + "/#/chat");
176
+ const box = page.locator("textarea:visible").last();
177
+ await box.fill(`Synthetic lifecycle check ${repetition}`);
178
+ await box.press("Enter");
179
+ await until(() => calls().length > 0, "first local provider call");
180
+ let c = calls()[0];
181
+ c.send(c.prefix);
182
+ await until(
183
+ async () => (await text()).includes(c.prefix),
184
+ "first chunk displayed",
185
+ );
186
+ if (scenario === "cancellation") {
187
+ await page
188
+ .getByText("Stop", { exact: true })
189
+ .last()
190
+ .locator("..")
191
+ .click();
192
+ await until(
193
+ async () =>
194
+ !(await page
195
+ .getByText("Stop", { exact: true })
196
+ .last()
197
+ .isVisible()),
198
+ "stop control retired",
199
+ 10000,
200
+ );
201
+ if (!c.closed) c.finish();
202
+ } else if (retryError) {
203
+ c.error();
204
+ } else {
205
+ c.finish();
206
+ }
207
+ if (retry) {
208
+ if (regenerate)
209
+ await until(
210
+ async () => (await text()).includes(c.prefix + c.suffix),
211
+ "first answer complete",
212
+ );
213
+ await page
214
+ .getByText("Stop", { exact: true })
215
+ .last()
216
+ .waitFor({ state: "hidden" });
217
+ await page
218
+ .locator('[class*="chat-message"]')
219
+ .filter({ hasText: c.prefix })
220
+ .last()
221
+ .hover();
222
+ const retryButton = page
223
+ .getByText("Retry", { exact: true })
224
+ .last()
225
+ .locator("..");
226
+ await retryButton.waitFor({ state: "visible" });
227
+ const assistants = (await read()).filter(
228
+ (m) => m.role === "assistant",
229
+ );
230
+ original = assistants[0];
231
+ evidence.originalSavedBeforeRetry =
232
+ assistants.length === 1 &&
233
+ typeof original.id === "string" &&
234
+ original.id.length > 0 &&
235
+ (regenerate
236
+ ? original.content === c.prefix + c.suffix &&
237
+ original.streaming === false
238
+ : typeof original.content === "string" &&
239
+ original.content.startsWith(c.prefix) &&
240
+ !original.content.includes(c.suffix));
241
+ check("originalSavedBeforeRetry", evidence.originalSavedBeforeRetry);
242
+ await retryButton.click();
243
+ await until(() => calls().length === 2, "second provider call");
244
+ c = calls()[1];
245
+ c.send(c.prefix);
246
+ c.finish();
247
+ }
248
+ const expected =
249
+ scenario === "cancellation" ? c.prefix : c.prefix + c.suffix;
250
+ await until(
251
+ async () => (await text()).includes(expected),
252
+ "answer visible",
253
+ );
254
+ await until(
255
+ async () =>
256
+ (await read()).some(
257
+ (m) =>
258
+ m.role === "assistant" &&
259
+ typeof m.content === "string" &&
260
+ m.content.includes(c.prefix),
261
+ ),
262
+ "IndexedDB contains displayed text",
263
+ );
264
+ await page.reload();
265
+ await page.locator("textarea:visible").last().waitFor();
266
+ await until(
267
+ async () => (await text()).includes(c.prefix),
268
+ "rehydrated answer",
269
+ );
270
+ const saved = (await read()).filter((m) => m.role === "assistant");
271
+ check(
272
+ "Saved answer equals expected text",
273
+ saved.length === 1 && saved[0].content === expected,
274
+ );
275
+ check(
276
+ "Reload shows expected answer",
277
+ (await text()).includes(expected),
278
+ );
279
+ check("No extra provider call", calls().length === (retry ? 2 : 1));
280
+ if (scenario === "cancellation")
281
+ check(
282
+ "No late completion displayed",
283
+ !(await text()).includes(c.suffix),
284
+ );
285
+ if (retry) {
286
+ evidence.originalAbsentAfterReload =
287
+ !!original?.id && saved.every((m) => m.id !== original.id);
288
+ check(
289
+ "originalAbsentAfterReload",
290
+ evidence.originalAbsentAfterReload,
291
+ );
292
+ }
293
+ return { checks, evidence };
294
+ } catch (error) {
295
+ if (process.env.STREAMTRUST_NEXTCHAT_DEBUG === "1")
296
+ console.error(error);
297
+ throw Error(
298
+ "NextChat scenario could not complete its UI or storage observation",
299
+ );
300
+ } finally {
301
+ for (const c of calls()) if (!c.closed) c.res.end();
302
+ await context.close();
303
+ }
304
+ },
305
+ };
306
+ }
307
+
308
+ export default async function createNextChatAdapter() {
309
+ try {
310
+ return await create();
311
+ } catch {
312
+ throw Error(
313
+ "NextChat setup failed. Check the pinned checkout, dotenv files, Yarn dependencies and Chromium installation.",
314
+ );
315
+ }
316
+ }
@@ -0,0 +1,107 @@
1
+ import { createServer } from "node:http";
2
+ export async function startProvider() {
3
+ const calls = [];
4
+ const server = createServer(async (req, res) => {
5
+ res.setHeader("Access-Control-Allow-Origin", "*");
6
+ res.setHeader("Access-Control-Allow-Headers", "*");
7
+ if (req.method === "OPTIONS") return res.end();
8
+ if (req.url?.endsWith("/models")) {
9
+ res.setHeader("Content-Type", "application/json");
10
+ return res.end(
11
+ JSON.stringify({
12
+ object: "list",
13
+ data: [
14
+ { id: "gpt-4o-mini", object: "model", owned_by: "local-fixture" },
15
+ ],
16
+ }),
17
+ );
18
+ }
19
+ let raw = "";
20
+ for await (const b of req) raw += b;
21
+ const body = raw ? JSON.parse(raw) : {};
22
+ const id = `fixture-${calls.length + 1}`;
23
+ const call = {
24
+ id,
25
+ path: req.url,
26
+ model: body.model,
27
+ closed: false,
28
+ sentFinish: false,
29
+ res,
30
+ prefix: `Synthetic answer ${calls.length + 1}.`,
31
+ suffix: " Completed.",
32
+ };
33
+ calls.push(call);
34
+ res.on("close", () => {
35
+ call.closed = true;
36
+ });
37
+ if (!body.stream) {
38
+ res.setHeader("Content-Type", "application/json");
39
+ return res.end(
40
+ JSON.stringify({
41
+ id,
42
+ object: "chat.completion",
43
+ choices: [
44
+ {
45
+ index: 0,
46
+ message: { role: "assistant", content: "Synthetic title" },
47
+ finish_reason: "stop",
48
+ },
49
+ ],
50
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
51
+ }),
52
+ );
53
+ }
54
+ res.writeHead(200, {
55
+ "Content-Type": "text/event-stream",
56
+ "Cache-Control": "no-cache",
57
+ });
58
+ res.flushHeaders();
59
+ call.send = (text) =>
60
+ res.write(
61
+ `data: ${JSON.stringify({
62
+ id,
63
+ object: "chat.completion.chunk",
64
+ created: 1,
65
+ model: body.model,
66
+ choices: [
67
+ { index: 0, delta: { content: text }, finish_reason: null },
68
+ ],
69
+ })}\n\n`,
70
+ );
71
+ call.finish = () => {
72
+ call.sentFinish = true;
73
+ call.send(call.suffix);
74
+ res.write(
75
+ `data: ${JSON.stringify({
76
+ id,
77
+ object: "chat.completion.chunk",
78
+ created: 1,
79
+ model: body.model,
80
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
81
+ })}\n\ndata: [DONE]\n\n`,
82
+ );
83
+ res.end();
84
+ };
85
+ call.error = () => res.destroy();
86
+ });
87
+ await new Promise((ready, reject) => {
88
+ server.once("error", reject);
89
+ server.listen(0, "127.0.0.1", ready);
90
+ });
91
+ return {
92
+ url: `http://127.0.0.1:${server.address().port}`,
93
+ calls,
94
+ async close() {
95
+ server.closeAllConnections();
96
+ await new Promise((r) => server.close(r));
97
+ },
98
+ };
99
+ }
100
+ export async function until(fn, label, ms = 30000) {
101
+ const deadline = Date.now() + ms;
102
+ while (Date.now() < deadline) {
103
+ if (await fn()) return;
104
+ await new Promise((r) => setTimeout(r, 100));
105
+ }
106
+ throw new Error(`Adapter timeout: ${label}`);
107
+ }
@@ -4,11 +4,11 @@ This preset runs a **minimal Next.js chat built on the AI SDK** (`ai`, `@ai-sdk/
4
4
 
5
5
  ## Quick start (no API key)
6
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:
7
+ Use Node.js 22 or newer (AI SDK 7 requires it). This preset has been available since StreamTrust 0.3.0. In a separate verification project:
8
8
 
9
9
  ```sh
10
10
  npm init -y
11
- npm install --save-dev --save-exact streamtrust@0.3.0 playwright@1.64.1
11
+ npm install --save-dev --save-exact streamtrust@0.4.0 playwright@1.64.1
12
12
  npx playwright install chromium
13
13
  npx streamtrust init streamtrust-vercel-ai --preset vercel-ai
14
14
  node streamtrust-vercel-ai/setup.mjs
@@ -93,6 +93,29 @@ Run the installation steps once, then commit your verification project package.j
93
93
 
94
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
95
 
96
+ ## Run in CI
97
+
98
+ To use the published GitHub Action, keep the checkout, dependency installation, Playwright installation, and build steps in the CI example above. Replace only its verify step with:
99
+
100
+ ```yaml
101
+ - name: Verify AI SDK app with StreamTrust Action
102
+ uses: santoshsibyala/streamtrust-action@v1
103
+ with:
104
+ adapter: streamtrust-vercel-ai/adapter.mjs
105
+ version: '0.4.0'
106
+ node-version: '22'
107
+ output: reports/vercel-ai
108
+ repetitions: '3'
109
+ timeout-ms: '600000'
110
+ artifact-name: vercel-ai-action-report
111
+ upload-report: 'false'
112
+ fail-on: failures
113
+ ```
114
+
115
+ Keep the existing upload step: `upload-report: 'false'` disables the Action's directory upload so CI continues uploading only the four listed report files. The Action still adds a job summary and fails the job on failed or unsupported checks and harness errors. Use a fresh report directory for each invocation.
116
+
117
+ `@v1` receives compatible Action updates; `@v1.0.0` pins the exact Action release. The CLI version is pinned separately by `version`. See the [Action README](https://github.com/santoshsibyala/streamtrust-action/blob/main/README.md) for setup, inputs, outputs, and failure policies. This uses the published CLI; retain direct CLI calls when testing a locally built, unreleased StreamTrust package.
118
+
96
119
  ## Evidence and limits
97
120
 
98
121
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamtrust",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Adapter-driven CI verification for streaming application lifecycle guarantees",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,7 +37,9 @@
37
37
  "examples/vercel-ai/reference-app/app/**/*.js",
38
38
  "examples/vercel-ai/reference-app/app/**/*.jsx",
39
39
  "README.md",
40
- "LICENSE"
40
+ "LICENSE",
41
+ "examples/nextchat/*.mjs",
42
+ "examples/nextchat/README.md"
41
43
  ],
42
44
  "scripts": {
43
45
  "test": "node --test test/*.test.mjs",
@@ -48,7 +50,14 @@
48
50
  "streaming",
49
51
  "verification",
50
52
  "testing",
51
- "sse"
53
+ "sse",
54
+ "github-action",
55
+ "ai",
56
+ "llm",
57
+ "chat",
58
+ "playwright",
59
+ "librechat",
60
+ "vercel-ai-sdk"
52
61
  ],
53
62
  "repository": {
54
63
  "type": "git",
package/src/cli.js CHANGED
@@ -19,11 +19,13 @@ const PRESETS = {
19
19
  'reference-app/app/api/chat/route.js', 'reference-app/app/chat/[id]/chat.jsx',
20
20
  'reference-app/app/chat/[id]/page.jsx',
21
21
  ].map((name) => [`vercel-ai/${name}`, name]),
22
+ nextchat: ['adapter.mjs', 'provider.mjs', 'README.md'].map(
23
+ (name) => [`nextchat/${name}`, name]),
22
24
  };
23
25
  export function presets() { return Object.keys(PRESETS); }
24
26
  function presetFiles(preset) { return PRESETS[preset]; }
25
27
  const HELP = () =>
26
- `StreamTrust ${VERSION}\n\nUsage:\n streamtrust init [directory] [--preset <name>]\n streamtrust verify --adapter ./streamtrust/adapter.mjs [options]\n\nOptions:\n --output <directory> Reports (default: reports/streamtrust)\n --repetitions <1-1000> Repetitions per scenario (default: 3)\n --timeout-ms <integer> Total run deadline including setup/cleanup (default: 300000)\n --help Show help\n --version Show version\n\nPresets: ${presets().join(', ')} (default: http)\n\nExit codes: 0 all pass; 1 failed/unsupported checks; 2 setup, timeout or harness error.\nAdapters execute trusted local code. JavaScript ESM (.mjs) works without dependencies.\n`;
28
+ `StreamTrust ${VERSION}\n\nUsage:\n streamtrust init [directory] [--preset <name>]\n streamtrust init nextchat\n streamtrust verify --adapter ./streamtrust/adapter.mjs [options]\n\nOptions:\n --output <directory> Reports (default: reports/streamtrust)\n --repetitions <1-1000> Repetitions per scenario (default: 3)\n --timeout-ms <integer> Total run deadline including setup/cleanup (default: 300000)\n --help Show help\n --version Show version\n\nPresets: ${presets().join(', ')} (default: http)\n\nExit codes: 0 all pass; 1 failed/unsupported checks; 2 setup, timeout or harness error.\nAdapters execute trusted local code. JavaScript ESM (.mjs) works without dependencies.\n`;
27
29
  export async function main(args) {
28
30
  let outputDir;
29
31
  try {
@@ -38,7 +40,7 @@ export async function main(args) {
38
40
  if (args[0] === 'init') {
39
41
  const initArgs = args.slice(1);
40
42
  const presetIndex = initArgs.indexOf('--preset');
41
- let preset = 'http';
43
+ let preset = initArgs.length === 1 && initArgs[0] === 'nextchat' ? 'nextchat' : 'http';
42
44
  if (presetIndex !== -1) {
43
45
  preset = initArgs[presetIndex + 1];
44
46
  initArgs.splice(presetIndex, 2);