diff --git a/AGENTS.md b/AGENTS.md index 058c272..7c4f626 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,106 @@ 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 ALL magnitude tests: `pnpm test` + - ✅ All tests passing + - ✅ No console errors or warnings in production code paths + - 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 +- Magnitude.run Documentation: https://magnitude.run/docs +- Project Test README: `tests/README.md` + 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 diff --git a/app/api/nodes/[id]/route.ts b/app/api/nodes/[id]/route.ts index 0e34c70..26ce4e4 100644 --- a/app/api/nodes/[id]/route.ts +++ b/app/api/nodes/[id]/route.ts @@ -50,9 +50,13 @@ export async function DELETE( // 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 = $nodeId', - { nodeId: id } + 'SELECT id, user_did, atp_uri FROM node WHERE id = type::thing($table, $recordId)', + { table: 'node', recordId: cleanId } ); const node = nodeResult[0]?.[0]; @@ -114,7 +118,11 @@ export async function DELETE( // 4. Delete from SurrealDB cache (only after successful ATproto deletion) try { - await db.delete(id); + // 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); diff --git a/components/DeleteNodeModal.tsx b/components/DeleteNodeModal.tsx new file mode 100644 index 0000000..210d126 --- /dev/null +++ b/components/DeleteNodeModal.tsx @@ -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 ( + + + + Are you sure you want to delete "{nodeTitle}"? This will: + + + • Remove the post from Bluesky + • Delete the node from your galaxy + This action cannot be undone. + + + + + + + + ); +} diff --git a/components/ThoughtGalaxy.tsx b/components/ThoughtGalaxy.tsx index f1983a0..985dd31 100644 --- a/components/ThoughtGalaxy.tsx +++ b/components/ThoughtGalaxy.tsx @@ -7,9 +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, Button, Modal } 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'; @@ -482,41 +483,13 @@ export function ThoughtGalaxy() { )} {/* Delete confirmation modal */} - setDeleteConfirmOpen(false)} - title="Delete Node" - centered - zIndex={1001} - > - - - Are you sure you want to delete this node? This will: - - - • Remove the post from Bluesky - • Delete the node from your galaxy - This action cannot be undone. - - - - - - - + onConfirm={handleDeleteNode} + nodeTitle={selectedNode?.title || null} + isDeleting={isDeleting} + /> ([]); const [nodesLoading, setNodesLoading] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [nodeToDelete, setNodeToDelete] = useState(null); useEffect(() => { // Fetch user profile on mount @@ -65,12 +68,15 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) { }; // Delete a node (debug) - Matches ThoughtGalaxy delete pattern - const handleDebugDelete = async (nodeId: string) => { + const handleDebugDelete = async () => { + if (!nodeToDelete) return; + setIsDeleting(true); + setDeleteConfirmOpen(false); try { // Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩) - const cleanId = String(nodeId).replace(/[⟨⟩]/g, ''); + const cleanId = String(nodeToDelete.id).replace(/[⟨⟩]/g, ''); const response = await fetch(`/api/nodes/${cleanId}`, { method: 'DELETE', @@ -89,7 +95,8 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) { }); // Update local state to remove the deleted node - setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeId)); + setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeToDelete.id)); + setNodeToDelete(null); } catch (error) { console.error('[UserMenu Debug] Delete error:', error); notifications.show({ @@ -269,9 +276,8 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) { {node.title} @@ -306,6 +313,15 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) { Log out + + {/* Delete confirmation modal - shared with ThoughtGalaxy */} + setDeleteConfirmOpen(false)} + onConfirm={handleDebugDelete} + nodeTitle={nodeToDelete?.title || null} + isDeleting={isDeleting} + /> ); } diff --git a/docker-compose.yml b/docker-compose.yml index 2ca3005..5130b92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,12 +8,27 @@ 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 diff --git a/tests/magnitude/03-delete-node.mag.ts b/tests/magnitude/03-delete-node.mag.ts index 5f79e8c..1b4ff67 100644 --- a/tests/magnitude/03-delete-node.mag.ts +++ b/tests/magnitude/03-delete-node.mag.ts @@ -135,3 +135,70 @@ test('Node deletion removes associated links', async (agent) => { 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'); +});