Compare commits
17 Commits
e91886a1ce
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 57319e6712 | |||
| a553cc6130 | |||
| 5fc02f8d9b | |||
| ef0725be58 | |||
| b457e94ccb | |||
| 4abe8183d8 | |||
| bb650a3ed9 | |||
| 9df7278d55 | |||
| a8da8753f1 | |||
| 0ea3296885 | |||
| 39aea34026 | |||
| 1ff9a2cf4b | |||
| a520814771 | |||
| d072b71eec | |||
| 63c955c848 | |||
| a4739bddc1 | |||
| 57d5405c41 |
59
.claude/agents/playwright-test-generator.md
Normal file
59
.claude/agents/playwright-test-generator.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: playwright-test-generator
|
||||
description: Use this agent when you need to create automated browser tests using Playwright. Examples: <example>Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the generator agent to create and validate this login test for you' <commentary> The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. </commentary></example><example>Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the generator agent to build a comprehensive checkout flow test' <commentary> This is a complex user journey that needs to be automated and tested, perfect for the generator agent. </commentary></example>
|
||||
tools: Glob, Grep, Read, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test
|
||||
model: sonnet
|
||||
color: blue
|
||||
---
|
||||
|
||||
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
|
||||
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
|
||||
application behavior.
|
||||
|
||||
# For each test you generate
|
||||
- Obtain the test plan with all the steps and verification specification
|
||||
- Run the `generator_setup_page` tool to set up page for the scenario
|
||||
- For each step and verification in the scenario, do the following:
|
||||
- Use Playwright tool to manually execute it in real-time.
|
||||
- Use the step description as the intent for each Playwright tool call.
|
||||
- Retrieve generator log via `generator_read_log`
|
||||
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
|
||||
- File should contain single test
|
||||
- File name must be fs-friendly scenario name
|
||||
- Test must be placed in a describe matching the top-level test plan item
|
||||
- Test title must match the scenario name
|
||||
- Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
|
||||
multiple actions.
|
||||
- Always use best practices from the log when generating tests.
|
||||
|
||||
<example-generation>
|
||||
For following plan:
|
||||
|
||||
```markdown file=specs/plan.md
|
||||
### 1. Adding New Todos
|
||||
**Seed:** `tests/seed.spec.ts`
|
||||
|
||||
#### 1.1 Add Valid Todo
|
||||
**Steps:**
|
||||
1. Click in the "What needs to be done?" input field
|
||||
|
||||
#### 1.2 Add Multiple Todos
|
||||
...
|
||||
```
|
||||
|
||||
Following file is generated:
|
||||
|
||||
```ts file=add-valid-todo.spec.ts
|
||||
// spec: specs/plan.md
|
||||
// seed: tests/seed.spec.ts
|
||||
|
||||
test.describe('Adding New Todos', () => {
|
||||
test('Add Valid Todo', async { page } => {
|
||||
// 1. Click in the "What needs to be done?" input field
|
||||
await page.click(...);
|
||||
|
||||
...
|
||||
});
|
||||
});
|
||||
```
|
||||
</example-generation>
|
||||
45
.claude/agents/playwright-test-healer.md
Normal file
45
.claude/agents/playwright-test-healer.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: playwright-test-healer
|
||||
description: Use this agent when you need to debug and fix failing Playwright tests. Examples: <example>Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the healer agent to debug and fix the failing login test.' <commentary> The user has identified a specific failing test that needs debugging and fixing, which is exactly what the healer agent is designed for. </commentary></example><example>Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the healer agent to investigate and fix the user-registration test.' <commentary> A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. </commentary></example>
|
||||
tools: Glob, Grep, Read, Write, Edit, MultiEdit, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run
|
||||
model: sonnet
|
||||
color: red
|
||||
---
|
||||
|
||||
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
|
||||
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
|
||||
broken Playwright tests using a methodical approach.
|
||||
|
||||
Your workflow:
|
||||
1. **Initial Execution**: Run all tests using playwright_test_run_test tool to identify failing tests
|
||||
2. **Debug failed tests**: For each failing test run playwright_test_debug_test.
|
||||
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
|
||||
- Examine the error details
|
||||
- Capture page snapshot to understand the context
|
||||
- Analyze selectors, timing issues, or assertion failures
|
||||
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
|
||||
- Element selectors that may have changed
|
||||
- Timing and synchronization issues
|
||||
- Data dependencies or test environment problems
|
||||
- Application changes that broke test assumptions
|
||||
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
|
||||
- Updating selectors to match current application state
|
||||
- Fixing assertions and expected values
|
||||
- Improving test reliability and maintainability
|
||||
- For inherently dynamic data, utilize regular expressions to produce resilient locators
|
||||
6. **Verification**: Restart the test after each fix to validate the changes
|
||||
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
|
||||
|
||||
Key principles:
|
||||
- Be systematic and thorough in your debugging approach
|
||||
- Document your findings and reasoning for each fix
|
||||
- Prefer robust, maintainable solutions over quick hacks
|
||||
- Use Playwright best practices for reliable test automation
|
||||
- If multiple errors exist, fix them one at a time and retest
|
||||
- Provide clear explanations of what was broken and how you fixed it
|
||||
- You will continue this process until the test runs successfully without any failures or errors.
|
||||
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
|
||||
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
|
||||
of the expected behavior.
|
||||
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
|
||||
- Never wait for networkidle or use other discouraged or deprecated apis
|
||||
93
.claude/agents/playwright-test-planner.md
Normal file
93
.claude/agents/playwright-test-planner.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: playwright-test-planner
|
||||
description: Use this agent when you need to create comprehensive test plan for a web application or website. Examples: <example>Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test scenarios.' <commentary> The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. </commentary></example><example>Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test scenarios.' <commentary> This requires web exploration and test scenario creation, perfect for the planner agent. </commentary></example>
|
||||
tools: Glob, Grep, Read, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
|
||||
scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
|
||||
planning.
|
||||
|
||||
You will:
|
||||
|
||||
1. **Navigate and Explore**
|
||||
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
|
||||
- Explore the browser snapshot
|
||||
- Do not take screenshots unless absolutely necessary
|
||||
- Use browser_* tools to navigate and discover interface
|
||||
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
|
||||
|
||||
2. **Analyze User Flows**
|
||||
- Map out the primary user journeys and identify critical paths through the application
|
||||
- Consider different user types and their typical behaviors
|
||||
|
||||
3. **Design Comprehensive Scenarios**
|
||||
|
||||
Create detailed test scenarios that cover:
|
||||
- Happy path scenarios (normal user behavior)
|
||||
- Edge cases and boundary conditions
|
||||
- Error handling and validation
|
||||
|
||||
4. **Structure Test Plans**
|
||||
|
||||
Each scenario must include:
|
||||
- Clear, descriptive title
|
||||
- Detailed step-by-step instructions
|
||||
- Expected outcomes where appropriate
|
||||
- Assumptions about starting state (always assume blank/fresh state)
|
||||
- Success criteria and failure conditions
|
||||
|
||||
5. **Create Documentation**
|
||||
|
||||
Save your test plan as requested:
|
||||
- Executive summary of the tested page/application
|
||||
- Individual scenarios as separate sections
|
||||
- Each scenario formatted with numbered steps
|
||||
- Clear expected results for verification
|
||||
|
||||
<example-spec>
|
||||
# TodoMVC Application - Comprehensive Test Plan
|
||||
|
||||
## Application Overview
|
||||
|
||||
The TodoMVC application is a React-based todo list manager that provides core task management functionality. The
|
||||
application features:
|
||||
|
||||
- **Task Management**: Add, edit, complete, and delete individual todos
|
||||
- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos
|
||||
- **Filtering**: View todos by All, Active, or Completed status
|
||||
- **URL Routing**: Support for direct navigation to filtered views via URLs
|
||||
- **Counter Display**: Real-time count of active (incomplete) todos
|
||||
- **Persistence**: State maintained during session (browser refresh behavior not tested)
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Adding New Todos
|
||||
|
||||
**Seed:** `tests/seed.spec.ts`
|
||||
|
||||
#### 1.1 Add Valid Todo
|
||||
**Steps:**
|
||||
1. Click in the "What needs to be done?" input field
|
||||
2. Type "Buy groceries"
|
||||
3. Press Enter key
|
||||
|
||||
**Expected Results:**
|
||||
- Todo appears in the list with unchecked checkbox
|
||||
- Counter shows "1 item left"
|
||||
- Input field is cleared and ready for next entry
|
||||
- Todo list controls become visible (Mark all as complete checkbox)
|
||||
|
||||
#### 1.2
|
||||
...
|
||||
</example-spec>
|
||||
|
||||
**Quality Standards**:
|
||||
- Write steps that are specific enough for any tester to follow
|
||||
- Include negative testing scenarios
|
||||
- Ensure scenarios are independent and can be run in any order
|
||||
|
||||
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
|
||||
professional formatting suitable for sharing with development and QA teams.
|
||||
63
.gitea/workflows/magnitude.yml
Normal file
63
.gitea/workflows/magnitude.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
# Gitea Actions workflow for running Magnitude tests
|
||||
# Uses docker-compose.ci.yml for fully containerized testing
|
||||
name: Magnitude Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, development]
|
||||
pull_request:
|
||||
branches: [main, development]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create .env file for CI
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
SURREALDB_URL=ws://surrealdb:8000/rpc
|
||||
SURREALDB_USER=root
|
||||
SURREALDB_PASS=root
|
||||
SURREALDB_NS=ponderants
|
||||
SURREALDB_DB=main
|
||||
SURREALDB_JWT_SECRET=${{ secrets.SURREALDB_JWT_SECRET }}
|
||||
ATPROTO_CLIENT_ID=${{ secrets.ATPROTO_CLIENT_ID }}
|
||||
ATPROTO_REDIRECT_URI=${{ secrets.ATPROTO_REDIRECT_URI }}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
DEEPGRAM_API_KEY=${{ secrets.DEEPGRAM_API_KEY }}
|
||||
TEST_BLUESKY_HANDLE=${{ secrets.TEST_BLUESKY_HANDLE }}
|
||||
TEST_BLUESKY_PASSWORD=${{ secrets.TEST_BLUESKY_PASSWORD }}
|
||||
ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
|
||||
EOF
|
||||
|
||||
- name: Run tests with docker-compose
|
||||
run: |
|
||||
docker compose -f docker-compose.ci.yml --profile test up \
|
||||
--abort-on-container-exit \
|
||||
--exit-code-from magnitude
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== SurrealDB Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs surrealdb
|
||||
echo "=== Next.js Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs nextjs
|
||||
echo "=== Magnitude Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs magnitude
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: magnitude-results
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: docker compose -f docker-compose.ci.yml down -v
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -46,3 +47,6 @@ tests/playwright/.auth/
|
||||
|
||||
# claude settings (keep .claude/CLAUDE.md but ignore user settings)
|
||||
.claude/settings.local.json
|
||||
|
||||
# surrealdb data
|
||||
surreal/data/
|
||||
|
||||
11
.mcp.json
Normal file
11
.mcp.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-test": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"playwright",
|
||||
"run-test-mcp-server"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
205
AGENTS.md
205
AGENTS.md
@@ -17,6 +17,211 @@ EOF
|
||||
|
||||
These credentials should be used for all automated testing (Magnitude, Playwright) and manual testing when needed. Do not attempt to authenticate without using these credentials.
|
||||
|
||||
**Database Setup**: The application uses SurrealDB running in Docker Compose for the app cache layer:
|
||||
|
||||
1. Start the database services:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
2. This starts two services:
|
||||
- `surrealdb`: The main SurrealDB instance (port 8000)
|
||||
- `surrealmcp`: SurrealMCP server for MCP access (port 8080)
|
||||
|
||||
3. Start the Next.js development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. To stop the services:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
5. Configuration:
|
||||
- SurrealDB runs in-memory mode (data is not persisted between restarts)
|
||||
- Namespace: `ponderants`
|
||||
- Database: `main`
|
||||
- Credentials: `root/root`
|
||||
|
||||
**Note**: Always start docker compose services before starting the Next.js dev server to ensure the database is available.
|
||||
|
||||
**Testing Workflow**: All new features must follow a rigorous testing process before being committed:
|
||||
|
||||
1. **Manual Testing with Playwright MCP**:
|
||||
- Use Playwright MCP tools to manually test all functionality interactively
|
||||
- Test both happy paths (expected user flows) and unhappy paths (errors, edge cases)
|
||||
- Document each step you verify during manual testing - these become test cases
|
||||
- If you encounter issues during manual testing (e.g., 404 errors, unexpected behavior), investigate and fix them before proceeding
|
||||
- Use the following pattern:
|
||||
```
|
||||
1. Navigate to the feature
|
||||
2. Perform user actions (clicks, typing, etc.)
|
||||
3. Verify expected outcomes
|
||||
4. Test error scenarios
|
||||
5. Verify cleanup/state updates
|
||||
```
|
||||
|
||||
2. **Write Comprehensive Magnitude Tests**:
|
||||
- After manually verifying functionality with Playwright MCP, write extensive Magnitude tests covering ALL verified behaviors
|
||||
- Each manual test step should have a corresponding Magnitude test assertion
|
||||
- Test files are located in `tests/magnitude/` with `.mag.ts` extension
|
||||
- Use the test credentials from .env (TEST_BLUESKY_HANDLE, TEST_BLUESKY_PASSWORD)
|
||||
- Include both happy path and unhappy path test cases
|
||||
- Example test structure:
|
||||
```typescript
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||
|
||||
test('Feature description', async (agent) => {
|
||||
await agent.act('Navigate to /page');
|
||||
await agent.act('Perform user action');
|
||||
await agent.check('Verify expected outcome');
|
||||
});
|
||||
```
|
||||
|
||||
3. **Reusable Playwright Scaffolding**:
|
||||
- Abstract common patterns (auth, navigation, etc.) into helper files in `tests/playwright/helpers/`
|
||||
- These helpers should be usable both during manual Playwright MCP testing AND by Magnitude tests
|
||||
- Examples: `tests/playwright/helpers/chat.ts`, `tests/playwright/helpers/galaxy.ts`, `tests/playwright/helpers/node.ts`
|
||||
- For auth setup, use Playwright's global setup pattern (see https://playwright.dev/docs/test-global-setup-teardown)
|
||||
- Current auth setup: `tests/playwright/auth.setup.ts`
|
||||
|
||||
4. **Generating Playwright Code**:
|
||||
- Use https://playwright.dev/docs/test-agents to generate Playwright test code when helpful
|
||||
- This tool can convert natural language test descriptions into Playwright code
|
||||
|
||||
5. **Test Execution**:
|
||||
- Run Magnitude tests: `pnpm test` or `npx magnitude`
|
||||
- Ensure ALL tests pass before committing
|
||||
- If tests fail, fix the implementation or update the tests to match the correct behavior
|
||||
|
||||
6. **Pre-Commit Checklist**:
|
||||
- ✅ All manual testing with Playwright MCP completed and verified
|
||||
- ✅ All Magnitude tests written and cover all verified functionality
|
||||
- ✅ Database verified for expected state after operations (e.g., deletions actually removed records)
|
||||
- ✅ Run magnitude tests for current feature FIRST: `pnpm test tests/magnitude/your-feature.mag.ts`
|
||||
- ✅ Verify current feature tests pass
|
||||
- ✅ Run ALL magnitude tests: `pnpm test`
|
||||
- ✅ Verify ENTIRE test suite passes
|
||||
- ✅ No console errors or warnings in production code paths
|
||||
- **CRITICAL**: Do NOT commit until ALL tests pass - feature tests AND full test suite
|
||||
- Only commit after ALL checklist items are complete
|
||||
|
||||
7. **Documentation**:
|
||||
- Document test coverage in `tests/README.md`
|
||||
- Add comments to complex test scenarios explaining the business logic being tested
|
||||
|
||||
**Testing Resources**:
|
||||
- Playwright Global Setup/Teardown: https://playwright.dev/docs/test-global-setup-teardown
|
||||
- Playwright Test Agents: https://playwright.dev/docs/test-agents
|
||||
- Playwright Docker: https://playwright.dev/docs/docker
|
||||
- Magnitude.run Documentation: https://magnitude.run/docs
|
||||
- Project Test README: `tests/README.md`
|
||||
|
||||
**Playwright Docker Setup**:
|
||||
|
||||
Playwright is integrated into docker-compose for consistent testing environments:
|
||||
|
||||
1. **Run Playwright tests with docker-compose**:
|
||||
```bash
|
||||
# Start database services
|
||||
docker compose up -d
|
||||
|
||||
# Start Next.js dev server
|
||||
pnpm dev
|
||||
|
||||
# Run Playwright tests in Docker (in another terminal)
|
||||
docker compose run --rm playwright
|
||||
```
|
||||
|
||||
2. **Alternative: Use the 'test' profile**:
|
||||
```bash
|
||||
# Start all services including Playwright
|
||||
docker compose --profile test up
|
||||
|
||||
# Or run tests one-off without keeping services up
|
||||
docker compose --profile test run --rm playwright
|
||||
```
|
||||
|
||||
3. **Benefits**:
|
||||
- Non-root user execution (pwuser) for security
|
||||
- Consistent browser versions across environments
|
||||
- Integrated with existing docker-compose setup
|
||||
- Uses host networking to access dev server on localhost:3000
|
||||
- Node modules volume prevents permission issues
|
||||
|
||||
4. **Configuration**:
|
||||
- Environment variables loaded from .env file
|
||||
- Uses `network_mode: host` to access dev server
|
||||
- Runs with `profiles: [test]` to keep it optional
|
||||
|
||||
**CI/CD with Gitea Actions**:
|
||||
|
||||
Magnitude tests run automatically on every push and pull request using a fully containerized setup:
|
||||
|
||||
1. **Configuration**: `.gitea/workflows/magnitude.yml`
|
||||
|
||||
2. **Workflow steps** (simplified to just 2 steps!):
|
||||
- Create `.env` file with secrets
|
||||
- Run `docker compose -f docker-compose.ci.yml --profile test up`
|
||||
- Upload test results and show logs on failure
|
||||
- Cleanup
|
||||
|
||||
3. **Required Secrets** (configure in Gitea repository settings):
|
||||
- `ANTHROPIC_API_KEY` - For Magnitude AI vision testing
|
||||
- `TEST_BLUESKY_HANDLE` - Test account handle
|
||||
- `TEST_BLUESKY_PASSWORD` - Test account password
|
||||
- `ATPROTO_CLIENT_ID`, `ATPROTO_REDIRECT_URI`
|
||||
- `GOOGLE_API_KEY`, `DEEPGRAM_API_KEY`
|
||||
- `SURREAL_JWT_SECRET`
|
||||
|
||||
4. **CI-specific docker-compose**: `docker-compose.ci.yml`
|
||||
- Fully containerized (SurrealDB + Next.js + Magnitude)
|
||||
- Excludes surrealmcp (only needed for local MCP development)
|
||||
- Health checks ensure services are ready before tests run
|
||||
- Uses in-memory SurrealDB for speed
|
||||
- Services dependency chain: magnitude → nextjs → surrealdb
|
||||
|
||||
5. **Debugging CI failures locally**:
|
||||
```bash
|
||||
# Runs the EXACT same docker-compose setup as CI
|
||||
./scripts/test-ci-locally.sh
|
||||
|
||||
# Or manually:
|
||||
docker compose -f docker-compose.ci.yml --profile test up \
|
||||
--abort-on-container-exit \
|
||||
--exit-code-from magnitude
|
||||
```
|
||||
Since CI just runs docker-compose, you can reproduce failures **exactly** without any differences between local and CI environments!
|
||||
|
||||
6. **Test results**: Available as workflow artifacts for 30 days
|
||||
|
||||
7. **Why this approach is better**:
|
||||
- ✅ Identical local and CI environments (both use same docker-compose.ci.yml)
|
||||
- ✅ Fast debugging (no push-test-fail cycles)
|
||||
- ✅ Self-contained (all dependencies in containers)
|
||||
- ✅ Simple (just 2 steps in CI workflow)
|
||||
- ✅ Reproducible (docker-compose ensures consistency)
|
||||
|
||||
**Testing Framework Separation**:
|
||||
|
||||
- **Playwright**: Used for manual testing with Playwright MCP and global auth setup
|
||||
- Location: `tests/playwright/`
|
||||
- Helpers: `tests/playwright/helpers.ts`
|
||||
- Auth setup: `tests/playwright/auth.setup.ts`
|
||||
- Run: `npx playwright test`
|
||||
|
||||
- **Magnitude**: Used for automated end-to-end testing in development and CI/CD
|
||||
- Location: `tests/magnitude/`
|
||||
- Helpers: `tests/magnitude/helpers.ts`
|
||||
- Configuration: `magnitude.config.ts` (uses Claude Sonnet 4.5)
|
||||
- Run: `npx magnitude` or `pnpm test`
|
||||
|
||||
Both frameworks are independent and can be used separately or together depending on the testing need.
|
||||
|
||||
You are an expert-level, full-stack AI coding agent. Your task is to implement
|
||||
the "Ponderants" application. Product Vision: Ponderants is an AI-powered
|
||||
thought partner that interviews a user to capture, structure, and visualize
|
||||
|
||||
140
app/api/nodes/[id]/route.ts
Normal file
140
app/api/nodes/[id]/route.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { Agent } from '@atproto/api';
|
||||
import { connectToDB } from '@/lib/db';
|
||||
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
||||
import { getOAuthClient } from '@/lib/auth/oauth-client';
|
||||
|
||||
/**
|
||||
* DELETE /api/nodes/[id]
|
||||
*
|
||||
* Deletes a node from both ATproto (source of truth) and SurrealDB (cache).
|
||||
*
|
||||
* Process:
|
||||
* 1. Verify user authentication and ownership
|
||||
* 2. Fetch node from SurrealDB to get atp_uri
|
||||
* 3. Delete post(s) from ATproto/Bluesky
|
||||
* 4. Delete node from SurrealDB cache
|
||||
*
|
||||
* Note: ATproto is the source of truth. If ATproto deletion fails, we don't delete from cache.
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const cookieStore = await cookies();
|
||||
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
||||
|
||||
console.log('[DELETE /api/nodes/[id]] Auth check:', {
|
||||
hasSurrealJwt: !!surrealJwt,
|
||||
});
|
||||
|
||||
if (!surrealJwt) {
|
||||
console.error('[DELETE /api/nodes/[id]] Missing auth cookie');
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verify the JWT and extract user info
|
||||
const userSession = verifySurrealJwt(surrealJwt);
|
||||
if (!userSession) {
|
||||
console.error('[DELETE /api/nodes/[id]] Invalid JWT');
|
||||
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { did: userDid } = userSession;
|
||||
const { id } = await params;
|
||||
|
||||
console.log('[DELETE /api/nodes/[id]] Deleting node:', { nodeId: id, userDid });
|
||||
|
||||
try {
|
||||
// 1. Fetch the node from SurrealDB to verify ownership and get atp_uri
|
||||
const db = await connectToDB();
|
||||
|
||||
// Parse the ID to extract table and record ID parts
|
||||
// Format: "node:e9e38d09-c0f4-4834-a6ba-c92dfa4c0910" or "node:⟨e9e38d09-c0f4-4834-a6ba-c92dfa4c0910⟩"
|
||||
const cleanId = id.replace(/^node:/, '').replace(/[⟨⟩]/g, '');
|
||||
|
||||
const nodeResult = await db.query<[Array<{ id: string; user_did: string; atp_uri: string }>]>(
|
||||
'SELECT id, user_did, atp_uri FROM node WHERE id = type::thing($table, $recordId)',
|
||||
{ table: 'node', recordId: cleanId }
|
||||
);
|
||||
|
||||
const node = nodeResult[0]?.[0];
|
||||
|
||||
if (!node) {
|
||||
console.error('[DELETE /api/nodes/[id]] Node not found:', id);
|
||||
return NextResponse.json({ error: 'Node not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 2. Verify ownership
|
||||
if (node.user_did !== userDid) {
|
||||
console.error('[DELETE /api/nodes/[id]] Unauthorized: user does not own node');
|
||||
return NextResponse.json(
|
||||
{ error: 'You do not have permission to delete this node' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Delete from ATproto (source of truth)
|
||||
try {
|
||||
const client = await getOAuthClient();
|
||||
console.log('[DELETE /api/nodes/[id]] Got OAuth client, restoring session for DID:', userDid);
|
||||
|
||||
const session = await client.restore(userDid);
|
||||
const agent = new Agent(session);
|
||||
|
||||
console.log('[DELETE /api/nodes/[id]] Successfully restored OAuth session');
|
||||
|
||||
// Parse the atp_uri to get repo and rkey
|
||||
// Format: at://did:plc:xxx/app.bsky.feed.post/xxxxx
|
||||
const atUriMatch = node.atp_uri.match(/at:\/\/([^/]+)\/([^/]+)\/(.+)/);
|
||||
if (!atUriMatch) {
|
||||
throw new Error(`Invalid atp_uri format: ${node.atp_uri}`);
|
||||
}
|
||||
|
||||
const [, repo, collection, rkey] = atUriMatch;
|
||||
|
||||
console.log('[DELETE /api/nodes/[id]] Deleting ATproto record:', {
|
||||
repo,
|
||||
collection,
|
||||
rkey,
|
||||
});
|
||||
|
||||
// Delete the post from ATproto
|
||||
await agent.api.com.atproto.repo.deleteRecord({
|
||||
repo,
|
||||
collection,
|
||||
rkey,
|
||||
});
|
||||
|
||||
console.log('[DELETE /api/nodes/[id]] ✓ Deleted post from ATproto');
|
||||
} catch (error) {
|
||||
console.error('[DELETE /api/nodes/[id]] ATproto deletion error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete post from Bluesky. Node not deleted.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Delete from SurrealDB cache (only after successful ATproto deletion)
|
||||
try {
|
||||
// Use type::thing() to properly construct the RecordId for deletion
|
||||
await db.query('DELETE FROM type::thing($table, $recordId)', {
|
||||
table: 'node',
|
||||
recordId: cleanId,
|
||||
});
|
||||
console.log('[DELETE /api/nodes/[id]] ✓ Deleted node from SurrealDB cache');
|
||||
} catch (error) {
|
||||
console.warn('[DELETE /api/nodes/[id]] ⚠ SurrealDB cache deletion failed (non-critical):', error);
|
||||
// This is non-critical since ATproto (source of truth) was successfully deleted
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, nodeId: id });
|
||||
} catch (error) {
|
||||
console.error('[DELETE /api/nodes/[id]] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete node' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
66
app/api/nodes/debug/route.ts
Normal file
66
app/api/nodes/debug/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { connectToDB } from '@/lib/db';
|
||||
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
||||
|
||||
/**
|
||||
* GET /api/nodes/debug
|
||||
*
|
||||
* Debug endpoint to list all nodes for the current user in SurrealDB.
|
||||
* Only available in development mode.
|
||||
*/
|
||||
export async function GET() {
|
||||
// Only allow in development
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
||||
|
||||
console.log('[DEBUG /api/nodes/debug] Auth check:', {
|
||||
hasSurrealJwt: !!surrealJwt,
|
||||
});
|
||||
|
||||
if (!surrealJwt) {
|
||||
console.error('[DEBUG /api/nodes/debug] Missing auth cookie');
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verify the JWT and extract user info
|
||||
const userSession = verifySurrealJwt(surrealJwt);
|
||||
if (!userSession) {
|
||||
console.error('[DEBUG /api/nodes/debug] Invalid JWT');
|
||||
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { did: userDid } = userSession;
|
||||
|
||||
try {
|
||||
const db = await connectToDB();
|
||||
|
||||
// Fetch ALL nodes for this user (no filters)
|
||||
const nodesResult = await db.query<
|
||||
[Array<{ id: string; title: string; body: string; user_did: string; atp_uri: string }>]
|
||||
>('SELECT id, title, body, user_did, atp_uri FROM node WHERE user_did = $userDid', {
|
||||
userDid,
|
||||
});
|
||||
|
||||
const nodes = nodesResult[0] || [];
|
||||
|
||||
console.log('[DEBUG /api/nodes/debug] Found nodes:', {
|
||||
count: nodes.length,
|
||||
userDid,
|
||||
nodeIds: nodes.map((n) => n.id),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
nodes,
|
||||
userDid,
|
||||
count: nodes.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[DEBUG /api/nodes/debug] Error:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch nodes' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
58
components/DeleteNodeModal.tsx
Normal file
58
components/DeleteNodeModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { Modal, Stack, Text, Group, Button } from '@mantine/core';
|
||||
import { IconTrash } from '@tabler/icons-react';
|
||||
|
||||
interface DeleteNodeModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
nodeTitle: string | null;
|
||||
isDeleting: boolean;
|
||||
}
|
||||
|
||||
export function DeleteNodeModal({
|
||||
opened,
|
||||
onClose,
|
||||
onConfirm,
|
||||
nodeTitle,
|
||||
isDeleting,
|
||||
}: DeleteNodeModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="Delete Node"
|
||||
centered
|
||||
zIndex={1001}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
Are you sure you want to delete "{nodeTitle}"? This will:
|
||||
</Text>
|
||||
<Stack gap="xs" ml="md">
|
||||
<Text size="sm">• Remove the post from Bluesky</Text>
|
||||
<Text size="sm">• Delete the node from your galaxy</Text>
|
||||
<Text size="sm" fw={600} c="red">This action cannot be undone.</Text>
|
||||
</Stack>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={onClose}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
>
|
||||
Delete Permanently
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { IconMessageCircle, IconEdit, IconChartBubbleFilled } from '@tabler/icon
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useAppMachine } from '@/hooks/useAppMachine';
|
||||
import { UserMenu } from '@/components/UserMenu';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import styles from './DesktopSidebar.module.css';
|
||||
|
||||
export function DesktopSidebar() {
|
||||
@@ -106,10 +105,7 @@ export function DesktopSidebar() {
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* User Menu - styled like other nav items */}
|
||||
{/* User Menu - styled like other nav items, now includes theme toggle */}
|
||||
<UserMenu showLabel={true} />
|
||||
|
||||
{/* Development state panel */}
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
Text,
|
||||
} from '@react-three/drei';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme } from '@mantine/core';
|
||||
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme, Button } from '@mantine/core';
|
||||
import { IconTrash } from '@tabler/icons-react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { DeleteNodeModal } from './DeleteNodeModal';
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import * as THREE from 'three';
|
||||
|
||||
@@ -96,6 +99,9 @@ export function ThoughtGalaxy() {
|
||||
const [links, setLinks] = useState<LinkData[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
|
||||
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
|
||||
const [currentUserDid, setCurrentUserDid] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const cameraControlsRef = useRef<CameraControls>(null);
|
||||
const hasFitCamera = useRef(false);
|
||||
const hasFocusedNode = useRef<string | null>(null);
|
||||
@@ -104,6 +110,28 @@ export function ThoughtGalaxy() {
|
||||
const selectedNodeId = searchParams.get('node');
|
||||
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
|
||||
|
||||
// Fetch current user's profile to get their DID
|
||||
useEffect(() => {
|
||||
async function fetchCurrentUser() {
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentUserDid(data.did);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ThoughtGalaxy] Error fetching current user:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Only fetch current user if we're viewing our own galaxy
|
||||
if (!targetUserDid) {
|
||||
fetchCurrentUser();
|
||||
}
|
||||
}, [targetUserDid]);
|
||||
|
||||
// Fetch data from API on mount and poll for updates
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
@@ -290,6 +318,51 @@ export function ThoughtGalaxy() {
|
||||
router.replace(`${pathname}${newSearch ? `?${newSearch}` : ''}`, { scroll: false });
|
||||
};
|
||||
|
||||
// Handle deleting a node
|
||||
const handleDeleteNode = async () => {
|
||||
if (!selectedNode) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
setDeleteConfirmOpen(false);
|
||||
|
||||
try {
|
||||
// Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩)
|
||||
const cleanId = String(selectedNode.id).replace(/[⟨⟩]/g, '');
|
||||
|
||||
const response = await fetch(`/api/nodes/${cleanId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to delete node');
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
title: 'Node deleted',
|
||||
message: 'Node has been deleted from Bluesky and your galaxy',
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
// Remove the node from local state
|
||||
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== selectedNode.id));
|
||||
setLinks((prevLinks) => prevLinks.filter((l) => l.in !== selectedNode.id && l.out !== selectedNode.id));
|
||||
|
||||
// Close the modal
|
||||
handleCloseModal();
|
||||
} catch (error) {
|
||||
console.error('[ThoughtGalaxy] Delete error:', error);
|
||||
notifications.show({
|
||||
title: 'Delete failed',
|
||||
message: error instanceof Error ? error.message : 'Failed to delete node',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ThoughtGalaxy] Rendering with', nodes.length, 'nodes and', linkLines.length, 'link lines');
|
||||
|
||||
// Show message if no nodes are ready yet
|
||||
@@ -360,6 +433,7 @@ export function ThoughtGalaxy() {
|
||||
<Title order={2} style={{ margin: 0, marginBottom: '0.25rem' }}>
|
||||
{selectedNode.title}
|
||||
</Title>
|
||||
<Group gap="sm" mt="xs">
|
||||
<Anchor
|
||||
href={`https://bsky.app/profile/${selectedNode.user_did}/post/${selectedNode.atp_uri.split('/').pop()}`}
|
||||
target="_blank"
|
||||
@@ -369,6 +443,20 @@ export function ThoughtGalaxy() {
|
||||
>
|
||||
View on Bluesky
|
||||
</Anchor>
|
||||
{/* Show delete button only for user's own nodes */}
|
||||
{currentUserDid && selectedNode.user_did === currentUserDid && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
loading={isDeleting}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
<CloseButton
|
||||
size="lg"
|
||||
@@ -394,6 +482,15 @@ export function ThoughtGalaxy() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation modal */}
|
||||
<DeleteNodeModal
|
||||
opened={deleteConfirmOpen}
|
||||
onClose={() => setDeleteConfirmOpen(false)}
|
||||
onConfirm={handleDeleteNode}
|
||||
nodeTitle={selectedNode?.title || null}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
|
||||
<Canvas
|
||||
camera={{ position: [0, 5, 10], fov: 60 }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Menu, Avatar, NavLink, ActionIcon } from '@mantine/core';
|
||||
import { Menu, Avatar, NavLink, ActionIcon, SegmentedControl, Text, Stack, ScrollArea, Code } from '@mantine/core';
|
||||
import { useMantineColorScheme } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { DeleteNodeModal } from './DeleteNodeModal';
|
||||
|
||||
interface UserProfile {
|
||||
did: string;
|
||||
@@ -11,10 +15,22 @@ interface UserProfile {
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
title: string;
|
||||
user_did: string;
|
||||
}
|
||||
|
||||
export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
||||
const router = useRouter();
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [nodeToDelete, setNodeToDelete] = useState<Node | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch user profile on mount
|
||||
@@ -33,6 +49,66 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Fetch user's nodes for debugging
|
||||
const fetchNodes = async () => {
|
||||
setNodesLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/nodes/debug', {
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!data.error) {
|
||||
setNodes(data.nodes || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch nodes:', error);
|
||||
} finally {
|
||||
setNodesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a node (debug) - Matches ThoughtGalaxy delete pattern
|
||||
const handleDebugDelete = async () => {
|
||||
if (!nodeToDelete) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
setDeleteConfirmOpen(false);
|
||||
|
||||
try {
|
||||
// Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩)
|
||||
const cleanId = String(nodeToDelete.id).replace(/[⟨⟩]/g, '');
|
||||
|
||||
const response = await fetch(`/api/nodes/${cleanId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to delete node');
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
title: 'Node deleted',
|
||||
message: 'Node has been deleted from Bluesky and your galaxy',
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
// Update local state to remove the deleted node
|
||||
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeToDelete.id));
|
||||
setNodeToDelete(null);
|
||||
} catch (error) {
|
||||
console.error('[UserMenu Debug] Delete error:', error);
|
||||
notifications.show({
|
||||
title: 'Delete failed',
|
||||
message: error instanceof Error ? error.message : 'Failed to delete node',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
@@ -129,11 +205,123 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
||||
@{profile.handle}
|
||||
</span>
|
||||
</Menu.Label>
|
||||
<Menu.Divider />
|
||||
|
||||
{/* Theme Selection */}
|
||||
<div style={{ padding: '8px 12px' }}>
|
||||
<Text size="xs" fw={500} c="dimmed" mb={8}>
|
||||
Theme
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
value={colorScheme}
|
||||
onChange={(value) => setColorScheme(value as 'light' | 'dark' | 'auto')}
|
||||
data={[
|
||||
{
|
||||
value: 'light',
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<IconSun size={16} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<IconMoon size={16} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<IconDeviceDesktop size={16} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
fullWidth
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Debug: Show all nodes */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Label>Debug: SurrealDB Nodes</Menu.Label>
|
||||
<div style={{ padding: '8px 12px' }}>
|
||||
<Text size="xs" fw={500} c="dimmed" mb={8}>
|
||||
<button
|
||||
onClick={fetchNodes}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{nodesLoading ? 'Loading...' : `Fetch Nodes (${nodes.length})`}
|
||||
</button>
|
||||
</Text>
|
||||
{nodes.length > 0 && (
|
||||
<ScrollArea h={200}>
|
||||
<Stack gap="xs">
|
||||
{nodes.map((node) => (
|
||||
<div key={node.id} style={{ fontSize: '0.7rem', marginBottom: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
||||
<Text size="xs" fw={600}>{node.title}</Text>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNodeToDelete(node);
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
style={{
|
||||
background: '#fa5252',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
padding: '2px 6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.65rem',
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<Code block style={{ fontSize: '0.65rem' }}>{node.id}</Code>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
)}
|
||||
{nodes.length === 0 && !nodesLoading && (
|
||||
<Text size="xs" c="red">
|
||||
No nodes found in SurrealDB
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Item onClick={handleLogout} c="red">
|
||||
Log out
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
|
||||
{/* Delete confirmation modal - shared with ThoughtGalaxy */}
|
||||
<DeleteNodeModal
|
||||
opened={deleteConfirmOpen}
|
||||
onClose={() => setDeleteConfirmOpen(false)}
|
||||
onConfirm={handleDebugDelete}
|
||||
nodeTitle={nodeToDelete?.title || null}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
54
debug-db.mjs
Normal file
54
debug-db.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
import Surreal from 'surrealdb';
|
||||
|
||||
const USER_DID = 'did:plc:sypdx6a4u2fblmclv6wbxjl3';
|
||||
|
||||
async function main() {
|
||||
const db = new Surreal();
|
||||
|
||||
try {
|
||||
console.log('Connecting to SurrealDB...');
|
||||
await db.connect('ws://localhost:8000/rpc');
|
||||
|
||||
console.log('Signing in...');
|
||||
await db.signin({
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
});
|
||||
|
||||
console.log('Using namespace/database...');
|
||||
await db.use({
|
||||
namespace: 'ponderants',
|
||||
database: 'main',
|
||||
});
|
||||
|
||||
console.log('\n===== ALL NODES IN DATABASE =====');
|
||||
const allNodes = await db.query('SELECT * FROM node LIMIT 20');
|
||||
console.log('Total nodes:', allNodes[0]?.length || 0);
|
||||
console.log('Nodes:', JSON.stringify(allNodes[0], null, 2));
|
||||
|
||||
console.log(`\n===== NODES FOR USER ${USER_DID} (WITHOUT coords_3d filter) =====`);
|
||||
const userNodesNoFilter = await db.query(
|
||||
'SELECT id, title, user_did, coords_3d FROM node WHERE user_did = $userDid',
|
||||
{ userDid: USER_DID }
|
||||
);
|
||||
console.log('Count:', userNodesNoFilter[0]?.length || 0);
|
||||
console.log('Nodes:', JSON.stringify(userNodesNoFilter[0], null, 2));
|
||||
|
||||
console.log(`\n===== NODES FOR USER ${USER_DID} (WITH coords_3d != NONE filter) =====`);
|
||||
const userNodesWithFilter = await db.query(
|
||||
'SELECT id, title, user_did, coords_3d FROM node WHERE user_did = $userDid AND coords_3d != NONE',
|
||||
{ userDid: USER_DID }
|
||||
);
|
||||
console.log('Count:', userNodesWithFilter[0]?.length || 0);
|
||||
console.log('Nodes:', JSON.stringify(userNodesWithFilter[0], null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error('Stack:', error.stack);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
89
docker-compose.ci.yml
Normal file
89
docker-compose.ci.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
# Simplified docker-compose for CI/CD environments
|
||||
# Only includes services needed for testing (excludes surrealmcp)
|
||||
|
||||
services:
|
||||
surrealdb:
|
||||
image: surrealdb/surrealdb:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command:
|
||||
- start
|
||||
- --log
|
||||
- trace
|
||||
- --user
|
||||
- ${SURREALDB_USER:-root}
|
||||
- --pass
|
||||
- ${SURREALDB_PASS:-root}
|
||||
- memory
|
||||
environment:
|
||||
- SURREAL_LOG=trace
|
||||
|
||||
nextjs:
|
||||
image: node:20-alpine
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
environment:
|
||||
- SURREALDB_URL=ws://surrealdb:8000/rpc
|
||||
- SURREALDB_USER=${SURREALDB_USER:-root}
|
||||
- SURREALDB_PASS=${SURREALDB_PASS:-root}
|
||||
- SURREALDB_NS=${SURREALDB_NS:-ponderants}
|
||||
- SURREALDB_DB=${SURREALDB_DB:-main}
|
||||
- SURREALDB_JWT_SECRET=${SURREALDB_JWT_SECRET}
|
||||
- ATPROTO_CLIENT_ID=${ATPROTO_CLIENT_ID}
|
||||
- ATPROTO_REDIRECT_URI=${ATPROTO_REDIRECT_URI}
|
||||
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||
- DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY}
|
||||
- TEST_BLUESKY_HANDLE=${TEST_BLUESKY_HANDLE}
|
||||
- TEST_BLUESKY_PASSWORD=${TEST_BLUESKY_PASSWORD}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- NODE_ENV=development
|
||||
command: >
|
||||
sh -c "
|
||||
npm install -g pnpm &&
|
||||
pnpm install --frozen-lockfile &&
|
||||
echo 'Waiting for SurrealDB to be ready...' &&
|
||||
sleep 10 &&
|
||||
pnpm dev
|
||||
"
|
||||
depends_on:
|
||||
- surrealdb
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 40s
|
||||
|
||||
magnitude:
|
||||
image: mcr.microsoft.com/playwright:v1.56.1-noble
|
||||
working_dir: /app
|
||||
user: root
|
||||
network_mode: "service:nextjs"
|
||||
volumes:
|
||||
- .:/app
|
||||
- node_modules:/app/node_modules
|
||||
environment:
|
||||
- TEST_BLUESKY_HANDLE=${TEST_BLUESKY_HANDLE}
|
||||
- TEST_BLUESKY_PASSWORD=${TEST_BLUESKY_PASSWORD}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- HOME=/root
|
||||
command: >
|
||||
sh -c "
|
||||
npm install -g pnpm &&
|
||||
pnpm install --frozen-lockfile &&
|
||||
npx wait-on http://localhost:3000 --timeout 120000 &&
|
||||
xvfb-run --auto-servernum --server-args='-screen 0 1280x960x24' npx magnitude
|
||||
"
|
||||
depends_on:
|
||||
nextjs:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- test
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
@@ -8,12 +8,50 @@ services:
|
||||
- --log
|
||||
- trace
|
||||
- --user
|
||||
- root
|
||||
- ${SURREALDB_USER}
|
||||
- --pass
|
||||
- root
|
||||
- ${SURREALDB_PASS}
|
||||
- memory
|
||||
volumes:
|
||||
- ./surreal/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
surrealmcp:
|
||||
image: surrealdb/surrealmcp:latest
|
||||
command: >
|
||||
start
|
||||
--bind-address 0.0.0.0:8080
|
||||
--server-url http://localhost:8080
|
||||
-e ws://surrealdb:8000/rpc
|
||||
--ns ${SURREALDB_NS} --db ${SURREALDB_DB} -u ${SURREALDB_USER} -p ${SURREALDB_PASS}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- surrealdb
|
||||
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.56.1-noble
|
||||
working_dir: /home/pwuser/app
|
||||
user: pwuser
|
||||
network_mode: host
|
||||
volumes:
|
||||
- .:/home/pwuser/app
|
||||
- /home/pwuser/app/node_modules
|
||||
environment:
|
||||
- TEST_BLUESKY_HANDLE=${TEST_BLUESKY_HANDLE}
|
||||
- TEST_BLUESKY_PASSWORD=${TEST_BLUESKY_PASSWORD}
|
||||
- PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-http://localhost:3000}
|
||||
command: >
|
||||
sh -c "
|
||||
npm install -g pnpm &&
|
||||
pnpm install --frozen-lockfile &&
|
||||
npx playwright test
|
||||
"
|
||||
depends_on:
|
||||
- surrealdb
|
||||
profiles:
|
||||
- test
|
||||
|
||||
@@ -6,4 +6,6 @@ export default {
|
||||
tests: 'tests/magnitude/**/*.mag.ts',
|
||||
// Run tests in headless mode to avoid window focus issues
|
||||
headless: true,
|
||||
// Use Claude Sonnet 4.5 for best performance
|
||||
model: 'anthropic:claude-sonnet-4-5-20250929',
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"@types/three": "^0.181.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "latest",
|
||||
"eslint-config-next": "latest",
|
||||
"jiti": "^2.6.1",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/playwright',
|
||||
@@ -12,6 +16,7 @@ export default defineConfig({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
headless: true,
|
||||
},
|
||||
|
||||
projects: [
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -105,6 +105,9 @@ importers:
|
||||
'@types/three':
|
||||
specifier: ^0.181.0
|
||||
version: 0.181.0
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
eslint:
|
||||
specifier: latest
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
@@ -1710,6 +1713,10 @@ packages:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
draco3d@1.5.7:
|
||||
resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==}
|
||||
|
||||
@@ -5034,6 +5041,8 @@ snapshots:
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
draco3d@1.5.7: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
|
||||
85
scripts/README.md
Normal file
85
scripts/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Development Scripts
|
||||
|
||||
## test-ci-locally.sh
|
||||
|
||||
Tests the CI workflow locally by running the **exact same docker-compose command** that the Gitea Actions workflow runs.
|
||||
|
||||
### Purpose
|
||||
|
||||
When CI tests fail, this script reproduces the exact CI environment locally to debug issues without repeatedly pushing to trigger CI runs. It runs `docker-compose.ci.yml` with the same parameters as the CI workflow, so you're testing in an identical environment.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
./scripts/test-ci-locally.sh
|
||||
```
|
||||
|
||||
Or run docker-compose directly (this is what the script does):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.ci.yml --profile test up \
|
||||
--abort-on-container-exit \
|
||||
--exit-code-from magnitude
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
1. Checks that `.env` file exists
|
||||
2. Runs `docker compose -f docker-compose.ci.yml --profile test up`
|
||||
3. This starts all services:
|
||||
- **surrealdb**: In-memory database with health check
|
||||
- **nextjs**: Node.js container running `pnpm dev` with health check
|
||||
- **magnitude**: Playwright container running the test suite
|
||||
4. Waits for tests to complete
|
||||
5. Exits with magnitude's exit code
|
||||
6. Shows service logs on failure
|
||||
7. Cleans up containers and volumes
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker and docker-compose installed
|
||||
- `.env` file with test credentials
|
||||
|
||||
### Services Architecture
|
||||
|
||||
The script starts a containerized test environment with proper health checks and dependencies:
|
||||
|
||||
```
|
||||
magnitude (Playwright container - runs tests)
|
||||
↓ depends on (waits for health check)
|
||||
nextjs (Node.js container - runs pnpm dev)
|
||||
↓ depends on (waits for health check)
|
||||
surrealdb (SurrealDB container - in-memory mode)
|
||||
```
|
||||
|
||||
All services share the same network:
|
||||
- Next.js accesses SurrealDB via `ws://surrealdb:8000/rpc`
|
||||
- Magnitude accesses Next.js via `http://localhost:3000`
|
||||
|
||||
### Why This Approach?
|
||||
|
||||
This is simpler and more accurate than using workflow runner tools like `act` or `act_runner` because:
|
||||
|
||||
1. **Identical to CI**: The CI workflow (`.gitea/workflows/magnitude.yml`) literally runs this docker-compose command, so you're testing the exact same thing
|
||||
2. **No Additional Tools**: Doesn't require `act`, `act_runner`, or any workflow execution tools
|
||||
3. **Direct Debugging**: Runs the actual test commands directly, making it easier to see what's happening
|
||||
4. **Faster**: No overhead from workflow interpretation or runner setup
|
||||
|
||||
### Debugging CI Failures
|
||||
|
||||
If Gitea Actions fail:
|
||||
|
||||
1. Check the workflow logs for errors in Gitea UI
|
||||
2. Run `./scripts/test-ci-locally.sh` to reproduce **exactly**
|
||||
3. The script will show the same output as CI
|
||||
4. Debug with docker-compose logs if needed:
|
||||
```bash
|
||||
docker compose -f docker-compose.ci.yml logs surrealdb
|
||||
docker compose -f docker-compose.ci.yml logs nextjs
|
||||
docker compose -f docker-compose.ci.yml logs magnitude
|
||||
```
|
||||
5. Fix issues locally
|
||||
6. Run script again to verify fix
|
||||
7. Commit and push once tests pass locally
|
||||
|
||||
This is **much** faster than debugging via CI push cycles and gives you identical results!
|
||||
62
scripts/test-ci-locally.sh
Executable file
62
scripts/test-ci-locally.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Script to test CI workflow locally by running the exact same docker-compose command as CI
|
||||
# This runs docker-compose.ci.yml which is what the Gitea Actions workflow uses
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "========================================="
|
||||
echo "Testing CI Workflow Locally"
|
||||
echo "========================================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if .env exists
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${RED}Error: .env file not found!${NC}"
|
||||
echo "Please create .env file with required variables"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Running the exact same docker-compose command as CI${NC}"
|
||||
echo -e "${YELLOW}This executes: docker compose -f docker-compose.ci.yml --profile test up${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo -e "${YELLOW}Cleaning up containers and volumes...${NC}"
|
||||
docker compose -f docker-compose.ci.yml down -v
|
||||
}
|
||||
|
||||
# Trap cleanup on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Run the exact same command that CI runs
|
||||
docker compose -f docker-compose.ci.yml --profile test up \
|
||||
--abort-on-container-exit \
|
||||
--exit-code-from magnitude || {
|
||||
echo ""
|
||||
echo -e "${RED}=========================================${NC}"
|
||||
echo -e "${RED}Tests failed!${NC}"
|
||||
echo -e "${RED}=========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Showing service logs:${NC}"
|
||||
echo ""
|
||||
echo "=== SurrealDB Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs --tail=50 surrealdb
|
||||
echo ""
|
||||
echo "=== Next.js Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs --tail=50 nextjs
|
||||
echo ""
|
||||
echo "=== Magnitude Logs ==="
|
||||
docker compose -f docker-compose.ci.yml logs --tail=50 magnitude
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
@@ -1,11 +1,10 @@
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
test('Application boots and displays homepage', async (agent) => {
|
||||
// Act: Navigate to the homepage (uses the default URL
|
||||
// from magnitude.config.ts)
|
||||
await agent.act('Navigate to the homepage');
|
||||
test('Application boots and displays login page', async (agent) => {
|
||||
// Act: Navigate to the root URL (should redirect to /login)
|
||||
await agent.act('Navigate to http://localhost:3000');
|
||||
|
||||
// Check: Verify that the homepage text is visible
|
||||
// This confirms the Next.js app is serving content.
|
||||
await agent.check('The text "Ponderants" is visible on the screen');
|
||||
// Check: Verify the login page loads with expected elements
|
||||
await agent.check('The text "Ponderants" or "Log in with Bluesky" is visible on the screen');
|
||||
await agent.check('A login form or button is displayed');
|
||||
});
|
||||
|
||||
@@ -1,38 +1,86 @@
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
test('Theme toggle switches between light and dark modes', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||
|
||||
// Check: Verify the page loads with a theme
|
||||
await agent.check('The page has either a light or dark background');
|
||||
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||
throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env');
|
||||
}
|
||||
|
||||
// Act: Click the theme toggle button
|
||||
await agent.act('Click the theme toggle button');
|
||||
test('Theme selector in profile dropdown has three options', async (agent) => {
|
||||
// Act: Log in first (theme selector is in authenticated area)
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Wait for theme transition
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Open the profile dropdown menu
|
||||
await agent.act('Click on the profile avatar or "Profile" button');
|
||||
|
||||
// Check: Verify the theme has changed
|
||||
await agent.check('The background color has changed to the opposite theme');
|
||||
// Check: Verify the theme selector is visible with three options
|
||||
await agent.check('A "Theme" label is visible in the dropdown menu');
|
||||
await agent.check('A segmented control with three icon buttons is visible');
|
||||
await agent.check('A sun icon button is visible (for light mode)');
|
||||
await agent.check('A moon icon button is visible (for dark mode)');
|
||||
await agent.check('A desktop/monitor icon button is visible (for system/auto mode)');
|
||||
});
|
||||
|
||||
// Act: Click the theme toggle button again
|
||||
await agent.act('Click the theme toggle button');
|
||||
test('Theme can be changed between light, dark, and auto modes', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Wait for theme transition
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Open profile dropdown and select light mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the sun icon in the theme selector');
|
||||
|
||||
// Check: Verify the theme has changed back
|
||||
await agent.check('The background color has changed back to the original theme');
|
||||
// Check: Verify light mode is active
|
||||
await agent.check('The page has a light background color');
|
||||
await agent.check('The sun icon button appears selected/highlighted');
|
||||
|
||||
// Act: Switch to dark mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the moon icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify dark mode is active
|
||||
await agent.check('The page has a dark background color');
|
||||
await agent.check('The moon icon button appears selected/highlighted');
|
||||
|
||||
// Act: Switch to auto/system mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the desktop icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify auto mode is selected
|
||||
await agent.check('The desktop icon button appears selected/highlighted');
|
||||
});
|
||||
|
||||
test('Light mode displays correct colors', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Ensure we're in light mode by toggling if needed
|
||||
await agent.act('If the background is dark, click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Set to light mode via profile dropdown
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the sun icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify light mode colors
|
||||
await agent.check('The page has a light background color');
|
||||
@@ -42,12 +90,20 @@ test('Light mode displays correct colors', async (agent) => {
|
||||
});
|
||||
|
||||
test('Dark mode displays correct colors', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Ensure we're in dark mode by toggling if needed
|
||||
await agent.act('If the background is light, click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Set to dark mode via profile dropdown
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the moon icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify dark mode colors
|
||||
await agent.check('The page has a dark background color');
|
||||
@@ -57,12 +113,20 @@ test('Dark mode displays correct colors', async (agent) => {
|
||||
});
|
||||
|
||||
test('Theme persists across page refreshes', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Set to light mode
|
||||
await agent.act('If the background is dark, click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the sun icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Act: Refresh the page
|
||||
await agent.act('Refresh the page');
|
||||
@@ -72,8 +136,9 @@ test('Theme persists across page refreshes', async (agent) => {
|
||||
await agent.check('The page still has a light background color');
|
||||
|
||||
// Act: Switch to dark mode
|
||||
await agent.act('Click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the moon icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Act: Refresh the page again
|
||||
await agent.act('Refresh the page');
|
||||
@@ -84,12 +149,20 @@ test('Theme persists across page refreshes', async (agent) => {
|
||||
});
|
||||
|
||||
test('Theme affects all UI components', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Ensure light mode
|
||||
await agent.act('If the background is dark, click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Set to light mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the sun icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify all components use light theme
|
||||
await agent.check('The navigation sidebar uses light colors');
|
||||
@@ -97,8 +170,9 @@ test('Theme affects all UI components', async (agent) => {
|
||||
await agent.check('All buttons and inputs use light theme styling');
|
||||
|
||||
// Act: Switch to dark mode
|
||||
await agent.act('Click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the moon icon in the theme selector');
|
||||
await agent.act('Wait for 500 milliseconds');
|
||||
|
||||
// Check: Verify all components use dark theme
|
||||
await agent.check('The navigation sidebar uses dark colors');
|
||||
@@ -106,21 +180,35 @@ test('Theme affects all UI components', async (agent) => {
|
||||
await agent.check('All buttons and inputs use dark theme styling');
|
||||
});
|
||||
|
||||
test('Theme toggle icon changes based on current theme', async (agent) => {
|
||||
// Act: Navigate to the homepage
|
||||
await agent.act('Navigate to the homepage');
|
||||
test('Theme selector correctly indicates selected theme', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Ensure light mode
|
||||
await agent.act('If the background is dark, click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
// Act: Set to light mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the sun icon in the theme selector');
|
||||
|
||||
// Check: Verify icon shows moon (indicating can switch to dark)
|
||||
await agent.check('The theme toggle button shows a moon icon');
|
||||
// Check: Verify sun icon is highlighted/selected
|
||||
await agent.check('The sun icon button appears selected or highlighted');
|
||||
|
||||
// Act: Switch to dark mode
|
||||
await agent.act('Click the theme toggle button');
|
||||
await agent.act('Wait for 1 second');
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the moon icon in the theme selector');
|
||||
|
||||
// Check: Verify icon shows sun (indicating can switch to light)
|
||||
await agent.check('The theme toggle button shows a sun icon');
|
||||
// Check: Verify moon icon is highlighted/selected
|
||||
await agent.check('The moon icon button appears selected or highlighted');
|
||||
|
||||
// Act: Switch to auto mode
|
||||
await agent.act('Click on the profile avatar');
|
||||
await agent.act('Click the desktop icon in the theme selector');
|
||||
|
||||
// Check: Verify desktop icon is highlighted/selected
|
||||
await agent.check('The desktop icon button appears selected or highlighted');
|
||||
});
|
||||
|
||||
204
tests/magnitude/03-delete-node.mag.ts
Normal file
204
tests/magnitude/03-delete-node.mag.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||
|
||||
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||
throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env');
|
||||
}
|
||||
|
||||
test('User can delete their own node from galaxy view', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Create a test node via chat
|
||||
await agent.act('Type "This is a test node for deletion" into the chat input');
|
||||
await agent.act('Press Enter or click send');
|
||||
await agent.check('AI responds with a message');
|
||||
|
||||
// Act: Trigger node creation
|
||||
await agent.act('Wait for the AI to suggest creating a node or manually trigger node creation');
|
||||
await agent.check('A node draft is created in the editor');
|
||||
|
||||
// Act: Publish the node
|
||||
await agent.act('Click the "Publish" button');
|
||||
await agent.check('A success notification appears');
|
||||
await agent.check('The node is published to Bluesky');
|
||||
|
||||
// Act: Navigate to Galaxy view
|
||||
await agent.act('Click the "Galaxy" navigation link');
|
||||
await agent.check('The galaxy visualization loads');
|
||||
await agent.check('At least one node is visible in the 3D galaxy view');
|
||||
|
||||
// Act: Click on the newly created node
|
||||
await agent.act('Click on the test node in the galaxy view');
|
||||
await agent.check('A node detail panel opens showing the node title and body');
|
||||
await agent.check('The node detail panel shows "This is a test node for deletion"');
|
||||
|
||||
// Check: Verify delete button is visible (only for user\'s own nodes)
|
||||
await agent.check('A "Delete" button is visible in the node detail panel');
|
||||
|
||||
// Act: Click the delete button
|
||||
await agent.act('Click the "Delete" button');
|
||||
|
||||
// Check: Verify delete confirmation modal appears
|
||||
await agent.check('A delete confirmation modal appears');
|
||||
await agent.check('The modal is displayed above the node detail panel');
|
||||
await agent.check('The modal shows "Are you sure you want to delete this node?"');
|
||||
await agent.check('The modal explains this will remove the post from Bluesky');
|
||||
await agent.check('The modal has a "Delete Permanently" button');
|
||||
await agent.check('The modal has a "Cancel" button');
|
||||
|
||||
// Act: Confirm deletion
|
||||
await agent.act('Click the "Delete Permanently" button');
|
||||
|
||||
// Check: Verify deletion succeeded
|
||||
await agent.check('A success notification appears saying "Node deleted"');
|
||||
await agent.check('The node detail panel closes');
|
||||
await agent.check('The node is no longer visible in the galaxy view');
|
||||
|
||||
// Act: Verify node is deleted from Bluesky
|
||||
await agent.act('Navigate to the user\'s Bluesky profile');
|
||||
await agent.check('The test node "This is a test node for deletion" is not visible on Bluesky');
|
||||
});
|
||||
|
||||
test('Delete button is not shown for other users\' nodes', async (agent) => {
|
||||
// This test would require viewing another user's public galaxy
|
||||
// Skipping for now as it requires a second test account
|
||||
await agent.act('Skip this test - requires second test account');
|
||||
});
|
||||
|
||||
test('Cancel button closes delete confirmation without deleting', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Navigate to Galaxy view
|
||||
await agent.act('Click the "Galaxy" navigation link');
|
||||
await agent.check('The galaxy visualization loads');
|
||||
|
||||
// Act: Click on any existing node
|
||||
await agent.act('Click on any node in the galaxy view');
|
||||
await agent.check('A node detail panel opens');
|
||||
|
||||
// Act: Click the delete button
|
||||
await agent.act('Click the "Delete" button');
|
||||
await agent.check('A delete confirmation modal appears');
|
||||
|
||||
// Act: Click cancel
|
||||
await agent.act('Click the "Cancel" button');
|
||||
|
||||
// Check: Verify modal closes and node is still there
|
||||
await agent.check('The delete confirmation modal closes');
|
||||
await agent.check('The node detail panel is still open');
|
||||
await agent.check('The node is still visible in the galaxy view');
|
||||
});
|
||||
|
||||
test('Node deletion removes associated links', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Create two linked nodes
|
||||
await agent.act('Create a first test node via chat');
|
||||
await agent.act('Create a second test node that links to the first');
|
||||
|
||||
// Act: Navigate to Galaxy view
|
||||
await agent.act('Click the "Galaxy" navigation link');
|
||||
await agent.check('The galaxy visualization shows two nodes with a link between them');
|
||||
|
||||
// Act: Delete one of the nodes
|
||||
await agent.act('Click on the first test node');
|
||||
await agent.act('Click the "Delete" button');
|
||||
await agent.act('Click "Delete Permanently"');
|
||||
|
||||
// Check: Verify the link is also removed
|
||||
await agent.check('The link between the nodes is no longer visible');
|
||||
await agent.check('Only one node remains in the galaxy');
|
||||
});
|
||||
|
||||
test('User can delete node from debug panel in Profile menu', async (agent) => {
|
||||
// Act: Log in
|
||||
await agent.act('Navigate to /login');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
await agent.act('Click the submit/authorize button');
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
|
||||
// Act: Create a test node via chat
|
||||
await agent.act('Type "Test node for debug panel deletion" into the chat input');
|
||||
await agent.act('Press Enter or click send');
|
||||
await agent.check('AI responds with a message');
|
||||
|
||||
// Act: Trigger node creation and publish
|
||||
await agent.act('Wait for the AI to suggest creating a node or manually trigger node creation');
|
||||
await agent.check('A node draft is created in the editor');
|
||||
await agent.act('Click the "Publish" button');
|
||||
await agent.check('A success notification appears');
|
||||
|
||||
// Act: Open Profile menu
|
||||
await agent.act('Click the "Profile" button in the navigation sidebar');
|
||||
await agent.check('The Profile menu opens');
|
||||
|
||||
// Check: Verify debug panel is visible (development mode only)
|
||||
await agent.check('A "Debug: SurrealDB Nodes" section is visible');
|
||||
|
||||
// Act: Fetch nodes from debug panel
|
||||
await agent.act('Click the "Fetch Nodes" button');
|
||||
await agent.check('The button shows a count greater than 0');
|
||||
await agent.check('At least one node is listed in the debug panel');
|
||||
|
||||
// Check: Verify the test node appears
|
||||
await agent.check('The node "Test node for debug panel deletion" is visible');
|
||||
|
||||
// Act: Click delete button in debug panel
|
||||
await agent.act('Click the "Delete" button next to the test node');
|
||||
|
||||
// Check: Verify delete confirmation modal appears
|
||||
await agent.check('A delete confirmation modal appears');
|
||||
await agent.check('The modal is displayed above the profile menu');
|
||||
await agent.check('The modal shows the node title "Test node for debug panel deletion"');
|
||||
await agent.check('The modal explains this will remove the post from Bluesky');
|
||||
await agent.check('The modal shows "This action cannot be undone"');
|
||||
await agent.check('The modal has a "Delete Permanently" button');
|
||||
await agent.check('The modal has a "Cancel" button');
|
||||
|
||||
// Act: Confirm deletion
|
||||
await agent.act('Click the "Delete Permanently" button');
|
||||
|
||||
// Check: Verify deletion succeeded
|
||||
await agent.check('A success notification appears saying "Node deleted"');
|
||||
await agent.check('The notification says "Node has been deleted from Bluesky and your galaxy"');
|
||||
await agent.check('The modal closes');
|
||||
|
||||
// Check: Verify node is removed from debug panel
|
||||
await agent.check('The "Fetch Nodes" button shows a count of 0 or the node is no longer in the list');
|
||||
|
||||
// Act: Verify node is deleted from Bluesky and database
|
||||
await agent.act('Refresh the page');
|
||||
await agent.act('Click the "Profile" button again');
|
||||
await agent.act('Click the "Fetch Nodes" button');
|
||||
await agent.check('The node "Test node for debug panel deletion" is not in the list');
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { test } from 'magnitude-test';
|
||||
|
||||
test('[Happy Path] User can have a full voice conversation with AI', async (agent) => {
|
||||
// Act: Navigate to chat page (assumes user is already authenticated)
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Check: Initial state - voice button shows "Start Voice Conversation"
|
||||
await agent.check('A button with text "Start Voice Conversation" is visible');
|
||||
@@ -76,7 +76,7 @@ test('[Happy Path] User can have a full voice conversation with AI', async (agen
|
||||
});
|
||||
|
||||
test('[Unhappy Path] Voice mode handles errors gracefully', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Act: Start voice mode
|
||||
await agent.act('Click the "Start Voice Conversation" button');
|
||||
@@ -93,7 +93,7 @@ test('[Unhappy Path] Voice mode handles errors gracefully', async (agent) => {
|
||||
});
|
||||
|
||||
test('[Happy Path] Text input is disabled during voice mode', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Check: Text input is enabled initially
|
||||
await agent.check('The text input field "Or type your thoughts here..." is enabled');
|
||||
@@ -112,7 +112,7 @@ test('[Happy Path] Text input is disabled during voice mode', async (agent) => {
|
||||
});
|
||||
|
||||
test('[Happy Path] User can type a message while voice mode is idle', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Act: Type a message in the text input
|
||||
await agent.act('Type "This is a text message" into the text input field');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
test('Node publishes successfully with cache (no warnings)', async (agent) => {
|
||||
await agent.open('http://localhost:3000');
|
||||
await agent.act('Navigate to http://localhost:3000');
|
||||
|
||||
// Login
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
|
||||
59
tests/magnitude/helpers.ts
Normal file
59
tests/magnitude/helpers.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Reusable test helpers for Magnitude tests
|
||||
*
|
||||
* These helpers encapsulate common test patterns to reduce code duplication
|
||||
* and make tests more maintainable.
|
||||
*/
|
||||
|
||||
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||
|
||||
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||
throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env');
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complete OAuth login flow
|
||||
*
|
||||
* This function navigates to the login page and completes the full OAuth flow:
|
||||
* 1. Navigate to /login
|
||||
* 2. Enter handle and click "Log in with Bluesky"
|
||||
* 3. Wait for redirect to Bluesky OAuth page
|
||||
* 4. Enter password and click "Sign in"
|
||||
* 5. Click "Authorize" button
|
||||
* 6. Wait for redirect to /chat
|
||||
*
|
||||
* @param agent - The Magnitude test agent
|
||||
*/
|
||||
export async function loginFlow(agent: any) {
|
||||
// Navigate to login page
|
||||
await agent.act('Navigate to /login');
|
||||
|
||||
// Fill in handle and initiate OAuth
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
|
||||
// Wait for redirect to Bluesky OAuth page
|
||||
await agent.check('The page URL contains "bsky.social"');
|
||||
|
||||
// Fill in credentials on Bluesky OAuth page
|
||||
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||
|
||||
// Submit login form
|
||||
await agent.act('Click the submit/authorize button');
|
||||
|
||||
// Wait for and click authorize button
|
||||
await agent.act('Click the "Authorize" button');
|
||||
|
||||
// Verify successful login
|
||||
await agent.check('The page URL contains "/chat"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test credentials for use in tests that need them directly
|
||||
*/
|
||||
export const TEST_CREDENTIALS = {
|
||||
handle: TEST_HANDLE,
|
||||
password: TEST_PASSWORD,
|
||||
} as const;
|
||||
@@ -12,7 +12,7 @@ import { test } from 'magnitude-test';
|
||||
// ============================================================================
|
||||
|
||||
test('User can publish a node from conversation', async (agent) => {
|
||||
await agent.open('http://localhost:3000');
|
||||
await agent.act('Navigate to http://localhost:3000');
|
||||
|
||||
// Step 1: Login with Bluesky
|
||||
await agent.act('Click the "Log in with Bluesky" button');
|
||||
@@ -48,7 +48,7 @@ test('User can publish a node from conversation', async (agent) => {
|
||||
|
||||
test('User can edit node draft before publishing', async (agent) => {
|
||||
// Assumes user is already logged in from previous test
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Start conversation
|
||||
await agent.act('Type "Testing the edit flow" and press Enter');
|
||||
@@ -71,7 +71,7 @@ test('User can edit node draft before publishing', async (agent) => {
|
||||
});
|
||||
|
||||
test('User can cancel node draft without publishing', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Start conversation
|
||||
await agent.act('Type "Test cancellation" and press Enter');
|
||||
@@ -93,7 +93,7 @@ test('User can cancel node draft without publishing', async (agent) => {
|
||||
|
||||
test('Cannot publish node without authentication', async (agent) => {
|
||||
// Open edit page directly without being logged in
|
||||
await agent.open('http://localhost:3000/edit');
|
||||
await agent.act('Navigate to http://localhost:3000/edit');
|
||||
|
||||
await agent.check('Shows empty state message');
|
||||
await agent.check('Message says "No Node Draft"');
|
||||
@@ -101,7 +101,7 @@ test('Cannot publish node without authentication', async (agent) => {
|
||||
});
|
||||
|
||||
test('Cannot publish node with empty title', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Create draft
|
||||
await agent.act('Type "Test empty title validation" and press Enter');
|
||||
@@ -116,7 +116,7 @@ test('Cannot publish node with empty title', async (agent) => {
|
||||
});
|
||||
|
||||
test('Cannot publish node with empty content', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Create draft
|
||||
await agent.act('Type "Test empty content validation" and press Enter');
|
||||
@@ -131,7 +131,7 @@ test('Cannot publish node with empty content', async (agent) => {
|
||||
});
|
||||
|
||||
test('Shows error notification if publish fails', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Create draft
|
||||
await agent.act('Type "Test error handling" and press Enter');
|
||||
@@ -149,7 +149,7 @@ test('Shows error notification if publish fails', async (agent) => {
|
||||
});
|
||||
|
||||
test('Handles long content with truncation', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
// Create a very long message
|
||||
const longMessage = 'A'.repeat(500) + ' This is a test of long content truncation for Bluesky posts.';
|
||||
@@ -168,7 +168,7 @@ test('Handles long content with truncation', async (agent) => {
|
||||
});
|
||||
|
||||
test('Shows warning when cache fails but publish succeeds', async (agent) => {
|
||||
await agent.open('http://localhost:3000/chat');
|
||||
await agent.act('Navigate to http://localhost:3000/chat');
|
||||
|
||||
await agent.act('Type "Test cache failure graceful degradation" and press Enter');
|
||||
await agent.check('AI responds');
|
||||
@@ -190,7 +190,7 @@ test('Shows warning when cache fails but publish succeeds', async (agent) => {
|
||||
|
||||
test('Complete user journey: Login → Converse → Publish → View', async (agent) => {
|
||||
// Full end-to-end test
|
||||
await agent.open('http://localhost:3000');
|
||||
await agent.act('Navigate to http://localhost:3000');
|
||||
|
||||
// Login
|
||||
await agent.act('Login with Bluesky')
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { test as setup, expect } from '@playwright/test';
|
||||
import { test as setup } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { performOAuthLogin } from './helpers';
|
||||
|
||||
const authFile = 'tests/playwright/.auth/user.json';
|
||||
|
||||
setup('authenticate', async ({ page }) => {
|
||||
// For now, just create an empty auth file
|
||||
// TODO: Implement actual OAuth flow when test credentials are available
|
||||
console.log('[Auth Setup] Skipping authentication - implement OAuth flow with test credentials');
|
||||
console.log('[Auth Setup] Starting OAuth authentication flow');
|
||||
|
||||
// Save empty state for now
|
||||
// Clear any existing auth state file to ensure fresh login
|
||||
if (fs.existsSync(authFile)) {
|
||||
fs.unlinkSync(authFile);
|
||||
console.log('[Auth Setup] Cleared existing auth state');
|
||||
}
|
||||
|
||||
// Perform OAuth login using reusable helper
|
||||
await performOAuthLogin(page);
|
||||
|
||||
// Ensure the auth directory exists
|
||||
const authDir = path.dirname(authFile);
|
||||
if (!fs.existsSync(authDir)) {
|
||||
fs.mkdirSync(authDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save authenticated state
|
||||
await page.context().storageState({ path: authFile });
|
||||
console.log(`[Auth Setup] Saved authentication state to ${authFile}`);
|
||||
});
|
||||
|
||||
78
tests/playwright/helpers.ts
Normal file
78
tests/playwright/helpers.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Reusable test helpers for Playwright tests
|
||||
*
|
||||
* These helpers encapsulate common test patterns to reduce code duplication
|
||||
* and make tests more maintainable.
|
||||
*/
|
||||
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||
|
||||
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||
throw new Error(
|
||||
'TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env file'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complete OAuth login flow
|
||||
*
|
||||
* This function navigates to the login page and completes the full OAuth flow:
|
||||
* 1. Navigate to /login
|
||||
* 2. Enter handle and click "Log in with Bluesky"
|
||||
* 3. Wait for redirect to Bluesky OAuth page
|
||||
* 4. Enter password and click "Sign in"
|
||||
* 5. Click "Authorize" button
|
||||
* 6. Wait for redirect to /chat
|
||||
* 7. Verify authentication successful
|
||||
*
|
||||
* @param page - The Playwright Page object
|
||||
*/
|
||||
export async function performOAuthLogin(page: Page) {
|
||||
console.log('[Helper] Starting OAuth login flow');
|
||||
|
||||
// Navigate to login page
|
||||
await page.goto('/login');
|
||||
|
||||
// Fill in handle and initiate OAuth
|
||||
await page.getByLabel('Your Handle').fill(TEST_HANDLE!);
|
||||
|
||||
// Click button and wait for navigation to Bluesky OAuth page
|
||||
await Promise.all([
|
||||
page.waitForURL('**/bsky.social/**', { timeout: 30000 }),
|
||||
page.getByRole('button', { name: 'Log in with Bluesky' }).click(),
|
||||
]);
|
||||
console.log('[Helper] Redirected to Bluesky OAuth page');
|
||||
|
||||
// The identifier is pre-filled from our login flow, just fill in password
|
||||
// Use getByRole to avoid strict mode violations with multiple "Password" labeled elements
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(TEST_PASSWORD!);
|
||||
|
||||
// Click Sign in button
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
|
||||
// Wait for the OAuth authorization page by looking for the Authorize button
|
||||
await page.getByRole('button', { name: 'Authorize' }).waitFor({ timeout: 10000 });
|
||||
console.log('[Helper] On OAuth authorization page');
|
||||
|
||||
// Click Authorize button to grant access and wait for redirect
|
||||
await Promise.all([
|
||||
page.waitForURL('**/chat', { timeout: 20000 }),
|
||||
page.getByRole('button', { name: 'Authorize' }).click(),
|
||||
]);
|
||||
console.log('[Helper] Successfully authorized, redirected to /chat');
|
||||
|
||||
// Verify we're actually logged in by checking for Profile nav link
|
||||
await expect(page.getByText('Profile')).toBeVisible({ timeout: 5000 });
|
||||
console.log('[Helper] Verified authentication successful');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test credentials for use in tests that need them directly
|
||||
*/
|
||||
export const TEST_CREDENTIALS = {
|
||||
handle: TEST_HANDLE,
|
||||
password: TEST_PASSWORD,
|
||||
} as const;
|
||||
7
tests/playwright/seed.spec.ts
Normal file
7
tests/playwright/seed.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Test group', () => {
|
||||
test('seed', async ({ page }) => {
|
||||
// generate code here.
|
||||
});
|
||||
});
|
||||
14
todo.md
14
todo.md
@@ -6,11 +6,15 @@ Upcoming items that should be implemented (time-permitting):
|
||||
playwright mcp testing as well as that of magnitude
|
||||
- ADD MAGNITUDE TESTS FOR EVERYTHING, both existing and new additions
|
||||
- stream the AI output to deepgram for faster synthesis
|
||||
- fix the freaking galaxy node clicking -- when going directly to a node ID
|
||||
link, it redirects to /chat; when clicking on a node in /galaxy (either
|
||||
general or on a specific node ID url there), it closes the modal automatically
|
||||
- dark mode/light mode favicon and overall app theme
|
||||
- dark mode/light mode favicon
|
||||
- fix the double border on desktop between sidebar and conversation actions UI
|
||||
- delete "backup"/"old" page.tsx files
|
||||
- allow ai to transition to edit in chat
|
||||
- why wait for three nodes before umap?
|
||||
- fix creation/display of node links
|
||||
- render markdown
|
||||
- fix the "new tables being created instead of adding to the proper table"
|
||||
issues we're having with the other tables like we were having with the node
|
||||
table and we're now having with at least the oauth session, oauth state, and
|
||||
user tabless; it's probably happening with the link_to table as well but that
|
||||
one doesn't have data because it seems like link creation is broken (see task
|
||||
above to fix)
|
||||
|
||||
Reference in New Issue
Block a user