Skip to content

Commit 21bcfc5

Browse files
committed
Add AI Gateway smoke coverage to e2e
1 parent f498639 commit 21bcfc5

9 files changed

Lines changed: 1631 additions & 160 deletions

File tree

.github/workflows/ci-e2e.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ jobs:
4444
env:
4545
VITE_TAILOR_APP_URL: ${{ secrets.E2E_TAILOR_APP_URL }}
4646
VITE_TAILOR_CLIENT_ID: ${{ secrets.E2E_TAILOR_CLIENT_ID }}
47+
VITE_TAILOR_AI_GATEWAY_URL: ${{ secrets.E2E_TAILOR_AI_GATEWAY_URL }}
4748
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
4849
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}

e2e/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
VITE_TAILOR_APP_URL=
33
# OAuth2 public client ID
44
VITE_TAILOR_CLIENT_ID=
5+
# AI Gateway URL (e.g., https://<gateway-domain>)
6+
VITE_TAILOR_AI_GATEWAY_URL=
57

68
# E2E test user credentials
79
E2E_USER_EMAIL=e2e-test@example.com

e2e/README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# E2E Tests for AuthProvider
1+
# E2E Tests for AuthProvider and AI Gateway
22

3-
Playwright-based E2E tests that verify the AuthProvider OAuth authentication flow against a real Tailor Platform workspace.
3+
Playwright-based E2E tests that verify the AuthProvider OAuth authentication flow and a minimal AI Gateway smoke check against a real Tailor Platform workspace.
44

55
## Setup
66

@@ -11,7 +11,7 @@ cd e2e/backend
1111
TAILOR_PLATFORM_WORKSPACE_ID=<your-workspace-id> pnpm deploy
1212
```
1313

14-
After deploy, retrieve the app URL and client ID using `tailor-sdk`:
14+
After deploy, retrieve the app URL, client ID, and AI Gateway URL using `tailor-sdk`:
1515

1616
```bash
1717
# Get the app URL
@@ -21,6 +21,10 @@ npx tailor-sdk show --workspace-id <your-workspace-id> --json
2121
# Get the OAuth2 client ID
2222
npx tailor-sdk oauth2client list --workspace-id <your-workspace-id> --json
2323
# → [{"clientId": "tpoc_...", ...}]
24+
25+
# Get the AI Gateway domain
26+
npx tailor-sdk workspace app list --workspace-id <your-workspace-id> --json
27+
# → find the entry named "e2e-ai-gateway" and use https://<domain>
2428
```
2529

2630
### 2. Create a test user
@@ -50,7 +54,7 @@ mutation CreateUserProfile {
5054
cp e2e/.env.example e2e/.env
5155
```
5256

53-
Fill in `VITE_TAILOR_APP_URL` and `VITE_TAILOR_CLIENT_ID` (retrieved above). The test user credentials are pre-filled.
57+
Fill in `VITE_TAILOR_APP_URL`, `VITE_TAILOR_CLIENT_ID`, and `VITE_TAILOR_AI_GATEWAY_URL` (retrieved above). The test user credentials are pre-filled.
5458

5559
### 4. Install dependencies & browsers
5660

@@ -74,12 +78,13 @@ cd e2e && pnpm dev
7478

7579
## Test Scenarios
7680

77-
| Test | Description |
78-
| ------------------- | ---------------------------------------------------------------- |
79-
| Auth guard display | Verifies unauthenticated users see the login UI |
80-
| Login flow | Full OAuth redirect → IDP login → callback → authenticated state |
81-
| Logout | Verifies logout returns to auth guard |
82-
| Session persistence | Confirms page reload maintains authentication |
81+
| Test | Description |
82+
| ------------------- | ------------------------------------------------------------------- |
83+
| Auth guard display | Verifies unauthenticated users see the login UI |
84+
| Login flow | Full OAuth redirect → IDP login → callback → authenticated state |
85+
| Logout | Verifies logout returns to auth guard |
86+
| Session persistence | Confirms page reload maintains authentication |
87+
| AI Gateway smoke | Sends `PING` and checks the OpenAI-compatible reply contains `PONG` |
8388

8489
## Architecture
8590

e2e/app/src/App.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1-
import { AuthProvider, createAuthClient, useAuth } from "@tailor-platform/app-shell";
1+
import {
2+
type AIGatewayClient,
3+
AuthProvider,
4+
createAIGatewayClient,
5+
createAuthClient,
6+
useAIChat,
7+
useAuth,
8+
} from "@tailor-platform/app-shell";
29

310
const authClient = createAuthClient({
411
appUri: import.meta.env.VITE_TAILOR_APP_URL,
512
clientId: import.meta.env.VITE_TAILOR_CLIENT_ID,
613
});
714

