diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ca5e237 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +# Cancel superseded runs on the same branch/PR (e.g. rapid pushes). +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + backend-tests: + name: Backend tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # pyproject says requires-python ">=3.12", but the pinned + # memu-py==1.4.0 dependency requires >=3.13, so 3.13 is the real + # floor (and what the dev environment runs). Add more versions here + # if that pin is ever relaxed. + python-version: ["3.13"] + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + # Key the uv cache on the dependency source (this repo has no uv.lock). + cache-dependency-glob: "pyproject.toml" + + - name: Create virtual environment + run: uv venv --python ${{ matrix.python-version }} + + - name: Install project and test dependencies + run: uv pip install -e ".[test]" + + - name: Run test suite + run: .venv/bin/pytest tests/ -v + + frontend-build: + name: Frontend build (Vite) + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build frontend (tsc -b && vite build) + run: npm run build diff --git a/.gitignore b/.gitignore index cc1ec04..002a57c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ nerve.pid # Sync data sync_data/ *.jsonl +# ...except checked-in test fixtures, which the suite needs on a clean checkout +!tests/fixtures/**/*.jsonl # Docker (generated by 'nerve init' — safe to commit if sharing) Dockerfile diff --git a/pyproject.toml b/pyproject.toml index 332c7ea..9dc6432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,15 @@ dependencies = [ "opentelemetry-instrumentation-anthropic>=0.40.0", ] +[project.optional-dependencies] +# Test/dev dependencies. Install with: uv pip install -e ".[test]" +# Upper bounds guard CI against surprise major-version breaks (notably the +# pytest-asyncio 1.x event_loop changes the suite's conftest relies on). +test = [ + "pytest>=8,<10", + "pytest-asyncio>=0.24,<2", +] + [project.scripts] nerve = "nerve.cli:main" diff --git a/tests/fixtures/codex/rollouts/in_scope.jsonl b/tests/fixtures/codex/rollouts/in_scope.jsonl new file mode 100644 index 0000000..ce8743b --- /dev/null +++ b/tests/fixtures/codex/rollouts/in_scope.jsonl @@ -0,0 +1,13 @@ +{"timestamp":"2026-05-19T16:52:04.073Z","type":"session_meta","payload":{"id":"11111111-2222-3333-4444-555555555555","timestamp":"2026-05-19T16:52:02.403Z","cwd":"/tmp/nerve-test-ws","originator":"codex_exec","cli_version":"0.130.0","source":"exec","model_provider":"openai","base_instructions":{"text":"You are Codex, a fixture."}}} +{"timestamp":"2026-05-19T16:52:04.075Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1","started_at":1779209522,"model_context_window":258400}} +{"timestamp":"2026-05-19T16:52:04.076Z","type":"response_item","payload":{"type":"message","role":"developer","content":[{"type":"input_text","text":"system instructions to ignore"}]}} +{"timestamp":"2026-05-19T16:52:04.077Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"AGENTS.md auto-injection — should be dropped"}]}} +{"timestamp":"2026-05-19T16:52:04.080Z","type":"turn_context","payload":{"turn_id":"turn-1","cwd":"/tmp/nerve-test-ws","model":"gpt-5.5"}} +{"timestamp":"2026-05-19T16:52:04.081Z","type":"event_msg","payload":{"type":"user_message","message":"hello fixture","images":[],"local_images":[],"text_elements":[]}} +{"timestamp":"2026-05-19T16:52:04.090Z","type":"response_item","payload":{"type":"reasoning","encrypted_content":"ENCRYPTED-BLOB-PLACEHOLDER","summary":[]}} +{"timestamp":"2026-05-19T16:52:04.100Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"ls\"}","call_id":"call_exec_1"}} +{"timestamp":"2026-05-19T16:52:04.110Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_exec_1","output":"Chunk ID: abc123\nWall time: 0.1234 seconds\nProcess exited with code 0\nOutput:\nfile1\nfile2\n"}} +{"timestamp":"2026-05-19T16:52:04.120Z","type":"response_item","payload":{"type":"function_call","name":"task_list","namespace":"mcp__nerve__","arguments":"{\"limit\":3}","call_id":"call_mcp_1"}} +{"timestamp":"2026-05-19T16:52:04.130Z","type":"event_msg","payload":{"type":"mcp_tool_call_end","call_id":"call_mcp_1","invocation":{"server":"nerve","tool":"task_list","arguments":{"limit":3}},"duration":{"secs":0,"nanos":1000},"result":{"Ok":"task list output here"}}} +{"timestamp":"2026-05-19T16:52:04.140Z","type":"event_msg","payload":{"type":"agent_message","message":"final reply","phase":"final_answer"}} +{"timestamp":"2026-05-19T16:52:04.150Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","last_agent_message":"final reply","completed_at":1779209524,"duration_ms":1450}} diff --git a/tests/fixtures/codex/rollouts/out_of_scope.jsonl b/tests/fixtures/codex/rollouts/out_of_scope.jsonl new file mode 100644 index 0000000..95c84e1 --- /dev/null +++ b/tests/fixtures/codex/rollouts/out_of_scope.jsonl @@ -0,0 +1,3 @@ +{"timestamp":"2026-05-19T17:00:00.000Z","type":"session_meta","payload":{"id":"99999999-aaaa-bbbb-cccc-dddddddddddd","timestamp":"2026-05-19T17:00:00.000Z","cwd":"/tmp/some-other-project","originator":"codex_exec","cli_version":"0.130.0","source":"exec","model_provider":"openai","base_instructions":{"text":""}}} +{"timestamp":"2026-05-19T17:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"this thread is not in scope — must be skipped"}} +{"timestamp":"2026-05-19T17:00:02.000Z","type":"event_msg","payload":{"type":"agent_message","message":"agent reply we should NOT ingest","phase":"final_answer"}} diff --git a/tests/fixtures/codex/rollouts/partial_tail.jsonl b/tests/fixtures/codex/rollouts/partial_tail.jsonl new file mode 100644 index 0000000..c6d7116 --- /dev/null +++ b/tests/fixtures/codex/rollouts/partial_tail.jsonl @@ -0,0 +1,4 @@ +{"timestamp":"2026-05-19T18:00:00.000Z","type":"session_meta","payload":{"id":"22222222-3333-4444-5555-666666666666","cwd":"/tmp/nerve-test-ws","originator":"codex_tui","cli_version":"0.130.0","source":"tui","model_provider":"openai","base_instructions":{"text":""}}} +{"timestamp":"2026-05-19T18:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"first complete line"}} +{"timestamp":"2026-05-19T18:00:02.000Z","type":"event_msg","payload":{"type":"agent_message","message":"second complete","phase":"final_answer"}} +{"timestamp":"2026-05-19T18:00:03.000Z","type":"event_msg","payload":{"type":"user_message","message":"third incomplete \ No newline at end of file