Skip to content
Open
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 .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ANTHROPIC_API_KEY=your-api-key-here

# Optional settings
# CLAUDE_MODEL=claude-sonnet-4-20250514
# ASSISTANT_PORT=3000
# ASSISTANT_HOST=localhost
89 changes: 73 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,57 @@ Tests the OS tab switching on the [Installation](https://manual.manticoresearch.
| `switch to Kubernetes` | Kubernetes tab shows `helm` commands |
| `switching tabs changes content and restores` | RHEL → Docker → RHEL: content changes and restores |

### `navigation.spec.ts` — Sidebar navigation
Checks that the sidebar contains expected sections and links work correctly.

| Test | What it verifies |
|---|---|
| `sidebar contains expected sections` | Page body contains Introduction, Installation, Quick start guide, Searching |
| `clicking sidebar link navigates to correct page` | Clicking "Quick start guide" navigates to `/Quick_start_guide` |
| `page heading matches page content` | Quick Start page contains "quick start" text |
| `page title reflects current page` | Browser tab title contains "quick start" |

### `anchors.spec.ts` — Page anchors and deep linking
Verifies that anchor links point to existing elements and deep linking works.

| Test | What it verifies |
|---|---|
| `anchor links on Quick Start page point to existing elements` | All `#` anchor links have valid targets on the page |
| `headings have named anchors for deep linking` | Page has `<a class="anchor" name="...">` elements for section linking |

### `copy-button.spec.ts` — Code block copy button
Tests the clipboard copy button on code examples.

| Test | What it verifies |
|---|---|
| `code blocks have a copy button` | Code examples have visible `.copy-btn` buttons |
| `copy button responds to click` | Clicking a copy button doesn't throw an error |

### `broken-links.spec.ts` — Link integrity
Checks that links on the page return valid HTTP responses.

| Test | What it verifies |
|---|---|
| `sidebar navigation links return 200` | All sidebar links return HTTP 200 |
| `main page external links are reachable` | External links return HTTP status < 400 |

### `page-404.spec.ts` — 404 error handling
Tests behavior when navigating to a non-existent page.

| Test | What it verifies |
|---|---|
| `non-existent page redirects or shows error` | Non-existent URL returns status < 500 (no server error) |
| `non-existent page still has working navigation` | Page still has links and meaningful content |

### `mobile-viewport.spec.ts` — Mobile responsiveness
Tests the documentation on an iPhone 12 viewport (390×844).

| Test | What it verifies |
|---|---|
| `page loads on mobile viewport` | Title is non-empty, H1 is visible |
| `content is readable without horizontal scroll` | Body width ≤ viewport width |
| `code blocks are visible on mobile` | Code examples are rendered on small screens |

---

## How to run tests
Expand Down Expand Up @@ -127,7 +178,7 @@ npx playwright codegen manual.manticoresearch.com
### When tests pass

```
19 passed (33.6s)
34 passed (50.1s)
```

### When tests fail
Expand Down Expand Up @@ -190,22 +241,28 @@ npm run test:ui

```
doc-tests-playwright/
├── tests/ # Test files
│ ├── basic-connection.spec.ts # Smoke test
│ ├── search.spec.ts # Search functionality
│ ├── code-tabs.spec.ts # SQL/HTTP/PHP/Python tabs
│ ├── language-switcher.spec.ts # EN/RU/ZH switching
│ └── os-tabs.spec.ts # OS installation tabs
├── pages/ # Page Object Models
│ ├── manual.page.ts # Common page elements (language selector, heading)
│ └── code-tabs.page.ts # Tab block logic (code tabs + OS tabs)
├── playwright.config.ts # Playwright configuration
├── docker-compose.yml # Docker setup for local runs
├── Dockerfile # Docker image for tests
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── tests/ # Test files (34 tests)
│ ├── basic-connection.spec.ts # Smoke test
│ ├── search.spec.ts # Search functionality
│ ├── code-tabs.spec.ts # SQL/HTTP/PHP/Python tabs
│ ├── language-switcher.spec.ts # EN/RU/ZH switching
│ ├── os-tabs.spec.ts # OS installation tabs
│ ├── navigation.spec.ts # Sidebar navigation
│ ├── anchors.spec.ts # Page anchors and deep linking
│ ├── copy-button.spec.ts # Code block copy button
│ ├── broken-links.spec.ts # Link integrity
│ ├── page-404.spec.ts # 404 error handling
│ └── mobile-viewport.spec.ts # Mobile responsiveness
├── pages/ # Page Object Models
│ ├── manual.page.ts # Common page elements (language selector, heading)
│ └── code-tabs.page.ts # Tab block logic (code tabs + OS tabs)
├── playwright.config.ts # Playwright configuration
├── docker-compose.yml # Docker setup for local runs
├── Dockerfile # Docker image for tests
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── .github/
├── workflows/playwright.yml # CI pipeline
├── workflows/playwright.yml # CI pipeline
└── FUNDING.yml
```

Expand Down
127 changes: 127 additions & 0 deletions assistant/ai/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Anthropic from '@anthropic-ai/sdk';
import type { MessageParam, ContentBlockParam } from '@anthropic-ai/sdk/resources/messages';
import type { Response } from 'express';
import { tools } from './tools';
import { handleTool } from './tool-handlers';
import { buildSystemPrompt } from './system-prompt';

let client: Anthropic | null = null;

function getClient(): Anthropic {
if (!client) {
client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
}
return client;
}

const MAX_ITERATIONS = 15;

export async function streamChat(
messages: MessageParam[],
res: Response,
): Promise<void> {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();

const systemPrompt = buildSystemPrompt();

try {
await agenticLoop(messages, systemPrompt, res);
} catch (err: any) {
sendSSE(res, { type: 'error', message: err.message });
}

res.write('data: [DONE]\n\n');
res.end();
}

async function agenticLoop(
messages: MessageParam[],
systemPrompt: string,
res: Response,
): Promise<void> {
for (let i = 0; i < MAX_ITERATIONS; i++) {
// Trim conversation if too long
trimConversation(messages);

const model = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
const stream = getClient().messages.stream({
model,
max_tokens: 8192,
system: systemPrompt,
tools,
messages,
});

// Stream text deltas in real time
stream.on('text', (text) => {
sendSSE(res, { type: 'text_delta', content: text });
});

const response = await stream.finalMessage();
const assistantContent = response.content;

// Add assistant message to history
messages.push({ role: 'assistant', content: assistantContent });

// Find tool_use blocks
const toolUses = assistantContent.filter(
(b): b is Anthropic.Messages.ToolUseBlock => b.type === 'tool_use',
);

if (toolUses.length === 0) {
// No tool calls — done
return;
}

// Execute tools and send indicators to the frontend
const toolResults: ContentBlockParam[] = [];

for (const toolUse of toolUses) {
sendSSE(res, {
type: 'tool_call',
name: toolUse.name,
input: toolUse.input,
});

const result = await handleTool(
toolUse.name,
toolUse.input as Record<string, string>,
);

sendSSE(res, {
type: 'tool_result',
name: toolUse.name,
result: result.length > 1000 ? result.substring(0, 1000) + '...' : result,
});

toolResults.push({
type: 'tool_result',
tool_use_id: toolUse.id,
content: result,
});
}

// Add tool results and loop
messages.push({ role: 'user', content: toolResults });
}
}

function trimConversation(messages: MessageParam[]): void {
const MAX_CHARS = 150_000;
let total = messages.reduce(
(sum, m) => sum + JSON.stringify(m.content).length,
0,
);

while (total > MAX_CHARS && messages.length > 4) {
const removed = messages.shift();
total -= JSON.stringify(removed!.content).length;
}
}

function sendSSE(res: Response, data: Record<string, any>): void {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
89 changes: 89 additions & 0 deletions assistant/ai/system-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from 'fs';
import path from 'path';
import { PROJECT_ROOT } from '../utils/security';

export function buildSystemPrompt(): string {
// Dynamically read test file list
const testsDir = path.join(PROJECT_ROOT, 'tests');
let testFiles: string[] = [];
try {
testFiles = fs.readdirSync(testsDir).filter((f) => f.endsWith('.spec.ts'));
} catch {
// tests dir might not exist yet
}

const testList = testFiles.map((f) => `- tests/${f}`).join('\n');

return `You are an AI assistant for a Playwright test suite that tests the Manticore Search documentation website (https://manual.manticoresearch.com).

## Your role
You help a QA team create, run, and manage Playwright UI tests. You can create new test files, modify existing ones, run tests, and create GitHub PRs.

## Project context
- Target: https://manual.manticoresearch.com (live documentation site)
- Framework: Playwright + TypeScript
- Config: playwright.config.ts (baseURL set, Chromium only, 30s timeout)
- Tests directory: tests/ (${testFiles.length} spec files)
- Page objects: pages/ (ManualPage for language switching, CodeTabsPage for code/OS tabs)

## Existing test files
${testList}

## Test writing conventions (follow exactly)
1. Every test file starts with:
import { test, expect } from '@playwright/test';

2. Tests are wrapped in test.describe('Feature Name', () => { ... });

3. Page navigation:
await page.goto('/path');
await page.waitForLoadState('networkidle');

4. Use page objects from pages/ for complex components:
import { CodeTabsPage } from '../pages/code-tabs.page';
import { ManualPage } from '../pages/manual.page';

5. Selectors (preference order):
- CSS: page.locator('.class'), page.locator('#id')
- Text: page.locator('a', { hasText: 'Text' })
- XPath only for complex DOM: locator('xpath=ancestor::div[...]')

6. Assertions:
- await expect(page).toHaveURL(/pattern/);
- await expect(locator).toBeVisible();
- await expect(locator).toContainText('text');
- expect(value).toBeGreaterThan(0);

7. Device-specific tests use test.use({...}) at file level

8. File naming: tests/feature-name.spec.ts (kebab-case, .spec.ts suffix)

## Key DOM selectors on the documentation site
- Search: #query (input), .search-res-item (results)
- Sidebar: #docs-tree (tree), a[href] (links)
- Code blocks: div.example > div.example-header > div.lang-sel > ul.lang-tabs > li
- Code content: div.example > div.example-body
- Copy button: .copy-btn.example-btn:visible
- Language selector: #language-select
- Anchors: a.anchor[name]

## Workflow for new tests
1. Read existing similar tests to match the style
2. Create the test file in tests/
3. Run the test to verify it passes
4. If it fails, analyze errors and fix
5. Once passing, offer to create a branch and PR

## Git & PR rules
- Commit messages: always in English, short, no Co-Authored-By
- PR titles and descriptions: always in English
- Branch names: always in English, kebab-case (e.g. test/add-console-errors)

## Rules
- Only create files in tests/ and pages/
- Never modify config files
- Explain what you're doing before using tools
- When tests fail, analyze and fix — don't give up
- Keep responses concise
- Respond in the same language the user uses`;
}
Loading
Loading