15+
const aiGatewayUrl = import.meta.env.VITE_TAILOR_AI_GATEWAY_URL;
16+
const aiClient = aiGatewayUrl
17+
? createAIGatewayClient({
18+
gatewayUri: aiGatewayUrl,
19+
authClient,
20+
})
21+
: null;
22+
23+
const unavailableAIClient = {
24+
async *chatCompletionStream() {
25+
throw new Error("AI Gateway not configured");
26+
},
27+
} satisfies AIGatewayClient;
28+
829
const AuthGuard = () => {
930
const { login } = useAuth();
1031

@@ -27,6 +48,20 @@ const AuthGuard = () => {
2748

2849
const AuthenticatedContent = () => {
2950
const { logout, isAuthenticated } = useAuth();
51+
const { messages, status, error, sendMessage } = useAIChat({
52+
client: aiClient ?? unavailableAIClient,
53+
model: "gpt-5-mini",
54+
});
55+
const aiResponse =
56+
[...messages].reverse().find((message) => message.role === "assistant")?.content ?? "";
57+
58+
const runAISmoke = async () => {
59+
if (!aiClient) {
60+
return;
61+
}
62+
63+
await sendMessage("Reply with exactly PONG. Do not add any other text. PING");
64+
};
3065

3166
return (
3267
<main data-testid="authenticated-content">
@@ -41,6 +76,19 @@ const AuthenticatedContent = () => {
4176
>
4277
Log out
4378
</button>
79+
<button
80+
type="button"
81+
data-testid="ai-smoke-button"
82+
disabled={!aiClient || status === "submitted" || status === "streaming"}
83+
onClick={() => {
84+
void runAISmoke();
85+
}}
86+
>
87+
Check AI Gateway
88+
</button>
89+
<p data-testid="ai-smoke-status">{status}</p>
90+
<pre data-testid="ai-smoke-response">{aiResponse}</pre>
91+
{error ? <pre data-testid="ai-smoke-error">{error.message}</pre> : null}
4492
</main>
4593
);
4694
};

e2e/backend/tailor.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAuth, defineConfig, defineIdp } from "@tailor-platform/sdk";
1+
import { defineAIGateway, defineAuth, defineConfig, defineIdp } from "@tailor-platform/sdk";
22
import { user } from "./src/tailordb/user";
33

44
const oauth2Config = {
@@ -32,7 +32,14 @@ const auth = defineAuth("e2e-auth", {
3232
idProvider: idp.provider(idp.name, idp.clients[0]),
3333
});
3434

35+
const aiGateway = defineAIGateway("e2e-ai-gateway", {
36+
authNamespace: "default",
37+
cors: [oauth2Config.redirectURIs[0]],
38+
});
39+
3540
export default defineConfig({
41+
// SDK-managed app id — do not edit, except when copying this config to a separate app.
42+
id: "c1f3a27c-3771-4ca9-99ae-fb38b435bbbc",
3643
name: "app-shell-e2e",
3744
cors: [oauth2Config.redirectURIs[0]],
3845

@@ -42,4 +49,5 @@ export default defineConfig({
4249

4350
auth,
4451
idp: [idp],
52+
aiGateways: [aiGateway],
4553
});

e2e/backend/tailor.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This file is auto-generated by @tailor-platform/sdk
22
// Do not edit this file manually
3-
// Regenerated automatically when running 'tailor-sdk apply' or 'tailor-sdk generate'
3+
// Regenerated automatically when running 'tailor-sdk deploy' or 'tailor-sdk generate'
44

55
declare module "@tailor-platform/sdk" {
66
interface AttributeMap {

e2e/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"devDependencies": {
1212
"@playwright/test": "1.51.1",
1313
"@tailor-platform/app-shell": "workspace:*",
14-
"@tailor-platform/sdk": "^1.45.1",
14+
"@tailor-platform/sdk": "^1.66.0",
1515
"@tailwindcss/vite": "^4.3.0",
1616
"@types/node": "catalog:",
1717
"@types/react": "catalog:",

e2e/tests/auth.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,40 @@ test.describe("AuthProvider", () => {
114114
});
115115
await expect(page.getByTestId("auth-status")).toHaveText("Logged in");
116116
});
117+
118+
test("authenticated user can reach AI Gateway with an OpenAI smoke prompt", async ({ page }) => {
119+
const email = process.env.E2E_USER_EMAIL;
120+
const password = process.env.E2E_USER_PASSWORD;
121+
const aiGatewayUrl = process.env.VITE_TAILOR_AI_GATEWAY_URL;
122+
123+
if (!email || !password) {
124+
test.skip(true, "E2E_USER_EMAIL and E2E_USER_PASSWORD must be set");
125+
return;
126+
}
127+
128+
if (!aiGatewayUrl) {
129+
test.skip(true, "VITE_TAILOR_AI_GATEWAY_URL must be set");
130+
return;
131+
}
132+
133+
await page.goto("/");
134+
await page.getByTestId("login-button").click();
135+
136+
await page.waitForURL(/idp\.erp\.dev\/.*\/signin/);
137+
await page.getByLabel(/email/i).fill(email);
138+
await page.locator("#password").fill(password);
139+
await page.getByRole("button", { name: /sign in|log in|submit/i }).click();
140+
141+
await page.waitForURL("http://localhost:3100/**");
142+
await expect(page.getByTestId("authenticated-content")).toBeVisible({
143+
timeout: 10000,
144+
});
145+
146+
await page.getByTestId("ai-smoke-button").click();
147+
148+
await expect(page.getByTestId("ai-smoke-response")).toContainText(/pong/i, {
149+
timeout: 30000,
150+
});
151+
await expect(page.getByTestId("ai-smoke-status")).toHaveText("ready");
152+
});
117153
});

0 commit comments

Comments
 (0)