Changed Claude code configuration
parent
97cb58ff42
commit
fb604a408b
@ -0,0 +1,115 @@
|
||||
# Error Location Logging — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add source file and line number to WARN and ERROR log lines, leaving INFO/DEBUG unchanged.
|
||||
|
||||
**Architecture:** Custom `FormatEvent` implementation that conditionally appends `[file:line]` based on log level. One new file (`logging.rs`), one modified line in `main.rs`.
|
||||
|
||||
**Tech Stack:** `tracing_subscriber::fmt::FormatEvent`, `tracing::Level`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-23-error-location-logging-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create the custom formatter
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/src/logging.rs`
|
||||
- Modify: `backend/src/main.rs:6,11,30`
|
||||
|
||||
- [ ] **Step 1: Create `backend/src/logging.rs`**
|
||||
|
||||
```rust
|
||||
//! Custom log formatter that includes source file and line for WARN/ERROR events.
|
||||
|
||||
use std::fmt;
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::fmt::format::{self, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::fmt::FmtContext;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
/// Event formatter that appends `[file:line]` to WARN and ERROR log lines.
|
||||
///
|
||||
/// INFO, DEBUG, and TRACE events use the default format without source location.
|
||||
pub struct ErrorLocationFormat;
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for ErrorLocationFormat
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &FmtContext<'_, S, N>,
|
||||
mut writer: format::Writer<'_>,
|
||||
event: &Event<'_>,
|
||||
) -> fmt::Result {
|
||||
let metadata = event.metadata();
|
||||
let level = metadata.level();
|
||||
let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.6fZ");
|
||||
|
||||
// Write: timestamp + level + target
|
||||
write!(writer, "{} {:>5} {}", timestamp, level, metadata.target())?;
|
||||
|
||||
// Append [file:line] for WARN and ERROR only
|
||||
if *level <= Level::WARN {
|
||||
if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) {
|
||||
write!(writer, " [{}:{}]", file, line)?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(writer, ": ")?;
|
||||
|
||||
// Write the event fields (message, structured fields)
|
||||
ctx.format_fields(writer.by_ref(), event)?;
|
||||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `mod logging;` to `main.rs`**
|
||||
|
||||
In `backend/src/main.rs`, add the module declaration alongside `mod cli;`:
|
||||
|
||||
```rust
|
||||
mod cli;
|
||||
mod logging;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire the formatter into tracing initialization**
|
||||
|
||||
In `backend/src/main.rs`, replace line 30:
|
||||
|
||||
```rust
|
||||
// Before:
|
||||
fmt().with_env_filter(filter).init();
|
||||
|
||||
// After:
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.event_format(logging::ErrorLocationFormat)
|
||||
.init();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify nothing breaks**
|
||||
|
||||
Run: `cd backend && cargo test --lib`
|
||||
Expected: 338 tests pass (the formatter is only used in main.rs binary, not in lib tests)
|
||||
|
||||
- [ ] **Step 5: Run the binary to verify output format**
|
||||
|
||||
Run: `cd backend && RUST_LOG=debug cargo run -- serve 2>&1 | head -10`
|
||||
Expected: INFO/DEBUG lines have no `[file:line]`, but any WARN/ERROR would show it.
|
||||
|
||||
To force an error, run without DATABASE_URL:
|
||||
Run: `cd backend && RUST_LOG=debug cargo run -- serve 2>&1 | head -5`
|
||||
Expected: An ERROR line with `[file:line]` showing the source location.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/logging.rs backend/src/main.rs
|
||||
git commit -m "feat: add source file:line to WARN and ERROR log lines"
|
||||
```
|
||||
@ -0,0 +1,320 @@
|
||||
# Generation UAT with Real OpenAI API — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a Playwright E2E test that exercises the full synthesis generation pipeline with a real OpenAI API key.
|
||||
|
||||
**Architecture:** Single Playwright spec that makes API calls via `page.evaluate(fetch(...))` in the browser context (sharing the session cookie). Gated by `.env.test` — skips when no key is available.
|
||||
|
||||
**Tech Stack:** Playwright, dotenv, EventSource (browser API)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-23-generation-uat-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `.env.test` to gitignore and create example file
|
||||
|
||||
**Files:**
|
||||
- Modify: `e2e/.gitignore`
|
||||
- Create: `e2e/.env.test.example`
|
||||
|
||||
- [ ] **Step 1: Add `.env.test` to `e2e/.gitignore`**
|
||||
|
||||
Append to `e2e/.gitignore`:
|
||||
|
||||
```
|
||||
.env.test
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `e2e/.env.test.example`**
|
||||
|
||||
```
|
||||
# Copy this file to .env.test and fill in your real API key.
|
||||
# This file is gitignored and MUST NOT be committed.
|
||||
OPENAI_TEST_API_KEY=sk-your-key-here
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add e2e/.gitignore e2e/.env.test.example
|
||||
git commit -m "test: add .env.test template for live generation UAT"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Install dotenv dependency
|
||||
|
||||
**Files:**
|
||||
- Modify: `e2e/package.json`
|
||||
|
||||
- [ ] **Step 1: Install dotenv**
|
||||
|
||||
```bash
|
||||
cd e2e && npm install --save-dev dotenv
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add e2e/package.json e2e/package-lock.json
|
||||
git commit -m "test: add dotenv dependency for E2E test env loading"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create the live generation test
|
||||
|
||||
**Files:**
|
||||
- Create: `e2e/tests/generation-live.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Create `e2e/tests/generation-live.spec.ts`**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* E2E UAT: Synthesis generation with a real OpenAI API key.
|
||||
*
|
||||
* Exercises the full pipeline: settings, API key encryption, model resolution,
|
||||
* LLM call (search + rewrite), response parsing, and synthesis storage.
|
||||
*
|
||||
* Gated by OPENAI_TEST_API_KEY in e2e/.env.test — skips if not set.
|
||||
* Uses gpt-4o-mini to keep cost under $0.01 per run.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import { loginAsUser } from '../helpers/auth';
|
||||
|
||||
// Load .env.test from the e2e directory
|
||||
dotenv.config({ path: path.resolve(__dirname, '..', '.env.test') });
|
||||
|
||||
const OPENAI_KEY = process.env.OPENAI_TEST_API_KEY;
|
||||
|
||||
/** Helper to make an authenticated API call from the browser context. */
|
||||
async function apiCall(
|
||||
page: any,
|
||||
method: string,
|
||||
url: string,
|
||||
body?: object,
|
||||
): Promise<{ status: number; data: any }> {
|
||||
return page.evaluate(
|
||||
async ({
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
url: string;
|
||||
body?: object;
|
||||
}) => {
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
};
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const resp = await fetch(url, options);
|
||||
const data = await resp.json().catch(() => null);
|
||||
return { status: resp.status, data };
|
||||
},
|
||||
{ method, url, body },
|
||||
);
|
||||
}
|
||||
|
||||
/** Wait for SSE generation to complete, return the synthesis_id. */
|
||||
async function waitForGenerationComplete(
|
||||
page: any,
|
||||
jobId: string,
|
||||
timeoutMs = 120_000,
|
||||
): Promise<string> {
|
||||
return page.evaluate(
|
||||
async ({ jobId, timeoutMs }: { jobId: string; timeoutMs: number }) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error('Generation timed out')),
|
||||
timeoutMs,
|
||||
);
|
||||
const es = new EventSource(
|
||||
`/api/v1/syntheses/generate/${jobId}/progress`,
|
||||
);
|
||||
es.addEventListener('complete', (event: MessageEvent) => {
|
||||
clearTimeout(timer);
|
||||
es.close();
|
||||
const parsed = JSON.parse(event.data);
|
||||
resolve(parsed.synthesis_id);
|
||||
});
|
||||
es.addEventListener('error', (event: MessageEvent) => {
|
||||
clearTimeout(timer);
|
||||
es.close();
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
reject(new Error(`Generation failed: ${parsed.message}`));
|
||||
} catch {
|
||||
reject(new Error('Generation failed with unknown error'));
|
||||
}
|
||||
});
|
||||
es.onerror = () => {
|
||||
clearTimeout(timer);
|
||||
es.close();
|
||||
reject(new Error('SSE connection error'));
|
||||
};
|
||||
});
|
||||
},
|
||||
{ jobId, timeoutMs },
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Live generation with OpenAI', () => {
|
||||
test.skip(!OPENAI_KEY, 'OPENAI_TEST_API_KEY not set in e2e/.env.test');
|
||||
|
||||
test('full generation pipeline produces valid synthesis', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
// Navigate to app first (required for cookie domain), then login
|
||||
// OpenAI provider is already enabled from the migration seed data
|
||||
await page.goto('/');
|
||||
await loginAsUser(page);
|
||||
|
||||
// Step 2: Configure settings
|
||||
const settingsResp = await apiCall(page, 'PUT', '/api/v1/settings', {
|
||||
theme: 'AI Weekly',
|
||||
max_age_days: 7,
|
||||
categories: ['AI News'],
|
||||
max_items_per_category: 5,
|
||||
search_agent_behavior: '',
|
||||
ai_provider: 'openai',
|
||||
ai_model: 'gpt-4o-mini',
|
||||
ai_model_writing: 'gpt-4o-mini',
|
||||
});
|
||||
expect(settingsResp.status).toBe(200);
|
||||
|
||||
// Step 3: Store the real OpenAI API key
|
||||
const keyResp = await apiCall(page, 'POST', '/api/v1/user/api-keys', {
|
||||
provider_name: 'openai',
|
||||
api_key: OPENAI_KEY,
|
||||
});
|
||||
expect(keyResp.status).toBe(200);
|
||||
|
||||
// Step 4: Add a source
|
||||
const sourceResp = await apiCall(page, 'POST', '/api/v1/sources', {
|
||||
title: 'OpenAI Blog',
|
||||
url: 'https://openai.com/blog',
|
||||
});
|
||||
expect(sourceResp.status).toBe(200);
|
||||
|
||||
// Step 5: Trigger generation
|
||||
const genResp = await apiCall(
|
||||
page,
|
||||
'POST',
|
||||
'/api/v1/syntheses/generate',
|
||||
);
|
||||
expect(genResp.status).toBe(202);
|
||||
const jobId = genResp.data.job_id;
|
||||
expect(jobId).toBeTruthy();
|
||||
|
||||
// Step 6: Wait for SSE completion
|
||||
const synthesisId = await waitForGenerationComplete(page, jobId);
|
||||
expect(synthesisId).toBeTruthy();
|
||||
|
||||
// Step 7: Fetch the full synthesis
|
||||
const synthResp = await apiCall(
|
||||
page,
|
||||
'GET',
|
||||
`/api/v1/syntheses/${synthesisId}`,
|
||||
);
|
||||
expect(synthResp.status).toBe(200);
|
||||
const synthesis = synthResp.data;
|
||||
|
||||
// Step 8: Validate structure
|
||||
expect(synthesis.status).toBe('completed');
|
||||
expect(synthesis.sections).toBeDefined();
|
||||
expect(synthesis.sections.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
for (const section of synthesis.sections) {
|
||||
// Section has a title (category name)
|
||||
expect(section.title).toBeTruthy();
|
||||
expect(typeof section.title).toBe('string');
|
||||
|
||||
// Section has items
|
||||
expect(section.items).toBeDefined();
|
||||
expect(section.items.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
for (const item of section.items) {
|
||||
// Each item has a non-empty title
|
||||
expect(item.title).toBeTruthy();
|
||||
expect(typeof item.title).toBe('string');
|
||||
|
||||
// Each item URL starts with http
|
||||
expect(item.url).toBeTruthy();
|
||||
expect(item.url.startsWith('http')).toBe(true);
|
||||
|
||||
// Each item summary is non-trivial (> 50 chars)
|
||||
expect(item.summary).toBeTruthy();
|
||||
expect(item.summary.length).toBeGreaterThan(50);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the test skips without `.env.test`**
|
||||
|
||||
Run (from the e2e directory, with the Docker stack running):
|
||||
```bash
|
||||
cd e2e && npx playwright test generation-live --reporter=list
|
||||
```
|
||||
Expected: test shows as "skipped" with reason "OPENAI_TEST_API_KEY not set"
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add e2e/tests/generation-live.spec.ts
|
||||
git commit -m "test: add live generation UAT with real OpenAI API key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Run the full UAT with a real key
|
||||
|
||||
This task is manual — requires a real OpenAI API key.
|
||||
|
||||
- [ ] **Step 1: Create `e2e/.env.test` with your real key**
|
||||
|
||||
```bash
|
||||
cp e2e/.env.test.example e2e/.env.test
|
||||
# Edit e2e/.env.test and set OPENAI_TEST_API_KEY=sk-your-real-key
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Start the E2E Docker stack**
|
||||
|
||||
```bash
|
||||
cd e2e && docker compose -f docker-compose.test.yml up --build -d
|
||||
# Wait for health checks to pass
|
||||
docker compose -f docker-compose.test.yml ps
|
||||
# Run seed
|
||||
npx tsx seed.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the live generation test**
|
||||
|
||||
```bash
|
||||
npx playwright test generation-live --reporter=list
|
||||
```
|
||||
|
||||
Expected: test passes in 60-120s, showing "1 passed".
|
||||
|
||||
- [ ] **Step 4: Verify in Docker logs**
|
||||
|
||||
```bash
|
||||
docker logs ai-synth-test 2>&1 | grep -E "Generation completed|OpenAI"
|
||||
```
|
||||
|
||||
Expected: log lines showing successful LLM calls and generation completion.
|
||||
@ -0,0 +1,67 @@
|
||||
# Design: Error Location Logging
|
||||
|
||||
**Date**: 2026-03-23
|
||||
**Scope**: Add source file and line number to WARN and ERROR log lines
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
During the first Docker deployment, several runtime errors (non-existent table, VARCHAR constraint violation) produced log lines like `ERROR ai_synth_backend::errors: Database error: PgDatabaseError { ... }` with no indication of which source file or function triggered the error. Diagnosing required grep-ing the codebase for the SQL string.
|
||||
|
||||
## Goal
|
||||
|
||||
WARN and ERROR log lines include the source file and line number. INFO and DEBUG remain unchanged.
|
||||
|
||||
## Approach
|
||||
|
||||
Create a custom `tracing_subscriber` event formatter that conditionally appends `[file:line]` when the event level is WARN or ERROR.
|
||||
|
||||
### New file: `backend/src/logging.rs`
|
||||
|
||||
~30 lines. Implements `FormatEvent` trait:
|
||||
- For WARN/ERROR: appends `[src/file.rs:123]` after the target module
|
||||
- For INFO/DEBUG/TRACE: standard format, no file:line
|
||||
|
||||
### Changes to `backend/src/main.rs`
|
||||
|
||||
Replace:
|
||||
```rust
|
||||
fmt().with_env_filter(filter).init();
|
||||
```
|
||||
|
||||
With:
|
||||
```rust
|
||||
fmt().with_env_filter(filter).event_format(logging::ErrorLocationFormat).init();
|
||||
```
|
||||
|
||||
Add `mod logging;` to `main.rs` (alongside the existing `mod cli;` — both are binary-only concerns).
|
||||
|
||||
### Output examples
|
||||
|
||||
**INFO (unchanged):**
|
||||
```
|
||||
2026-03-23T10:00:00Z INFO ai_synth_backend::services::auth: Magic link token created email="test@example.com"
|
||||
```
|
||||
|
||||
**ERROR (with file:line):**
|
||||
```
|
||||
2026-03-23T10:00:00Z ERROR ai_synth_backend::errors [src/errors.rs:91]: Database error: PgDatabaseError { ... }
|
||||
```
|
||||
|
||||
**WARN (with file:line):**
|
||||
```
|
||||
2026-03-23T10:00:00Z WARN ai_synth_backend::handlers::auth [src/handlers/auth.rs:75]: Rate limit exceeded for email
|
||||
```
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- No changes to any `tracing::error!` or `tracing::warn!` call sites
|
||||
- No changes to error types or error handling logic
|
||||
- No changes to the JSON error response sent to clients
|
||||
- INFO/DEBUG/TRACE output format unchanged
|
||||
|
||||
## Testing
|
||||
|
||||
- `cargo test --lib` — existing tests pass (formatter is only used in main.rs, not in tests)
|
||||
- Manual: run the app, trigger an error, verify file:line appears in log output
|
||||
@ -0,0 +1,79 @@
|
||||
# Design: Synthesis Generation UAT with Real OpenAI API
|
||||
|
||||
**Date**: 2026-03-23
|
||||
**Scope**: Automated Playwright E2E test that exercises the full generation pipeline with a real OpenAI API key
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The generation pipeline has never been tested end-to-end with a real LLM API call. Unit and integration tests use fake keys or mock the LLM layer. Runtime bugs (wrong table name, missing schema fields) were only discovered during manual Docker testing. A UAT with a real API key validates the entire pipeline: settings, key encryption, model resolution, LLM call, response parsing, and synthesis storage.
|
||||
|
||||
## Approach
|
||||
|
||||
Add a single Playwright spec (`e2e/tests/generation-live.spec.ts`) that calls the backend API directly with a real OpenAI key. The test is gated behind a `.env.test` file — it skips when no key is available, so it doesn't break CI or other developers.
|
||||
|
||||
## Files
|
||||
|
||||
- **Create:** `e2e/tests/generation-live.spec.ts` — the test
|
||||
- **Create:** `e2e/.env.test.example` — template showing required vars
|
||||
- **Modify:** `e2e/.gitignore` — add `.env.test`
|
||||
|
||||
No changes to existing tests, docker-compose, or Playwright config.
|
||||
|
||||
## Test Flow
|
||||
|
||||
1. Load `OPENAI_TEST_API_KEY` from `e2e/.env.test` via `dotenv`. Skip if missing.
|
||||
2. Override test timeout to 180s (`test.setTimeout(180_000)`) — real LLM calls take 30-120s.
|
||||
3. Login as seeded user via `loginAsUser()` helper (sets session cookie on the page context).
|
||||
4. Ensure OpenAI provider is enabled — `page.request.fetch` PUT to admin providers API, or use the seeded admin user.
|
||||
5. PUT `/api/v1/settings` with **all required fields**:
|
||||
```json
|
||||
{
|
||||
"theme": "AI Weekly",
|
||||
"max_age_days": 7,
|
||||
"categories": ["AI News"],
|
||||
"max_items_per_category": 5,
|
||||
"search_agent_behavior": "",
|
||||
"ai_provider": "openai",
|
||||
"ai_model": "gpt-4o-mini",
|
||||
"ai_model_writing": "gpt-4o-mini"
|
||||
}
|
||||
```
|
||||
6. POST `/api/v1/user/api-keys` — store the real OpenAI key (provider: `"openai"`).
|
||||
7. POST `/api/v1/sources` — add a source (e.g., `https://openai.com/blog`).
|
||||
8. POST `/api/v1/syntheses/generate` — trigger generation, get `job_id`.
|
||||
9. Consume SSE stream via `page.evaluate()` using `EventSource` in the browser context (where the session cookie is available). Wait for `complete` event, JSON-parse the `data` field to extract `synthesis_id`. Timeout: 120s.
|
||||
10. GET `/api/v1/syntheses/:synthesis_id` — fetch the full synthesis.
|
||||
11. Validate structure and content (see below).
|
||||
|
||||
**CSRF**: All PUT/POST/DELETE calls must include `X-Requested-With: XMLHttpRequest` header. Use `page.evaluate()` with `fetch()` in the browser context (which shares the session cookie), or use Playwright's `request` fixture with explicit cookie and header management.
|
||||
|
||||
Uses `gpt-4o-mini` to keep cost under $0.01 per run.
|
||||
|
||||
## Validation Assertions
|
||||
|
||||
- Synthesis has status `"completed"`
|
||||
- At least 1 section exists
|
||||
- Each section has a `title` field (the category name) matching configured categories
|
||||
- Each section has `items` array with at least 1 entry
|
||||
- Each item has:
|
||||
- `title`: non-empty string
|
||||
- `url`: starts with `"http"`
|
||||
- `summary`: string with length > 50 characters
|
||||
|
||||
No assertion on content quality — only structural integrity and non-trivial output.
|
||||
|
||||
## Gating
|
||||
|
||||
- `.env.test` is gitignored — never committed
|
||||
- `.env.test.example` is committed as a template: `OPENAI_TEST_API_KEY=sk-your-key-here`
|
||||
- The test uses `test.skip()` if the env var is not set
|
||||
- Existing `npx playwright test` (without `.env.test`) continues to work unchanged — this test simply skips
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- Existing E2E tests and their docker-compose
|
||||
- Playwright config (the new spec runs alongside existing specs)
|
||||
- Backend code — no changes
|
||||
- Frontend code — no changes
|
||||
Loading…
Reference in New Issue