diff --git a/CLAUDE.md b/CLAUDE.md index 881e5ea5..85adaa76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,6 +76,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Regenerate the CLI catalogue | `npm run generate:cli-catalog` (`--check` to fail on drift). Rewrites `config/clis.stock.json` **and** the marked block in `install.sh` from `stock.ts`. ⚠ **Commit both.** They are what `install.sh` and the Docker agent image read, since neither can import TypeScript; `test/cli-catalog-sync.test.ts` and a CI `--check` step fail if either goes stale. See `docs/cli-registry.md` | | Build the docker agent image | `node scripts/build-agent-image.mjs --no-cache` (builds `codeman/agent:base` from `docker/agent.Dockerfile`; prerequisite for Docker cases; `--engine`/`--image`). ⚠ **Always `--no-cache`** — a plain rebuild re-uses the cached `npm install -g` layer and silently keeps the CLIs frozen at their original versions, which once shipped a BROKEN codex while reporting success. See `docs/docker-cases.md` | | Browser-test exclusion check | `npm run check:browser-excludes` (`scripts/check-browser-test-excludes.mjs`; runs in CI, <1s). Fails if a test importing playwright/puppeteer is still collected by `config/vitest.ci.config.ts`; add it to `BROWSER_TEST_GLOBS` in `config/test-suites.ts` | +| Dead-code report | `npm run knip` (`config/knip.json`; fetched with `npx`, so it needs the network and is not a CI step). It reports nothing on a clean tree: an export, file or dependency it lists is new dead code, or a manual script that needs an `entry`. Tests count as users, and `src/web/public/` is outside it (plain scripts with no imports). A deliberate keep carries `@knipignore` in its JSDoc | | Pre-push hook | Installed by `npm install` (`scripts/git-hooks.mjs`, via postinstall): runs the static CI checks (~10-40s) before `git push`. Skip once: `CODEMAN_SKIP_PREPUSH=1 git push`. Skips itself with a notice when HEAD is not the pushed commit or the tree has uncommitted changes the checks would read. Marker-owned, so a hand-written `pre-push` is never overwritten; installs ONLY into the repo's own `/hooks` (worktree-safe; a `core.hooksPath` elsewhere, e.g. a global one, is left alone) | | Production logs | `journalctl --user -u codeman-web -f` | | Detached server | `codeman web -d` (`--status`, `--stop`; pidfile+log at `dataPath('web.pid'/'web.log')`). ⚠ Refuses to start a 2nd server on one data dir — see Instance isolation | diff --git a/config/knip.json b/config/knip.json index 922a0d96..b69c55f1 100644 --- a/config/knip.json +++ b/config/knip.json @@ -1,16 +1,19 @@ { - "$schema": "https://unpkg.com/knip@5/schema.json", - "entry": [ - "scripts/*.mjs", - "scripts/*.js", - "scripts/watch-subagents.ts", - "scripts/remotion/Root.tsx", - "scripts/remotion/index.ts", - "test/**/*.test.ts", - "test/mobile/vitest.config.ts", - "test/**/*.mjs" - ], - "project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,tsx,mjs,js}", "test/**/*.{ts,mjs}"], + "$schema": "https://unpkg.com/knip@6/schema.json", + "workspaces": { + ".": { + "entry": [ + "scripts/*.{mjs,js,mts,ts}", + "scripts/dev/*.mjs", + "scripts/remotion/Root.tsx", + "scripts/remotion/index.ts", + "test/**/*.test.ts" + ], + "project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,tsx,mjs,js,mts}", "!scripts/remotion/**", "test/**/*.{ts,mjs}"] + } + }, "ignoreExportsUsedInFile": true, - "ignoreDependencies": ["@remotion/cli", "@remotion/transitions", "esbuild", "agent-browser"] + "ignoreDependencies": ["@remotion/cli", "@remotion/transitions"], + "ignoreBinaries": ["ffprobe", "ffmpeg", "wslpath", "vm_stat", "jq", "codex", "script", "mkfifo"], + "tags": ["-knipignore"] } diff --git a/docs/browser-testing-guide.md b/docs/browser-testing-guide.md index 117d8335..8df6b47c 100644 --- a/docs/browser-testing-guide.md +++ b/docs/browser-testing-guide.md @@ -8,8 +8,8 @@ This guide documents the browser testing infrastructure, framework comparison re # Run standalone benchmark (recommended - avoids vitest hook issues) npx tsx scripts/browser-comparison.mjs -# Run existing browser E2E tests -npm test -- test/browser-e2e.test.ts +# Run the browser suite (Playwright; the files are listed in config/test-suites.ts) +npm run test:browser ``` ## Framework Comparison Results @@ -67,7 +67,7 @@ We tested three browser automation frameworks against the Codeman web UI: }); ``` -3. **Use the existing browser-e2e.test.ts pattern** which uses agent-browser CLI commands via `execSync` (avoids async hook issues) +3. **Drive agent-browser through its CLI** with `execSync`, as `scripts/browser-comparison.mjs` does (avoids async hook issues) ## Test File Structure @@ -93,10 +93,8 @@ fixed ports 3180-3182. | File | Framework | Status | |------|-----------|--------| -| `test/browser-e2e.test.ts` | agent-browser | ✅ Working | -| `test/browser-playwright.test.ts` | Playwright | ⚠️ Vitest hook issues | -| `test/browser-puppeteer.test.ts` | Puppeteer | ⚠️ Vitest hook issues | -| `test/browser-agent.test.ts` | agent-browser | ⚠️ Vitest hook issues | +| `test/*.browser.test.ts` and the other files in `BROWSER_TEST_GLOBS` (`config/test-suites.ts`) | Playwright | ✅ `npm run test:browser` | +| `test/mobile/**` | Playwright | ✅ `npm run test:mobile` | | `scripts/browser-comparison.mjs` | All three | ✅ Working (standalone) | ## Framework-Specific Patterns diff --git a/docs/orchestrator-loop-architecture.md b/docs/orchestrator-loop-architecture.md index 1026b53b..aeed92c1 100644 --- a/docs/orchestrator-loop-architecture.md +++ b/docs/orchestrator-loop-architecture.md @@ -363,5 +363,5 @@ The Orchestrator can optionally USE the Ralph Loop internally for phase executio | `src/types/orchestrator.ts` | Type definitions | | `src/prompts/orchestrator.ts` | Prompt templates | | `src/web/routes/orchestrator-routes.ts` | API endpoints | -| `src/web/public/orchestrator-ui.js` | Frontend panel | +| `src/web/public/orchestrator-panel.js` | Frontend panel | | `src/web/ports/orchestrator-port.ts` | Port interface | diff --git a/docs/ralph-wiggum-guide.md b/docs/ralph-wiggum-guide.md index fd3cbcb7..545f33ba 100644 --- a/docs/ralph-wiggum-guide.md +++ b/docs/ralph-wiggum-guide.md @@ -837,8 +837,6 @@ POST /api/sessions/:id/auto-clear - `src/ralph-tracker.ts` - Core detection engine - `src/ralph-loop.ts` - Task orchestration - `src/respawn-controller.ts` - Session cycling -- `src/spawn-orchestrator.ts` - Autonomous agent lifecycle (uses RalphTracker for completion) -- `src/spawn-detector.ts` - Detects `` tags in terminal output - `src/types.ts` - Type definitions --- diff --git a/docs/respawn-state-machine.md b/docs/respawn-state-machine.md index 07dbc06b..e808b8dd 100644 --- a/docs/respawn-state-machine.md +++ b/docs/respawn-state-machine.md @@ -79,5 +79,5 @@ Uses temp file for prompt to avoid E2BIG errors with large terminal buffers. - `test/respawn-scenarios.md` - Comprehensive test scenarios for edge cases - `test/respawn-test-plan.md` - Test environment architecture and strategies -- `test/respawn-test-utils.ts` - Mock utilities (MockSession, MockAiIdleChecker, MockAiPlanChecker) +- `test/mocks/mock-session.ts` - `MockSession`, the session double the respawn tests drive - `test/respawn-analysis.md` - Code coverage analysis and identified issues diff --git a/package-lock.json b/package-lock.json index e51ca889..e3f2f8a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", - "@types/uuid": "^10.0.0", "@types/web-push": "^3.6.4", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.1.8", @@ -3966,13 +3965,6 @@ "csstype": "^3.2.2" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/web-push": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", diff --git a/package.json b/package.json index 4741fd7e..857685a7 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,6 @@ "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.6", "@types/react": "^19.2.14", - "@types/uuid": "^10.0.0", "@types/web-push": "^3.6.4", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.1.8", diff --git a/scripts/data-generator.sh b/scripts/data-generator.sh deleted file mode 100755 index 87febcc7..00000000 --- a/scripts/data-generator.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Generates timestamped data to a file for testing -# Usage: ./scripts/data-generator.sh [output_file] [interval_seconds] -# Then: tail -f /tmp/test-data.log - -OUTPUT="${1:-/tmp/test-data.log}" -INTERVAL="${2:-1}" - -echo "Writing to $OUTPUT every ${INTERVAL}s (Ctrl+C to stop)" -echo "Run: tail -f $OUTPUT" - -i=0 -while true; do - ((i++)) - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Line $i - Random: $RANDOM" >> "$OUTPUT" - sleep "$INTERVAL" -done diff --git a/scripts/mobile-screenshot.mjs b/scripts/mobile-screenshot.mjs deleted file mode 100644 index f91bc0c4..00000000 --- a/scripts/mobile-screenshot.mjs +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env node -/** - * Take mobile screenshots of Codeman using Playwright - * Emulates iPhone 15 Pro (closest to iPhone 17 Pro) - * Includes keyboard simulation - */ - -import { chromium, devices } from 'playwright'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { mkdirSync } from 'fs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const BASE_URL = 'http://localhost:3000'; -const OUTPUT_DIR = join(__dirname, '..', 'docs', 'screenshots'); - -// Ensure output directory exists -try { mkdirSync(OUTPUT_DIR, { recursive: true }); } catch {} - -// iPhone 15 Pro specs (closest to requested iPhone 17 Pro) -// Screen: 393 x 852 at 3x = 1179 x 2556 physical pixels -const VIEWPORT_WIDTH = 393; -const VIEWPORT_HEIGHT = 852; -const KEYBOARD_HEIGHT = 336; // Typical iOS keyboard height - -const iPhone15Pro = { - ...devices['iPhone 14 Pro'], - viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }, - deviceScaleFactor: 3, - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', -}; - -async function takeScreenshots() { - console.log('Launching browser with iPhone 15 Pro emulation...'); - console.log('Output directory:', OUTPUT_DIR); - - const browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ - ...iPhone15Pro, - hasTouch: true, - isMobile: true, - }); - - const page = await context.newPage(); - - try { - // Screenshot 1: Main view (no keyboard) - console.log('Loading main page...'); - await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 10000 }); - await page.waitForTimeout(2000); - - await page.screenshot({ - path: join(OUTPUT_DIR, 'mobile-no-keyboard.png'), - fullPage: false - }); - console.log('✓ Saved: mobile-no-keyboard.png'); - - // Screenshot 2: Simulate keyboard appearing - console.log('Simulating virtual keyboard...'); - - // Trigger the keyboard handler manually - simulate what happens when keyboard shows - await page.evaluate((keyboardHeight) => { - // Add keyboard-visible class - document.body.classList.add('keyboard-visible'); - - // Move toolbar up by keyboard height - const toolbar = document.querySelector('.toolbar'); - if (toolbar) { - toolbar.style.transform = `translateY(${-keyboardHeight}px)`; - } - - // Show and move accessory bar - const accessoryBar = document.querySelector('.keyboard-accessory-bar'); - if (accessoryBar) { - accessoryBar.classList.add('visible'); - accessoryBar.style.transform = `translateY(${-keyboardHeight}px)`; - } - - // Shrink main content area so terminal doesn't extend behind keyboard - // Account for toolbar (40px) + accessory bar (44px) - const main = document.querySelector('.main'); - if (main) { - main.style.paddingBottom = `${keyboardHeight + 94}px`; - } - }, KEYBOARD_HEIGHT); - - await page.waitForTimeout(300); - - // Take screenshot of the reduced viewport (simulating keyboard up) - await page.screenshot({ - path: join(OUTPUT_DIR, 'mobile-keyboard-visible.png'), - fullPage: false - }); - console.log('✓ Saved: mobile-keyboard-visible.png'); - - // Screenshot 3: Full view with keyboard overlay visualization - // Reset viewport but keep toolbar position to show the effect - await page.setViewportSize({ - width: VIEWPORT_WIDTH, - height: VIEWPORT_HEIGHT - }); - - // Add a visual keyboard overlay - await page.evaluate((keyboardHeight) => { - const overlay = document.createElement('div'); - overlay.id = 'keyboard-overlay'; - overlay.style.cssText = ` - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: ${keyboardHeight}px; - background: linear-gradient(to bottom, #2a2a2a 0%, #1a1a1a 100%); - border-top: 1px solid #444; - z-index: 9999; - display: flex; - flex-wrap: wrap; - align-content: flex-start; - padding: 8px 4px; - gap: 4px; - `; - - // Add fake keyboard keys - const rows = [ - 'Q W E R T Y U I O P', - 'A S D F G H J K L', - '⇧ Z X C V B N M ⌫', - '123 🌐 space return' - ]; - - rows.forEach((row, rowIndex) => { - const rowDiv = document.createElement('div'); - rowDiv.style.cssText = 'display: flex; width: 100%; justify-content: center; gap: 4px;'; - - row.split(' ').forEach(key => { - const keyEl = document.createElement('div'); - const isSpecial = ['⇧', '⌫', '123', '🌐', 'space', 'return'].includes(key); - const isSpace = key === 'space'; - const isReturn = key === 'return'; - - keyEl.style.cssText = ` - background: ${isSpecial ? '#3a3a3a' : '#505050'}; - color: #fff; - border-radius: 5px; - padding: ${isSpace ? '10px 60px' : isReturn ? '10px 20px' : '10px 8px'}; - font-size: ${rowIndex === 3 ? '12px' : '14px'}; - min-width: ${isSpace ? '120px' : isReturn ? '60px' : '28px'}; - text-align: center; - box-shadow: 0 1px 0 #222; - `; - keyEl.textContent = isSpace ? '' : key; - rowDiv.appendChild(keyEl); - }); - - overlay.appendChild(rowDiv); - }); - - document.body.appendChild(overlay); - }, KEYBOARD_HEIGHT); - - await page.waitForTimeout(200); - - await page.screenshot({ - path: join(OUTPUT_DIR, 'mobile-with-keyboard.png'), - fullPage: false - }); - console.log('✓ Saved: mobile-with-keyboard.png'); - - // Screenshot 4: Toolbar close-up with keyboard - const toolbar = page.locator('.toolbar'); - if (await toolbar.count() > 0) { - await toolbar.screenshot({ - path: join(OUTPUT_DIR, 'mobile-toolbar-above-keyboard.png') - }); - console.log('✓ Saved: mobile-toolbar-above-keyboard.png'); - } - - console.log('\n✅ All screenshots saved to:', OUTPUT_DIR); - - } catch (error) { - console.error('Error taking screenshots:', error.message); - } finally { - await browser.close(); - } -} - -takeScreenshots(); diff --git a/scripts/monitor-respawn.sh b/scripts/monitor-respawn.sh deleted file mode 100755 index ad5340f0..00000000 --- a/scripts/monitor-respawn.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -# Respawn Controller Monitor - 12 hour monitoring script -# Usage: ./scripts/monitor-respawn.sh [duration_hours] - -SESSION_ID="${1:-5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98}" -DURATION_HOURS="${2:-12}" -ISSUES_FILE="/home/arkon/default/codeman/respawn-monitor-issues.md" -FIXES_FILE="/home/arkon/default/codeman/respawn-monitor-fixes.md" -API_URL="https://localhost:3000" -POLL_INTERVAL=60 # seconds - -# Calculate end time -END_TIME=$(($(date +%s) + DURATION_HOURS * 3600)) - -log_issue() { - local severity="$1" - local message="$2" - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "" >> "$ISSUES_FILE" - echo "### [$severity] $timestamp" >> "$ISSUES_FILE" - echo "$message" >> "$ISSUES_FILE" - echo "" >> "$ISSUES_FILE" -} - -add_fix() { - local issue="$1" - local fix="$2" - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo "" >> "$FIXES_FILE" - echo "### Issue: $issue" >> "$FIXES_FILE" - echo "**Detected:** $timestamp" >> "$FIXES_FILE" - echo "" >> "$FIXES_FILE" - echo "**Proposed Fix:**" >> "$FIXES_FILE" - echo "$fix" >> "$FIXES_FILE" - echo "" >> "$FIXES_FILE" - echo "---" >> "$FIXES_FILE" -} - -# State tracking -PREV_STATE="" -STATE_STUCK_COUNT=0 -PREV_CYCLE=0 -LAST_ACTIVITY=0 -AI_CHECK_ERRORS=0 - -echo "Starting respawn monitoring for session $SESSION_ID" -echo "Duration: $DURATION_HOURS hours" -echo "Issues logged to: $ISSUES_FILE" -echo "Fixes logged to: $FIXES_FILE" - -while [ $(date +%s) -lt $END_TIME ]; do - # Get respawn status - RESP=$(curl -sk "$API_URL/api/sessions/$SESSION_ID" 2>/dev/null) - - if [ -z "$RESP" ]; then - log_issue "ERROR" "API not responding - server may be down" - add_fix "API not responding" "Check if codeman-web service is running:\n\`\`\`bash\nsystemctl --user status codeman-web\nsystemctl --user restart codeman-web\n\`\`\`" - sleep $POLL_INTERVAL - continue - fi - - # Parse response - STATE=$(echo "$RESP" | jq -r '.respawn.state // "null"') - CYCLE=$(echo "$RESP" | jq -r '.respawn.cycleCount // 0') - ENABLED=$(echo "$RESP" | jq -r '.respawnEnabled // false') - TOKENS=$(echo "$RESP" | jq -r '.respawn.config.lastTokenCount // 0') - AI_STATUS=$(echo "$RESP" | jq -r '.respawn.detection.aiCheck.status // "unknown"') - AI_ERRORS=$(echo "$RESP" | jq -r '.respawn.detection.aiCheck.consecutiveErrors // 0') - TIME_SINCE_ACTIVITY=$(echo "$RESP" | jq -r '.respawn.timeSinceActivity // 0') - CONFIDENCE=$(echo "$RESP" | jq -r '.respawn.detection.confidenceLevel // 0') - SESSION_STATUS=$(echo "$RESP" | jq -r '.status // "unknown"') - - # Check for issues - - # 1. Session not running - if [ "$SESSION_STATUS" = "stopped" ] || [ "$SESSION_STATUS" = "error" ]; then - log_issue "CRITICAL" "Session status is '$SESSION_STATUS' - respawn may not function correctly" - add_fix "Session stopped/error" "Check session health and restart if needed:\n\`\`\`bash\ncurl -sk $API_URL/api/sessions/$SESSION_ID\n# If needed, create new session or restart\n\`\`\`" - fi - - # 2. Respawn disabled unexpectedly - if [ "$ENABLED" = "false" ] && [ "$PREV_STATE" != "" ] && [ "$PREV_STATE" != "null" ]; then - log_issue "WARNING" "Respawn appears to have been disabled (was in state: $PREV_STATE)" - add_fix "Respawn disabled" "Re-enable respawn controller:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/enable\n\`\`\`" - fi - - # 3. State stuck for too long (>30 minutes in same state) - if [ "$STATE" = "$PREV_STATE" ] && [ "$STATE" != "watching" ]; then - STATE_STUCK_COUNT=$((STATE_STUCK_COUNT + 1)) - if [ $STATE_STUCK_COUNT -ge 30 ]; then # 30 minutes - log_issue "WARNING" "State stuck in '$STATE' for $STATE_STUCK_COUNT minutes. Detection: confidence=$CONFIDENCE, timeSinceActivity=${TIME_SINCE_ACTIVITY}ms" - add_fix "State stuck in $STATE" "The respawn controller may be stuck. Options:\n1. Check terminal for blocking prompts\n2. Send input to unstick:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/input -d '{\"input\": \"\\\\r\"}'\n\`\`\`\n3. Restart respawn:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/stop\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/start\n\`\`\`" - STATE_STUCK_COUNT=0 # Reset to avoid spam - fi - else - STATE_STUCK_COUNT=0 - fi - - # 4. AI check errors - if [ "$AI_ERRORS" -gt 2 ] && [ "$AI_ERRORS" -gt "$AI_CHECK_ERRORS" ]; then - log_issue "WARNING" "AI idle check has $AI_ERRORS consecutive errors (status: $AI_STATUS)" - add_fix "AI check errors" "AI idle checker is failing. Check:\n1. Claude CLI is working\n2. Sufficient context available\n3. Consider disabling AI check temporarily:\n\`\`\`bash\ncurl -sk -X POST $API_URL/api/sessions/$SESSION_ID/respawn/config -H 'Content-Type: application/json' -d '{\"aiIdleCheckEnabled\": false}'\n\`\`\`" - AI_CHECK_ERRORS=$AI_ERRORS - fi - - # 5. Cycle completed - log success - if [ "$CYCLE" -gt "$PREV_CYCLE" ] && [ "$PREV_CYCLE" -gt 0 ]; then - log_issue "INFO" "Respawn cycle $CYCLE completed successfully (tokens: $TOKENS)" - fi - - # 6. Very high time since activity (>10 minutes) while not in watching state - if [ "$TIME_SINCE_ACTIVITY" -gt 600000 ] && [ "$STATE" != "watching" ]; then - log_issue "WARNING" "No activity for ${TIME_SINCE_ACTIVITY}ms while in state '$STATE' - may be stuck" - fi - - # Update tracking vars - PREV_STATE="$STATE" - PREV_CYCLE="$CYCLE" - - sleep $POLL_INTERVAL -done - -log_issue "INFO" "Monitoring completed after $DURATION_HOURS hours" -echo "Monitoring complete. Check $ISSUES_FILE and $FIXES_FILE" diff --git a/scripts/ralph-wizard-demo.mjs b/scripts/ralph-wizard-demo.mjs deleted file mode 100644 index 861b0eec..00000000 --- a/scripts/ralph-wizard-demo.mjs +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node -/** - * Ralph Loop Wizard Demo - runs through browser, leaves session running - */ -import { chromium } from 'playwright'; -import { readFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { homedir } from 'node:os'; - -const BASE_URL = 'https://localhost:3000'; -const CASE_NAME = 'codeman-ios'; - -async function main() { - console.log('🎬 Starting Ralph Loop Wizard demo...\n'); - - // Launch browser (visible) - const browser = await chromium.launch({ - headless: false, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'], - slowMo: 500, // Slow down so you can see what's happening - }); - - const context = await browser.newContext({ - ignoreHTTPSErrors: true, - }); - const page = await context.newPage(); - - console.log('📱 Opening Codeman web UI...'); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - console.log('🔄 Clicking Ralph Loop button...'); - await page.waitForSelector('.btn-ralph', { timeout: 10000 }); - await page.click('.btn-ralph'); - - console.log('📋 Waiting for wizard modal...'); - await page.waitForSelector('#ralphWizardModal.active', { timeout: 10000 }); - - // Select case - console.log(`📁 Selecting case: ${CASE_NAME}`); - await page.selectOption('#ralphCaseSelect', CASE_NAME); - - // Load initprompt.md if it exists - const initPromptPath = join(homedir(), 'codeman-cases', CASE_NAME, 'initprompt.md'); - let taskDescription = 'Build an awesome iOS app for Codeman!'; - if (existsSync(initPromptPath)) { - taskDescription = readFileSync(initPromptPath, 'utf-8').trim(); - console.log(`📝 Loaded initprompt.md (${taskDescription.length} chars)`); - } - - // Enter task description - await page.fill('#ralphTaskDescription', taskDescription); - - // Set completion phrase - await page.evaluate(() => { - const input = document.getElementById('ralphCompletionPhrase'); - if (input) input.value = 'IOS_APP_COMPLETE'; - }); - - console.log('➡️ Clicking Next (Step 1 → Step 2)...'); - await page.click('#ralphNextBtn'); - await page.waitForTimeout(1000); - - // Check for existing plan - const existingPlan = await page.$('#existingPlanSection:not(.hidden)'); - if (existingPlan) { - console.log('📋 Existing plan detected, using it...'); - await page.evaluate(() => { - const btns = Array.from(document.querySelectorAll('button')); - const useBtn = btns.find(b => b.textContent?.includes('Use Existing')); - if (useBtn) useBtn.click(); - }); - await page.waitForTimeout(500); - } - - console.log('➡️ Clicking Next (Step 2 → Step 3)...'); - await page.click('#ralphNextBtn'); - await page.waitForTimeout(1000); - - console.log('🚀 Clicking Start Loop...'); - await page.click('#ralphStartBtn'); - - // Wait for session to be created - console.log('⏳ Waiting for session to start...'); - await page.waitForSelector('.session-tab', { timeout: 30000 }); - - console.log('\n✅ Ralph Loop started! Session is running.'); - console.log('👀 Browser will stay open so you can observe.'); - console.log(' Press Ctrl+C in terminal when done.\n'); - - // Keep the script running - await new Promise(() => {}); -} - -main().catch(err => { - console.error('Error:', err.message); - process.exit(1); -}); diff --git a/scripts/ralph-wizard-prod.mjs b/scripts/ralph-wizard-prod.mjs deleted file mode 100644 index 11b13553..00000000 --- a/scripts/ralph-wizard-prod.mjs +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env node -/** - * Start Ralph Loop on PRODUCTION server (port 3000) using E2E fixtures - * Uses initprompt.md and @fix_plan.md from the case directory - * Does NOT clean up - leaves session running for user verification - */ -import { chromium } from 'playwright'; -import { readFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { homedir } from 'node:os'; - -const BASE_URL = 'https://localhost:3000'; -const CASE_NAME = 'codeman-ios'; -const CASE_PATH = join(homedir(), 'codeman-cases', CASE_NAME); - -async function main() { - // Read initprompt.md - const initPromptPath = join(CASE_PATH, 'initprompt.md'); - const fixPlanPath = join(CASE_PATH, '@fix_plan.md'); - - if (!existsSync(initPromptPath)) { - console.error(`Missing: ${initPromptPath}`); - process.exit(1); - } - - const taskDescription = readFileSync(initPromptPath, 'utf-8').trim(); - const hasFixPlan = existsSync(fixPlanPath); - - console.log('=== Ralph Loop Wizard E2E (Production) ==='); - console.log(`Case: ${CASE_NAME}`); - console.log(`initprompt.md: ${taskDescription.length} chars`); - console.log(`@fix_plan.md exists: ${hasFixPlan}`); - console.log(''); - - const browser = await chromium.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }); - - const context = await browser.newContext({ ignoreHTTPSErrors: true }); - const page = await context.newPage(); - - // Capture browser console logs - page.on('console', msg => { - if (msg.type() === 'error' || msg.text().includes('RalphWizard')) { - console.log(`[Browser ${msg.type()}] ${msg.text()}`); - } - }); - page.on('pageerror', err => console.log(`[Browser Error] ${err.message}`)); - - try { - console.log('[1/10] Navigating to Codeman...'); - await page.goto(BASE_URL, { timeout: 10000 }); - - console.log('[2/10] Clicking Ralph Loop button (.btn-ralph)...'); - await page.waitForSelector('.btn-ralph', { timeout: 5000 }); - await page.click('.btn-ralph'); - - console.log('[3/10] Waiting for wizard modal (#ralphWizardModal.active)...'); - await page.waitForSelector('#ralphWizardModal.active', { timeout: 5000 }); - - console.log(`[4/10] Selecting case: ${CASE_NAME}...`); - await page.selectOption('#ralphCaseSelect', CASE_NAME); - - console.log('[5/10] Filling task description from initprompt.md...'); - await page.fill('#ralphTaskDescription', taskDescription); - - console.log('[6/10] Setting completion phrase: IOS_APP_COMPLETE...'); - await page.evaluate(() => { - const input = document.getElementById('ralphCompletionPhrase'); - if (input) input.value = 'IOS_APP_COMPLETE'; - }); - - console.log('[7/10] Clicking Next -> Step 2...'); - await page.evaluate(() => { - document.getElementById('ralphNextBtn')?.click(); - }); - await page.waitForTimeout(1000); - - // Check for existing @fix_plan.md detection - const existingPlanSection = await page.$('#existingPlanSection:not(.hidden)'); - if (existingPlanSection) { - console.log('[8/10] Existing @fix_plan.md detected, clicking "Use Existing"...'); - await page.evaluate(() => { - const btns = Array.from(document.querySelectorAll('button')); - const useBtn = btns.find(b => b.textContent?.includes('Use Existing')); - if (useBtn) useBtn.click(); - }); - await page.waitForTimeout(500); - } else { - console.log('[8/10] No existing plan section visible...'); - } - - console.log('[9/10] Clicking Next -> Step 3 (Launch)...'); - await page.evaluate(() => { - document.getElementById('ralphNextBtn')?.click(); - }); - await page.waitForTimeout(1000); - - console.log('[10/10] Clicking Start Loop...'); - await page.evaluate(() => { - document.getElementById('ralphStartBtn')?.click(); - }); - - console.log('Waiting for session tab to appear...'); - await page.waitForSelector('.session-tab', { timeout: 30000 }); - - // IMPORTANT: Wait for the wizard's async startRalphLoop() to complete - // The wizard needs time to: wait for session ready, then send the prompt - console.log('Waiting for wizard to send prompt (checking for busy status)...'); - - let session = null; - let attempts = 0; - const maxAttempts = 30; // 30 seconds max - - while (attempts < maxAttempts) { - await page.waitForTimeout(1000); - - const sessions = await page.evaluate(async () => { - const res = await fetch('/api/sessions'); - return res.json(); - }); - - session = sessions - .filter(s => s.workingDir?.includes('codeman-ios')) - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0]; - - if (session) { - // Check if session is working (prompt was sent) - const detail = await page.evaluate(async (id) => { - const res = await fetch(`/api/sessions/${id}`); - return res.json(); - }, session.id); - - if (detail.isWorking || detail.tokens?.total > 0) { - console.log(`Session is working! Tokens: ${detail.tokens?.total}`); - break; - } - console.log(` Attempt ${attempts + 1}: status=${detail.status}, tokens=${detail.tokens?.total || 0}`); - } - - attempts++; - } - - if (!session || attempts >= maxAttempts) { - console.error('WARNING: Session may not have received the prompt!'); - } - - console.log(''); - console.log('========================================'); - console.log('SUCCESS! Ralph Loop started.'); - console.log('========================================'); - console.log(`Session ID: ${session?.id}`); - console.log(`Screen: codeman-${session?.id?.slice(0, 8)}`); - console.log(`Status: ${session?.status}`); - console.log(''); - console.log('Session is now running. View at: https://localhost:3000'); - console.log(''); - console.log('To delete when done:'); - console.log(` curl -sk -X DELETE "https://localhost:3000/api/sessions/${session?.id}"`); - console.log('========================================'); - - } finally { - await browser.close(); - } -} - -main().catch(err => { - console.error('FAILED:', err.message); - process.exit(1); -}); diff --git a/scripts/ralph-wizard-start.mjs b/scripts/ralph-wizard-start.mjs deleted file mode 100644 index 5faad32b..00000000 --- a/scripts/ralph-wizard-start.mjs +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node -/** - * Start Ralph Loop via wizard on production server, leave it running - */ -import { chromium } from 'playwright'; -import { readFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { homedir } from 'node:os'; - -const BASE_URL = 'https://localhost:3000'; -const CASE_NAME = 'codeman-ios'; - -async function main() { - console.log('Starting Ralph Loop via wizard on production server...\n'); - - const browser = await chromium.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'], - }); - - const context = await browser.newContext({ ignoreHTTPSErrors: true }); - const page = await context.newPage(); - - console.log('1. Opening Codeman web UI...'); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - console.log('2. Clicking Ralph Loop button...'); - await page.waitForSelector('.btn-ralph', { timeout: 10000 }); - await page.click('.btn-ralph'); - - console.log('3. Waiting for wizard modal...'); - await page.waitForSelector('#ralphWizardModal.active', { timeout: 10000 }); - - console.log(`4. Selecting case: ${CASE_NAME}`); - await page.selectOption('#ralphCaseSelect', CASE_NAME); - - // Load initprompt.md - const initPromptPath = join(homedir(), 'codeman-cases', CASE_NAME, 'initprompt.md'); - let taskDescription = 'Build an awesome iOS app for Codeman!'; - if (existsSync(initPromptPath)) { - taskDescription = readFileSync(initPromptPath, 'utf-8').trim(); - console.log(`5. Loaded initprompt.md (${taskDescription.length} chars)`); - } else { - console.log('5. Using default task description'); - } - - await page.fill('#ralphTaskDescription', taskDescription); - - // Set completion phrase - await page.evaluate(() => { - const input = document.getElementById('ralphCompletionPhrase'); - if (input) input.value = 'IOS_APP_COMPLETE'; - }); - - console.log('6. Clicking Next (Step 1 -> Step 2)...'); - await page.click('#ralphNextBtn'); - await page.waitForTimeout(1000); - - // Check for existing plan - const existingPlan = await page.$('#existingPlanSection:not(.hidden)'); - if (existingPlan) { - console.log('7. Existing plan detected, using it...'); - await page.evaluate(() => { - const btns = Array.from(document.querySelectorAll('button')); - const useBtn = btns.find(b => b.textContent?.includes('Use Existing')); - if (useBtn) useBtn.click(); - }); - await page.waitForTimeout(500); - } else { - console.log('7. No existing plan, proceeding...'); - } - - console.log('8. Clicking Next (Step 2 -> Step 3)...'); - await page.click('#ralphNextBtn'); - await page.waitForTimeout(1000); - - console.log('9. Clicking Start Loop...'); - await page.click('#ralphStartBtn'); - - console.log('10. Waiting for session to be created...'); - await page.waitForSelector('.session-tab', { timeout: 30000 }); - - // Get session info from the API - const sessions = await page.evaluate(async () => { - const res = await fetch('/api/sessions'); - return res.json(); - }); - - const iosSession = sessions.find(s => s.workingDir?.includes('codeman-ios')); - - console.log('\n========================================'); - console.log('SUCCESS! Ralph Loop started.'); - console.log('========================================'); - console.log(`Session ID: ${iosSession?.id}`); - console.log(`Screen: codeman-${iosSession?.id?.slice(0, 8)}`); - console.log(`Working Dir: ${iosSession?.workingDir}`); - console.log('\nThe session is now running on your production server.'); - console.log('View it at: https://localhost:3000'); - console.log('Kill it when done via the web UI or: curl -X DELETE https://localhost:3000/api/sessions/' + iosSession?.id); - console.log('========================================\n'); - - await browser.close(); -} - -main().catch(err => { - console.error('Error:', err.message); - process.exit(1); -}); diff --git a/scripts/test-links-browser.mjs b/scripts/test-links-browser.mjs deleted file mode 100644 index 6477067f..00000000 --- a/scripts/test-links-browser.mjs +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env node -/** - * Browser test for clickable file links in Codeman terminal - * Uses Playwright directly to ignore HTTPS errors - */ - -import { chromium } from 'playwright'; - -const BASE_URL = 'https://localhost:3000'; -const TEST_LOG_FILE = '/tmp/test-link-click.log'; - -async function main() { - console.log('Starting browser test for clickable links...\n'); - - // Create test log file - const { writeFileSync, appendFileSync } = await import('fs'); - writeFileSync(TEST_LOG_FILE, `=== Test started at ${new Date().toISOString()} ===\n`); - for (let i = 1; i <= 10; i++) { - appendFileSync(TEST_LOG_FILE, `Entry #${i} - ${Date.now()}\n`); - } - console.log(`Created test file: ${TEST_LOG_FILE}\n`); - - // Launch browser ignoring HTTPS errors - const browser = await chromium.launch({ - headless: true, - args: ['--ignore-certificate-errors', '--ignore-ssl-errors'] - }); - - const context = await browser.newContext({ - ignoreHTTPSErrors: true - }); - - const page = await context.newPage(); - - try { - // Navigate to Codeman - console.log('Navigating to Codeman...'); - await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 }); - console.log('Page loaded: ' + await page.title()); - - // Wait for terminal to be ready - await page.waitForSelector('.xterm-screen', { timeout: 10000 }); - console.log('Terminal found\n'); - - // Get list of sessions - const sessions = await page.evaluate(async () => { - const res = await fetch('/api/sessions'); - return res.json(); - }); - console.log(`Found ${sessions.length} session(s)`); - - if (sessions.length === 0) { - console.log('No sessions found. Creating a shell session...'); - const newSession = await page.evaluate(async () => { - const res = await fetch('/api/quick-start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ caseName: 'link-test', mode: 'shell' }) - }); - return res.json(); - }); - console.log('Created session:', newSession.session?.id); - await page.waitForTimeout(2000); - } - - // Get active session (use codeman session if exists) - const codemanSession = sessions.find(s => s.workingDir?.includes('codeman')); - const sessionId = codemanSession?.id || sessions[0]?.id; - console.log(`Using session: ${sessionId}\n`); - - // Send a command that outputs file paths - console.log('Sending command with file path...'); - await page.evaluate(async (sid, logFile) => { - await fetch(`/api/sessions/${sid}/input`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ input: `echo "Monitor: tail -f ${logFile}"\r` }) - }); - }, sessionId, TEST_LOG_FILE); - - await page.waitForTimeout(2000); - - // Take screenshot - await page.screenshot({ path: '/tmp/codeman-link-test-1.png' }); - console.log('Screenshot saved: /tmp/codeman-link-test-1.png\n'); - - // Check if terminal contains our file path - const terminalContent = await page.evaluate(() => { - const screen = document.querySelector('.xterm-screen'); - return screen?.textContent || ''; - }); - - if (terminalContent.includes(TEST_LOG_FILE)) { - console.log('File path appears in terminal output'); - } else { - console.log('WARNING: File path not found in terminal. Content preview:'); - console.log(terminalContent.substring(0, 500)); - } - - // Try to find clickable links in xterm - const linkCount = await page.evaluate(() => { - // xterm link detection happens on mouse move, so we need to trigger it - const links = document.querySelectorAll('.xterm-link'); - return links.length; - }); - console.log(`\nClickable links found: ${linkCount}`); - - // Try hovering over the terminal to trigger link detection - console.log('\nHovering over terminal to trigger link detection...'); - const termRect = await page.locator('.xterm-screen').boundingBox(); - if (termRect) { - // Move mouse across the terminal - for (let y = termRect.y + 50; y < termRect.y + termRect.height - 50; y += 20) { - for (let x = termRect.x + 50; x < termRect.x + termRect.width - 50; x += 50) { - await page.mouse.move(x, y); - await page.waitForTimeout(50); - } - } - } - - // Check for tooltip (shows when hovering over link) - const tooltipVisible = await page.evaluate(() => { - const tooltip = document.getElementById('file-link-tooltip'); - return tooltip?.style.display !== 'none' && tooltip?.style.display !== ''; - }); - console.log('Tooltip visible:', tooltipVisible); - - // Check for log viewer windows - const logViewerCount = await page.evaluate(() => { - return document.querySelectorAll('.log-viewer-window').length; - }); - console.log('Log viewer windows:', logViewerCount); - - // Take final screenshot - await page.screenshot({ path: '/tmp/codeman-link-test-2.png' }); - console.log('\nFinal screenshot: /tmp/codeman-link-test-2.png'); - - // Test the link provider registration - console.log('\n--- Link Provider Debug ---'); - const debugInfo = await page.evaluate(() => { - // Check if app has link provider registered - const app = window.app; - if (!app) return { error: 'No app object found' }; - - const terminal = app.terminals?.get(app.activeSessionId); - if (!terminal) return { error: 'No active terminal', activeSessionId: app.activeSessionId }; - - return { - hasTerminal: true, - activeSessionId: app.activeSessionId, - terminalBuffer: terminal.buffer?.active?.length || 0 - }; - }); - console.log('Debug info:', debugInfo); - - } catch (error) { - console.error('Error:', error.message); - await page.screenshot({ path: '/tmp/codeman-link-test-error.png' }); - console.log('Error screenshot saved to /tmp/codeman-link-test-error.png'); - } finally { - await browser.close(); - console.log('\nBrowser closed'); - } -} - -main().catch(console.error); diff --git a/scripts/test-patterns.mjs b/scripts/test-patterns.mjs deleted file mode 100644 index 25bd7cda..00000000 --- a/scripts/test-patterns.mjs +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env node -/** - * Test script for link detection patterns - */ - -const patterns = { - cmdPattern: /(tail|cat|head|less|grep|watch|multitail)\s+(?:[^\/\n]+?\s+)?(\/[^\s"'<>|;&\n\)]+)/g, - bashPathPattern: /Bash\([^)]*?(\/(?:var|tmp|home|usr|etc|opt|log|logs)[^\s"'<>|;&\)\n]+)/g, - generalPathPattern: /(\/(?:home|tmp|var|etc|usr|opt)[^\s"'<>|;&\n\)\]]+\.(?:log|txt|json|md|ts|js|sh|py|yaml|yml|csv|xml|html|css))/g, - logPathPattern: /(\/var\/log\/[^\s"'<>|;&\n\)\]]+)/g, - claudeOutputPattern: /(?:Created|Writing|Saved|File|Output|Streaming|Monitoring|Log)(?:\s+(?:file|to|at))?[:\s]+([\/~][^\s"'<>|;&\n\)]+)/gi -}; - -const tests = [ - 'tail -f /tmp/test.log', - 'tail -n 100 /var/log/syslog', - 'cat /home/user/file.txt', - 'head -n 10 /etc/passwd', - 'Bash(tail -f /tmp/test.log)', - 'Created file: /tmp/newfile.log', - 'Writing to /home/user/output.txt', - 'Monitoring file: /var/log/syslog', - 'Streaming /home/user/logs/app.log', - '/home/arkon/project/file.log', - '/tmp/test-output.txt', - 'echo "tail -f /tmp/test.log"', - 'The output is in /home/user/results.csv', - 'plain text without paths' -]; - -console.log('Link Pattern Test Results\n'); - -for (const [name, pattern] of Object.entries(patterns)) { - console.log(`\n=== ${name} ===`); - console.log(`Pattern: ${pattern.source}\n`); - - for (const test of tests) { - pattern.lastIndex = 0; - const matches = []; - let m; - while ((m = pattern.exec(test)) !== null) { - // Get the captured group (varies by pattern) - matches.push(m[1] || m[2]); - } - if (matches.length > 0) { - console.log(` ✓ "${test}"`); - console.log(` → Found: ${matches.join(', ')}`); - } - } -} - -console.log('\n\n=== Combined Test ==='); -console.log('Testing all patterns on each input:\n'); - -for (const test of tests) { - const allMatches = []; - for (const [name, pattern] of Object.entries(patterns)) { - pattern.lastIndex = 0; - let m; - while ((m = pattern.exec(test)) !== null) { - const path = m[1] || m[2]; - if (path && !allMatches.includes(path)) { - allMatches.push(path); - } - } - } - if (allMatches.length > 0) { - console.log(`✓ "${test}"`); - console.log(` → ${allMatches.join(', ')}`); - } else { - console.log(`✗ "${test}" - no match`); - } -} diff --git a/scripts/test-tail-links.sh b/scripts/test-tail-links.sh deleted file mode 100755 index b6787122..00000000 --- a/scripts/test-tail-links.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Test script for clickable file links in Codeman terminal -# Creates a log file and continuously writes timestamped data - -LOGFILE="/tmp/codeman-test-$(date +%s).log" - -echo "Created log file: $LOGFILE" -echo "" -echo "You can monitor this file with:" -echo " tail -f $LOGFILE" -echo "" -echo "Starting to write data every 2 seconds..." -echo "" - -# Write initial content -echo "=== Test Log Started at $(date) ===" > "$LOGFILE" - -# Write data continuously -count=0 -while true; do - count=$((count + 1)) - echo "[$(date '+%H:%M:%S')] Entry #$count - Random: $RANDOM" >> "$LOGFILE" - - # Print status every 5 entries - if [ $((count % 5)) -eq 0 ]; then - echo "Wrote $count entries to $LOGFILE" - fi - - sleep 2 -done diff --git a/src/config/cli-registry/profiles.ts b/src/config/cli-registry/profiles.ts index b05efa88..5d951e1f 100644 --- a/src/config/cli-registry/profiles.ts +++ b/src/config/cli-registry/profiles.ts @@ -76,7 +76,11 @@ export const SETENV_PROFILE_NAMES = [ export type LauncherProfileName = (typeof LAUNCHER_PROFILE_NAMES)[number]; export type SetenvProfileName = (typeof SETENV_PROFILE_NAMES)[number]; -/** True when `name` is a predictive-echo profile this build actually implements. */ +/** + * True when `name` is a predictive-echo profile this build actually implements. + * Nothing calls it yet; it stays beside the registry it reads. + * @knipignore + */ export function isKnownPredictProfile(name: string | undefined): boolean { return name !== undefined && Object.prototype.hasOwnProperty.call(PREDICT_PROFILES, name); } diff --git a/test/mobile/helpers/touch-sim.ts b/test/mobile/helpers/touch-sim.ts index acec9289..508bba39 100644 --- a/test/mobile/helpers/touch-sim.ts +++ b/test/mobile/helpers/touch-sim.ts @@ -202,44 +202,3 @@ export async function swipe( await swipeViaSynthetic(page, direction, swipeOpts); } -/** Tap an element via CDP (trusted touch) */ -export async function tapViaCDP(page: Page, selector: string): Promise { - const cdp = await getCDP(page); - const box = await page.locator(selector).boundingBox(); - if (!box) throw new Error(`Element ${selector} not found for tap`); - - const x = box.x + box.width / 2; - const y = box.y + box.height / 2; - - await dispatchTouchEvent(cdp, 'touchStart', [{ x, y }]); - await page.waitForTimeout(50); - await dispatchTouchEvent(cdp, 'touchEnd', []); -} - -/** Tap an element via synthetic touch (cross-engine) */ -export async function tapViaSynthetic(page: Page, selector: string): Promise { - await page.evaluate((sel) => { - const el = document.querySelector(sel); - if (!el) return; - const rect = el.getBoundingClientRect(); - const x = rect.left + rect.width / 2; - const y = rect.top + rect.height / 2; - const touch = new Touch({ - identifier: 0, - target: el, - clientX: x, - clientY: y, - }); - el.dispatchEvent(new TouchEvent('touchstart', { - touches: [touch], - changedTouches: [touch], - bubbles: true, - })); - el.dispatchEvent(new TouchEvent('touchend', { - touches: [], - changedTouches: [touch], - bubbles: true, - })); - }, selector); -} -