Compare commits

..

17 Commits

Author SHA1 Message Date
57319e6712 fix: Replace remaining agent.open() calls in voice and cache tests
Some checks failed
Magnitude Tests / test (push) Failing after 1m4s
Fixed agent.open() in:
- tests/magnitude/09-voice.mag.ts (4 instances)
- tests/magnitude/cache-success.mag.ts (1 instance)

All Magnitude tests now use the correct agent.act('Navigate to...') API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:35:47 +00:00
a553cc6130 fix: Replace agent.open() with agent.act('Navigate to...') in tests
Magnitude test framework doesn't have an agent.open() method.
Navigation must be done through agent.act() with natural language.

Fixed all 10 test cases in node-publishing.mag.ts:
- Happy path tests (3)
- Unhappy path tests (6)
- Integration test (1)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:35:13 +00:00
5fc02f8d9b fix: Complete CI/CD testing infrastructure setup
**Environment Variables:**
- Fixed docker-compose.ci.yml to use correct environment variable names:
  - SURREALDB_JWT_SECRET (not SURREAL_JWT_SECRET)
  - GOOGLE_GENERATIVE_AI_API_KEY (not GOOGLE_API_KEY)
- Updated Gitea Actions workflow to match correct variable names

**Docker Configuration:**
- Removed SurrealDB health check (minimal scratch image lacks utilities)
- Added 10-second sleep before Next.js starts to wait for SurrealDB
- Updated magnitude service to run as root user for npm global installs
- Added xvfb-run to magnitude command for headless browser testing
- Updated Playwright Docker image from v1.49.1 to v1.56.1 in both files
- Added named volume for node_modules to persist installations

**Test Configuration:**
- Updated magnitude.config.ts to use Claude Sonnet 4.5 (20250929)
- Added headless: true to playwright.config.ts

**Testing:**
- CI test script (./scripts/test-ci-locally.sh) now works correctly
- All services start properly: SurrealDB → Next.js → Magnitude
- Playwright launches successfully in headless mode with xvfb-run

Note: Users need to ensure .env contains:
- ATPROTO_CLIENT_ID
- ATPROTO_REDIRECT_URI
- SURREALDB_JWT_SECRET
- GOOGLE_GENERATIVE_AI_API_KEY
- ANTHROPIC_API_KEY

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 15:03:01 +00:00
ef0725be58 chore: Add development utilities and MCP configuration
- Added debug-db.mjs script for debugging SurrealDB queries
- Added .mcp.json configuration for Playwright test MCP server
- Added Claude Code agents for Playwright test generation, planning, and healing

These tools assist with development and debugging workflows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:13:51 +00:00
b457e94ccb chore: Add dotenv as devDependency
Added for potential use in development scripts and testing utilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:13:13 +00:00
4abe8183d8 docs: Update AGENTS.md with CI testing infrastructure details
- Documented the containerized CI approach using docker-compose.ci.yml
- Added instructions for local CI testing with test-ci-locally.sh
- Explained benefits of the approach (reproducibility, simplicity)
- Updated .gitignore to ignore SurrealDB data directory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:12:58 +00:00
bb650a3ed9 refactor: Simplify CI testing to use docker-compose directly
Instead of trying to use workflow runner tools (act/act_runner), the script
now directly runs the docker-compose command that CI uses. This is:

- More accurate (exact same command as CI)
- Simpler (no additional tools needed)
- Faster (no workflow interpretation overhead)
- Easier to debug (direct access to service logs)

The CI workflow literally runs `docker compose -f docker-compose.ci.yml`, so
running that locally is the most accurate way to test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:12:35 +00:00
9df7278d55 fix: Use nektos/act instead of gitea/act_runner for local testing
gitea/act_runner is a runner daemon that needs to connect to a Gitea instance,
not a local testing tool. nektos/act is the correct tool for running workflows
locally, and it's compatible with both GitHub Actions and Gitea Actions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:10:42 +00:00
a8da8753f1 feat: Add CI testing infrastructure with act_runner support
- Created scripts/test-ci-locally.sh to test Gitea Actions workflows locally using act_runner
- Created docker-compose.ci.yml for containerized CI test environment
- Updated .gitea/workflows/magnitude.yml to use docker-compose for CI
- Added scripts/README.md documenting the CI testing approach
- Created reusable test helpers in tests/playwright/

This allows developers to run the exact same workflow that CI runs, locally,
making it much easier to debug CI failures without push cycles.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 14:07:16 +00:00
0ea3296885 refactor: Remove redundant standalone Dockerfile.playwright
Some checks failed
Magnitude Tests / test (push) Failing after 37s
The standalone Dockerfile is no longer needed since we integrated Playwright
directly into docker-compose.yml using the official Playwright image.

