diff --git a/AGENTS.md b/AGENTS.md index 7810816d..d48a730d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,7 +148,7 @@ you are running inside of — NOT the automation backend. - **Production-fidelity launch**: The Playwright config (`playwright.mock-llm.config.ts`) starts the full `agent-canvas` stack via `bin/agent-canvas.mjs` — the same binary that `npx @openhands/agent-canvas` executes when users install the npm package. This means mock-LLM tests exercise the actual production path: pre-built static frontend + static-server.mjs + agent-server via uvx + automation backend via uvx + ingress proxy, all behind a single port. - A pre-built `build/` directory is required. The Playwright webServer command runs `npm run build:app` when `build/index.html` is absent, but CI should run the build step explicitly for caching (`npm run build:app` in `.github/workflows/mock-llm-e2e.yml`). - **Single ingress URL**: Tests use one URL for both the browser (`baseURL`) and backend API assertions (`BACKEND_URL`). The ingress proxy routes `/api/*` to the agent-server, `/api/automation/*` to the automation backend, and `/*` to the static frontend. Default ingress port for tests is `18300` (override via `MOCK_LLM_INGRESS_PORT` env var). -- **State isolation**: `OH_CANVAS_SAFE_STATE_DIR=.tmp/mock-llm-state` isolates test state from the user's real `~/.openhands/agent-canvas/` directory. The directory is cleaned before each test run. +- **State isolation**: `OH_CANVAS_SAFE_STATE_DIR=.tmp/mock-llm-state` isolates test state from the user's real `~/.openhands/agent-canvas/` directory. Both `STATE_DIR` (`.tmp/mock-llm-state`) and the automation DB dir (`.tmp/automation/`) are cleaned before each test run — the automation DB now lives outside STATE_DIR at `dirname(STATE_DIR)/automation/automations.db`, mirroring Docker's `~/.openhands/automation/automations.db`. - **Session API key**: A random key is generated per test run and passed to the stack via `SESSION_API_KEY` / `OH_SESSION_API_KEYS_0` / `VITE_SESSION_API_KEY`. The static server injects it into `index.html` at serve time so the frontend authenticates automatically. - **Mock LLM server** (`tests/e2e/mock-llm/scripts/mock-llm-server.py`): Python HTTP server using openhands-sdk's `TestLLM` to return scripted tool-call + text trajectories. Supports admin API endpoints for dynamic trajectory management: - `POST /admin/reset` — reset to the default trajectory (terminal printf + text reply) diff --git a/__tests__/scripts/dev-safe.test.ts b/__tests__/scripts/dev-safe.test.ts index 90e4247f..1c6c355c 100644 --- a/__tests__/scripts/dev-safe.test.ts +++ b/__tests__/scripts/dev-safe.test.ts @@ -595,7 +595,7 @@ describe("buildSafeDevConfig", () => { path.join(tmpdir(), "openhands-agent-canvas-tmux"), ); expect(config.conversationsPath).toBe( - path.join(config.stateDir, "conversations"), + path.join(config.stateDir, "dev_conversations"), ); expect(config.workspacesPath).toBe( path.join(config.stateDir, "workspaces"), diff --git a/playwright.mock-llm.config.ts b/playwright.mock-llm.config.ts index 00aa5fa9..7b645796 100644 --- a/playwright.mock-llm.config.ts +++ b/playwright.mock-llm.config.ts @@ -17,7 +17,7 @@ import { defineConfig, devices } from "@playwright/test"; import { randomBytes } from "node:crypto"; -import { resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; // ── Port allocation (separate from live E2E / dev to avoid collisions) ─ const MOCK_LLM_PORT = process.env.MOCK_LLM_PORT ?? "9999"; @@ -37,11 +37,13 @@ const sessionApiKey = process.env.MOCK_LLM_SESSION_API_KEY = sessionApiKey; // ── State directory (isolated per test run) ──────────────────────────── -// MUST be absolute — the automation backend's SQLite DB URL is derived from -// this path, and a relative path gets double-nested when the child process -// cwd is also set to stateDir (cwd/stateDir/stateDir/automations.db). const STATE_DIR = resolve(".tmp/mock-llm-state"); +// Automation DB lives at $parent_of_STATE_DIR/automation/automations.db, +// mirroring docker/entrypoint.sh which uses $HOME/.openhands/automation/automations.db. +// Both STATE_DIR and AUTOMATION_DB_DIR must be cleaned between runs to avoid stale data. +const AUTOMATION_DB_DIR = join(dirname(STATE_DIR), "automation"); + // ── URLs ─────────────────────────────────────────────────────────────── const INGRESS_URL = `http://localhost:${INGRESS_PORT}/`; const MOCK_LLM_URL = `http://127.0.0.1:${MOCK_LLM_PORT}`; @@ -116,8 +118,10 @@ export default defineConfig({ // kills children via process groups and exits cleanly. { command: - // Clean state dir to avoid stale profile/conversation data between runs - `node -e "const fs=require('node:fs'); fs.rmSync('${STATE_DIR}',{recursive:true,force:true});" && ` + + // Clean state dir and automation DB dir to avoid stale data between runs. + // Automation DB is stored outside STATE_DIR (at AUTOMATION_DB_DIR) so both + // must be cleaned; see scripts/dev-with-automation.mjs startAutomationBackend. + `node -e "const fs=require('node:fs'); fs.rmSync('${STATE_DIR}',{recursive:true,force:true}); fs.rmSync('${AUTOMATION_DB_DIR}',{recursive:true,force:true});" && ` + // Build frontend if not already built (CI should pre-build for caching) '[ -f build/index.html ] || npm run build:app && ' + [ diff --git a/scripts/dev-safe.mjs b/scripts/dev-safe.mjs index dd98b2d3..f7899a3b 100644 --- a/scripts/dev-safe.mjs +++ b/scripts/dev-safe.mjs @@ -579,7 +579,7 @@ function buildConfigFromPorts(ports, cwd, env) { env.OH_CANVAS_SAFE_STATE_DIR || path.join(homedir(), ".openhands", "agent-canvas"), ); - const conversationsPath = path.join(stateDir, "conversations"); + const conversationsPath = path.join(stateDir, "dev_conversations"); const workspacesPath = path.join(stateDir, "workspaces"); // Use provided secret key, or read/generate one persisted to // ~/.openhands/agent-canvas/secret-key.txt. Persisting ensures dev mode @@ -655,6 +655,8 @@ export function buildAgentServerEnv(config) { // This is a no-op on Linux/macOS where the locale is already UTF-8. PYTHONUTF8: "1", TMUX_TMPDIR: config.tmuxTmpDir, + // Parent of stateDir (= ~/.openhands) so settings/secrets match Docker. + OH_PERSISTENCE_DIR: path.dirname(config.stateDir), OH_CONVERSATIONS_PATH: config.conversationsPath, OH_BASH_EVENTS_DIR: config.bashEventsDir, OH_VSCODE_PORT: String(config.vscodePort), diff --git a/scripts/dev-static.mjs b/scripts/dev-static.mjs index 5abfe9fc..ee6d093a 100644 --- a/scripts/dev-static.mjs +++ b/scripts/dev-static.mjs @@ -557,7 +557,7 @@ async function main() { const { mkdirSync } = await import("node:fs"); for (const dir of [ config.stateDir, - join(config.stateDir, "conversations"), + join(config.stateDir, "dev_conversations"), join(config.stateDir, "workspaces"), join(config.stateDir, "bash_events"), join(config.stateDir, "storage"), @@ -588,7 +588,7 @@ async function main() { ); process.exit(1); } - const conversationsPath = join(config.stateDir, "conversations"); + const conversationsPath = join(config.stateDir, "dev_conversations"); const cleared = releaseStaleConversationLeases(conversationsPath); if (cleared > 0) { logService( diff --git a/scripts/dev-with-automation.mjs b/scripts/dev-with-automation.mjs index 72581d8a..54191d89 100644 --- a/scripts/dev-with-automation.mjs +++ b/scripts/dev-with-automation.mjs @@ -381,10 +381,12 @@ function checkPrerequisites({ checkFrontendDependencies = true } = {}) { function ensureDirectories(config) { const dirs = [ config.stateDir, - join(config.stateDir, "conversations"), + join(config.stateDir, "dev_conversations"), join(config.stateDir, "workspaces"), join(config.stateDir, "bash_events"), join(config.stateDir, "storage"), + // Automation DB directory — matches docker/entrypoint.sh mkdir -p behaviour. + dirname(join(dirname(config.stateDir), SHARED_DEFAULTS.paths.automationDb)), ]; for (const dir of dirs) { @@ -589,7 +591,8 @@ function startAutomationBackend(config) { } : {}), AUTOMATION_AGENT_SERVER_API_KEY: config.sessionApiKey, - AUTOMATION_DB_URL: `sqlite+aiosqlite:///${join(config.stateDir, "automations.db")}`, + // ~/.openhands/automation/automations.db — matches docker/entrypoint.sh. + AUTOMATION_DB_URL: `sqlite+aiosqlite:///${join(dirname(config.stateDir), SHARED_DEFAULTS.paths.automationDb)}`, // The automation backend uses this as its publicly-reachable base // URL: it's appended to callback URLs and injected into each // sandbox as `AUTOMATION_API_URL` (consumed by setup.sh for