Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/e2e-integration-roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@guidekit/core": patch
"@guidekit/react": patch
---

Expand E2E integration roadmap: contract/live test tiers, full tool coverage, publish live gate, proxy voice credential resolution, and widget test hooks.
2 changes: 1 addition & 1 deletion .changeset/integration-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"@guidekit/cli": patch
---

Harden real-app integration: session token recovery after server restart, voice widget transcript streaming, continuous mic listening, doctor VAD checks, and voice E2E smoke tests.
Harden real-app integration: session token recovery after server restart, voice widget transcript streaming, continuous mic listening, doctor VAD checks, voice E2E smoke tests, and full live-tier Playwright suite (agent tools, platform mode, proxy API, multi-turn).
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ jobs:
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Run E2E tests
- name: Run E2E contract tests
env:
GUIDEKIT_SECRET: guidekit-example-e2e-secret-32-chars
LLM_API_KEY: e2e-dummy-llm-key-for-contract-tests
run: pnpm test:e2e
run: pnpm test:e2e:contract

- name: Upload test results
if: failure()
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ jobs:
run: pnpm size:check

- name: Install Playwright browsers
run: npx playwright install --with-deps
run: npx playwright install --with-deps chromium

- name: E2E tests
- name: E2E contract tests
env:
GUIDEKIT_SECRET: guidekit-example-e2e-secret-32-chars
run: npx playwright test
LLM_API_KEY: e2e-dummy-llm-key-for-contract-tests
run: pnpm test:e2e:contract

- name: Dependency audit
run: pnpm audit --prod
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,46 @@ concurrency:
cancel-in-progress: true

jobs:
live-e2e:
name: Live E2E (pre-publish)
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Run live E2E suite
env:
LIVE_LLM: '1'
GUIDEKIT_SECRET: guidekit-example-e2e-secret-32-chars
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
SKIP_NPM_DRY_RUN: '1'
run: pnpm check:release

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: publish-live-e2e-report
path: playwright-report/
retention-days: 14