Benefits of removal:
- Simpler setup (no build step required)
- Less maintenance (one less file to keep updated)
- docker-compose.yml approach is more integrated and easier to use

The docker-compose service provides the same functionality with:
- Same base image (mcr.microsoft.com/playwright:v1.49.1-noble)
- Same non-root user execution
- Better integration with existing services

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 13:56:51 +00:00
39aea34026 feat: Integrate Playwright into docker-compose
Some checks failed
Magnitude Tests / test (push) Failing after 2m2s
Adds Playwright service to docker-compose.yml for easier test execution
and better integration with existing database services.

## Changes

- Add `playwright` service to docker-compose.yml:
  - Uses official Playwright image (mcr.microsoft.com/playwright:v1.49.1-noble)
  - Runs as non-root user (pwuser) for security
  - Uses host networking to access dev server on localhost:3000
  - Loads environment variables from .env
  - Uses `profiles: [test]` to keep it optional
  - Mounts node_modules volume to prevent permission issues

- Update documentation in AGENTS.md:
  - Replace standalone Docker commands with docker-compose usage
  - Document two usage patterns: `docker compose run` and `--profile test`
  - Explain benefits of integrated setup

## Usage

```bash
# Start database services
docker compose up -d

# Start dev server
pnpm dev

# Run Playwright tests in Docker
docker compose run --rm playwright
```

Or with profiles:

```bash
# Run tests one-off
docker compose --profile test run --rm playwright
```

## Benefits

- Unified infrastructure setup (database + tests)
- No need for separate Dockerfile build step
- Easier for new developers to run tests
- Consistent with existing docker-compose workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 13:52:05 +00:00
1ff9a2cf4b feat: Add comprehensive testing infrastructure
Implements robust testing setup with Playwright global auth, reusable test
helpers, Docker support, and CI/CD integration with Gitea Actions.

## Changes

### Playwright Setup
- Add global auth setup with storage state reuse (tests/playwright/auth.setup.ts)
- Fix auth setup to clear existing state before fresh login
- Create reusable performOAuthLogin helper (tests/playwright/helpers.ts)
- Configure dotenv loading for environment variables in playwright.config.ts

### Magnitude Configuration
- Update to use Claude Sonnet 4.5 (claude-sonnet-4-5-20250514)
- Create reusable loginFlow helper (tests/magnitude/helpers.ts)
- Fix smoke test to check login page instead of non-existent homepage

### Docker Support
- Add Dockerfile.playwright with non-root user (pwuser) for security
- Uses official Playwright Docker image (mcr.microsoft.com/playwright:v1.49.1-noble)
- Provides consistent testing environment across users and CI/CD

### CI/CD Integration
- Add Gitea Actions workflow (.gitea/workflows/magnitude.yml)
- Runs Magnitude tests on every push and PR
- Starts SurrealDB and Next.js dev server automatically
- Uploads test results as artifacts (30-day retention)

### Documentation
- Add comprehensive testing setup docs to AGENTS.md:
  - Playwright Docker setup instructions
  - CI/CD with Gitea Actions
  - Testing framework separation (Playwright vs Magnitude)
  - Required secrets for CI/CD

### Testing Best Practices
- Separate Playwright (manual + global auth) from Magnitude (automated E2E)
- Reusable helpers reduce code duplication
- Both frameworks work independently

## Testing

-  Playwright auth setup test passes (5.6s)
-  Magnitude smoke test passes
-  OAuth flow works correctly with helper function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 13:51:09 +00:00
a520814771 feat: Implement node deletion with shared modal and fix SurrealDB RecordId handling
Implements complete node deletion functionality for both galaxy view and debug panel:

**Core Changes:**
- Created shared DeleteNodeModal component used by both ThoughtGalaxy and UserMenu
- Modal provides consistent UX with proper confirmation messaging
- Deletion follows write-through cache pattern: ATproto first, then SurrealDB

**SurrealDB RecordId Fixes:**
- Fixed SELECT query to use type::thing($table, $recordId) for UUID-based RecordIds
- Fixed DELETE query to use type::thing() instead of db.delete() to handle dashes in UUIDs
- Without type::thing(), SurrealDB interprets dashes as subtraction operators

**Testing & Documentation:**
- Added comprehensive Magnitude tests for delete functionality (galaxy view and debug panel)
- Updated CLAUDE.md with complete testing workflow documentation
- Added pre-commit checklist requiring database verification and test execution
- Documented PlaywrightMCP manual testing process before Magnitude test writing

**Database Setup:**
- Configured docker-compose.yml to use environment variables for credentials
- Updated namespace/database to match .env configuration (ponderants/main)