publish:
name: Publish @guidekit/* to npm
needs: live-e2e
if: ${{ always() && (inputs.dry_run || needs.live-e2e.result == 'success') }}
runs-on: ubuntu-latest
environment: npm-publish
permissions:
Expand Down
50 changes: 47 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,52 @@ guidekit/

### Testing expectations

- Unit tests: Vitest (`pnpm test:unit`)
- E2E: Playwright (`pnpm test:e2e`) — agent flows in `e2e/`
- New pipeline or cognitive behavior needs unit coverage; integration changes should touch example app or E2E when user-facing
- Unit tests: Vitest (`pnpm test:unit`) — all packages
- Contract E2E: Playwright (`pnpm test:e2e:contract`) — mocked LLM + Web Speech voice, runs on every PR
- Live E2E: Playwright (`pnpm test:e2e:live`) — real Gemini via proxy; **publish gate only** (`LIVE_LLM=1` + `LLM_API_KEY`)
- New pipeline or cognitive behavior needs unit coverage; user-facing integration changes should touch example app or contract E2E

### E2E layout

```
e2e/
├── contract/ # CI + pnpm check (no API key)
├── live/ # Pre-publish only
├── fixtures/ # LLM mocks, Web Speech mocks, helpers
└── env.ts # .env.local + LIVE_LLM detection
```

Voice E2E always mocks the browser Web Speech API — no Deepgram/ElevenLabs in Playwright.

### E2E coverage matrix (user-facing flows)

| Flow | Contract | Live |
|------|:--------:|:----:|
| Widget UI / a11y | yes | — |
| Proxy health / token / LLM | yes | yes |
| Text chat + streaming | mocked | yes |
| Multi-turn memory | — | yes |
| Agent tools (scroll, highlight, navigate, tour, clickElement) | yes | yes |
| Platform Mode (RAG, plugin, cognitive page) | yes | yes |
| Session recovery 401 | yes | yes |
| Voice (Web Speech mock → LLM) | yes | yes |
| Custom actions / form / readPage / dismiss | yes | partial |
| STT/TTS proxy key minting | yes | — |
| Hallucination guard bus event | yes | — |
| Vanilla IIFE widget | yes | — |

Commands: `pnpm test:e2e:contract` (CI), `pnpm test:e2e:live` (local), `pnpm test:e2e:live:full` (publish gate).

Before release, run `pnpm check:release` (runs live suite twice for the flake budget). Publish workflow uploads Playwright artifacts on failure.

### Release gate (production readiness)

Run `pnpm check:release` before publishing. It includes:

- `pnpm check` (build, typecheck, lint, unit, size, contract E2E)
- Package artifact verification (`scripts/verify-published-packages.mjs`)
- CLI subprocess smoke (`packages/cli/src/cli.smoke.test.ts`)
- Live E2E twice (`pnpm test:e2e:live:full` ×2)

## Commands

Expand All @@ -74,6 +117,7 @@ pnpm skills:sync # Link skills/guidekit for Codex/Cursor discovery
pnpm dev # Start docs + example apps
pnpm build # Build all packages
pnpm check # Full CI parity (build, typecheck, lint, test)
pnpm check:release # Production publish gate (includes live E2E x2)
pnpm publish:packages:dry-run # Dry-run npm publish locally
pnpm stats # Package LOC + core facade size
pnpm llms:generate # Regenerate llms.txt agent index
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ pnpm lint # ESLint
pnpm test # Unit tests (Vitest)
```

### Production readiness gate (before publishing)

Use the full release gate locally (it runs the live E2E suite twice):

```bash
pnpm check:release
```

This requires a real `LLM_API_KEY` (or `apps/example-nextjs/.env.local`) and will skip npm publish dry-run unless `NODE_AUTH_TOKEN`/`NPM_TOKEN` is present.

## Development Workflow

1. **Fork** the repository and create a feature branch from `main`.
Expand Down
6 changes: 6 additions & 0 deletions apps/example-nextjs/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from 'react';
import { Providers } from '../providers';

export default function MainLayout({ children }: { children: ReactNode }) {
return <Providers>{children}</Providers>;
}
48 changes: 48 additions & 0 deletions apps/example-nextjs/app/demo/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import {
platformDemoPlugin,
platformKnowledgeDocuments,
} from '../../lib/guidekit-platform';
import { GuideKitDemoActions } from '../guidekit-demo-actions';
import { GuideKitTestBridge } from '../guidekit-test-bridge';

const GuideKitProvider = dynamic(
() => import('@guidekit/react').then((mod) => mod.GuideKitProvider),
{ ssr: false },
);

/** Demo layout with cognitive engine enabled for E2E contract tests. */
export default function DemoLayout({ children }: { children: ReactNode }) {
return (
<GuideKitProvider
tokenEndpoint="/api/guidekit/token"
proxy={{
llm: '/api/guidekit/llm',
health: '/api/guidekit/health',
stt: '/api/guidekit/stt',
tts: '/api/guidekit/tts',
}}
llm={{ provider: 'gemini', model: 'gemini-2.5-flash-lite' }}
intelligence={true}
knowledge={{ documents: platformKnowledgeDocuments, engine: 'bm25', topK: 3 }}
plugins={[platformDemoPlugin]}
hallucinationGuard
cognitive
agent={{ name: 'GuideKit Cognitive Demo', greeting: 'Cognitive mode is enabled.' }}
options={{
debug: process.env.NODE_ENV === 'development',
mode: 'text',
clickableSelectors: {
allow: ['#name', '#email', '#message', 'input', 'textarea'],
},
}}
>
<GuideKitDemoActions />
<GuideKitTestBridge />
{children}
</GuideKitProvider>
);
}
28 changes: 28 additions & 0 deletions apps/example-nextjs/app/demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default function DemoPage() {
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '32px 16px' }}>
<h1>GuideKit Cognitive Demo</h1>
<p>
This page runs Platform Mode with <code>cognitive=&#123;true&#125;</code> for E2E contract
tests. Voice is text-only here for stability.
</p>

<section data-guidekit-target="overview" id="overview" style={{ marginTop: '32px' }}>
<h2>Overview</h2>
<p>Ask the assistant about page sections or request guided navigation.</p>
</section>