**File Changes:**
- app/api/nodes/[id]/route.ts: Fixed RecordId query patterns (SELECT and DELETE)
- components/DeleteNodeModal.tsx: New shared modal component
- components/ThoughtGalaxy.tsx: Uses shared DeleteNodeModal
- components/UserMenu.tsx: Replaced browser confirm() with shared DeleteNodeModal
- tests/magnitude/03-delete-node.mag.ts: Added debug panel delete test
- AGENTS.md: Added testing workflow and pre-commit checklist documentation
- docker-compose.yml: Environment variable configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 13:25:01 +00:00
d072b71eec refactor: Improve debug panel delete handler and add debug endpoint
- Refactored UserMenu debug panel delete handler to match ThoughtGalaxy pattern
- Added proper error handling with Mantine notifications
- Added loading state management during delete operations
- Created /api/nodes/debug endpoint for development debugging
- Cleaned up debug logging from DELETE endpoint

The debug panel now uses the same delete pattern as ThoughtGalaxy for consistency,
with proper error notifications and state updates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 02:32:34 +00:00
63c955c848 feat: Add delete functionality for user-authored nodes
- Add DELETE /api/nodes/[id] endpoint for deleting nodes
- Verify user authentication and ownership before deletion
- Delete from ATproto (source of truth) first, then SurrealDB cache
- Add delete button in ThoughtGalaxy component for user's own nodes
- Add confirmation modal before deletion
- Fix Modal z-index to appear above node detail panel (zIndex: 1001)
- Fix RecordId encoding issue (strip angle brackets ⟨⟩ from IDs)
- Remove deleted node and associated links from local state
- Add comprehensive Magnitude tests for delete functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 01:49:48 +00:00
a4739bddc1 test: Update theme tests for SegmentedControl in profile dropdown
Changes:
- Updated all theme tests to reflect new UI where theme selector is in profile dropdown
- Tests now use three-option SegmentedControl (light/dark/auto) instead of toggle button
- Added authentication flow to tests since profile dropdown requires login
- Updated test assertions to check for icon-based selection (sun, moon, desktop)
- Tests cover all three modes: light, dark, and auto/system preference
- Verify selected state indication in SegmentedControl
- Updated persistence tests to work with new UI flow

All theme tests now accurately test the current implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 01:30:11 +00:00
57d5405c41 feat: Move theme toggle to profile dropdown with icon-only SegmentedControl
Changes:
- Moved theme toggle from separate DesktopSidebar component into UserMenu dropdown
- Replaced simple light/dark toggle with SegmentedControl offering three options:
  - Light (sun icon)
  - Dark (moon icon)
  - System/Auto (desktop icon)
- Uses icon-only labels for compact display in dropdown menu
- Defaults to 'auto' mode (respects system preference) as configured in layout.tsx
- Removed standalone ThemeToggle component from DesktopSidebar

Benefits:
- Cleaner navigation UI with one less separate control
- Better UX with system preference option
- More compact dropdown menu layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 01:26:33 +00:00
33 changed files with 1928 additions and 102 deletions

View 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>

View 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

View 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.

View 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
View File

@@ -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
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"playwright-test": {
"command": "npx",
"args": [
"playwright",
"run-test-mcp-server"
]
}
}
}

205
AGENTS.md
View File

@@ -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
View 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 }
);
}
}

View 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 });
}
}

View 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 &quot;{nodeTitle}&quot;? 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>
);
}

View File

@@ -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 */}

View File

@@ -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,15 +433,30 @@ export function ThoughtGalaxy() {
<Title order={2} style={{ margin: 0, marginBottom: '0.25rem' }}>
{selectedNode.title}
</Title>
<Anchor
href={`https://bsky.app/profile/${selectedNode.user_did}/post/${selectedNode.atp_uri.split('/').pop()}`}
target="_blank"
rel="noopener noreferrer"
size="sm"
c="dimmed"
>
View on Bluesky
</Anchor>
<Group gap="sm" mt="xs">
<Anchor
href={`https://bsky.app/profile/${selectedNode.user_did}/post/${selectedNode.atp_uri.split('/').pop()}`}
target="_blank"
rel="noopener noreferrer"
size="sm"
c="dimmed"
>
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%' }}

View File

@@ -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
View 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
View 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:

View File

@@ -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

View File

@@ -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',
};

View File

@@ -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",

View File

@@ -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
View File

@@ -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
View 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
View 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}"

View File

@@ -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');
});

View File

@@ -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');
});

View 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');
});

View File

@@ -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');

View File

@@ -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');

View 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;

View File

@@ -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')

View File

@@ -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}`);
});

View 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;

View 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
View File

@@ -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)