<section data-guidekit-target="contact" id="contact" style={{ marginTop: '32px' }}>
<h2>Contact Form</h2>
<form aria-label="Contact form" style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: '400px' }}>
<label htmlFor="name">Name</label>
<input id="name" name="name" type="text" placeholder="Your name" />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="you@example.com" />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows={3} />
</form>
</section>
</div>
);
}
112 changes: 112 additions & 0 deletions apps/example-nextjs/app/guidekit-test-bridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use client';

import { useEffect } from 'react';
import { useGuideKitCore } from '@guidekit/react';
import type { KnowledgeDocument } from '@guidekit/core';

type BusEvent = { name: string; data: unknown; at: number };

declare global {
interface Window {
__guidekitTest?: {
events: BusEvent[];
waitForEvent: (name: string, timeoutMs?: number) => Promise<BusEvent>;
waitForReady: (timeoutMs?: number) => Promise<void>;
addKnowledgeDocument: (doc: KnowledgeDocument) => void;
removeKnowledgeDocument: (documentId: string) => void;
clear: () => void;
};
}
}

/**
* Dev/E2E bridge: records EventBus emissions on window.__guidekitTest.
* Only active when NODE_ENV === 'development'.
*/
export function GuideKitTestBridge() {
const core = useGuideKitCore();

useEffect(() => {
if (process.env.NODE_ENV !== 'development' || !core) return;

const events: BusEvent[] = [];
const waiters = new Map<
string,
Array<{ resolve: (e: BusEvent) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>
>();

const push = (name: string, data: unknown) => {
const entry: BusEvent = { name, data, at: Date.now() };
events.push(entry);
const pending = waiters.get(name);
if (pending?.length) {
const next = pending.shift()!;
clearTimeout(next.timer);
next.resolve(entry);
if (!pending.length) waiters.delete(name);
}
};

const unsubValidation = core.bus.on('validation:complete', (data) => {
push('validation:complete', data);
});
const unsubLlmEnd = core.bus.on('llm:response-end', (data) => {
push('llm:response-end', data);
});

window.__guidekitTest = {
events,
waitForEvent: (name, timeoutMs = 30_000) =>
new Promise<BusEvent>((resolve, reject) => {
const existing = events.find((e) => e.name === name);
if (existing) {
resolve(existing);
return;
}
const timer = setTimeout(() => {
const list = waiters.get(name) ?? [];
waiters.set(
name,
list.filter((w) => w.resolve !== resolve),
);
reject(new Error(`Timed out waiting for bus event: ${name}`));
}, timeoutMs);
const list = waiters.get(name) ?? [];
list.push({ resolve, reject, timer });
waiters.set(name, list);
}),
waitForReady: (timeoutMs = 30_000) =>
new Promise<void>((resolve, reject) => {
const start = Date.now();
const timer = setInterval(() => {
if (core.isReady) {
clearInterval(timer);
resolve();
return;
}
if (Date.now() - start > timeoutMs) {
clearInterval(timer);
reject(new Error('Timed out waiting for GuideKitCore to become ready.'));
}
}, 50);
}),
addKnowledgeDocument: (doc) => {
core.addKnowledgeDocument(doc);
},
removeKnowledgeDocument: (documentId) => {
core.removeKnowledgeDocument(documentId);
},
clear: () => {
events.length = 0;
},
};

return () => {
unsubValidation();
unsubLlmEnd();
delete window.__guidekitTest;
};
}, [core]);

return null;
}
14 changes: 6 additions & 8 deletions apps/example-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import type { ReactNode } from 'react';
import { Providers } from './providers';

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers>
<nav aria-label="Main navigation" style={{ padding: '16px', borderBottom: '1px solid #e2e8f0' }}>
<a href="/" style={{ marginRight: '16px' }}>Home</a>
<a href="/about" style={{ marginRight: '16px' }}>About</a>
</nav>
<main>{children}</main>
</Providers>
<nav aria-label="Main navigation" style={{ padding: '16px', borderBottom: '1px solid #e2e8f0' }}>
<a href="/" style={{ marginRight: '16px' }}>Home</a>
<a href="/about" style={{ marginRight: '16px' }}>About</a>
<a href="/demo">Demo</a>
</nav>
<main>{children}</main>
</body>
</html>
);
Expand Down
Loading
Loading