Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Optional local native binary overrides for `just test-e2e`.
# Leave commented to infer or privately install the pinned hosts.
# V1 must be OpenCode 1.18.34; V2 must be OpenCode 2.0.15.

# VIMCODE_E2E_V1_BIN=/absolute/path/to/opencode-v1
# VIMCODE_E2E_V2_BIN=/absolute/path/to/opencode-v2
161 changes: 161 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,177 @@ name: ci
on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.2'
- uses: extractions/setup-just@v2
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: just install
- run: just check
- run: just compat-unit

test-int:
name: Harness integration tests
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.2'
- uses: extractions/setup-just@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: just install
- name: Install process inspection tools
run: sudo apt-get update && sudo apt-get install -y procps
- name: Harness unit and integration checks
shell: bash
run: just test-int 2>&1 | tee "$RUNNER_TEMP/test-int.log"
- name: Upload harness failure log
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-int-failure
path: ${{ runner.temp }}/test-int.log
if-no-files-found: warn
retention-days: 7

test-e2e:
name: E2E tests (${{ matrix.host }})
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- host: v1
version: '1.18.34'
- host: v2
version: '2.0.15'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.2'
- uses: extractions/setup-just@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: just install
- name: Install process and archive tools
run: sudo apt-get update && sudo apt-get install -y procps tar curl
- name: Install and verify pinned native host
shell: bash
env:
HOST: ${{ matrix.host }}
VERSION: ${{ matrix.version }}
run: |
prefix="$RUNNER_TEMP/opencode-$HOST"
mkdir -p "$prefix/native"
if [[ "$HOST" == v1 ]]; then
curl --fail --location --retry 3 --output "$prefix/archive.tar.gz" \
"https://github.com/anomalyco/opencode/releases/download/v$VERSION/opencode-linux-x64-baseline.tar.gz"
echo "24b0d458d21ef548b2752166303defcf7f4945b049fb4876ab78dfaf86d81b27 $prefix/archive.tar.gz" | sha256sum --check
tar -xzf "$prefix/archive.tar.gz" -C "$prefix/native"
else
npm install --prefix "$prefix/native" --no-save --ignore-scripts \
"@opencode/cli-linux-x64-baseline@$VERSION"
node - "$prefix/native/node_modules/@opencode/cli-linux-x64-baseline/package.json" "$VERSION" <<'NODE'
const fs = require('node:fs');
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
if (manifest.name !== '@opencode/cli-linux-x64-baseline' || manifest.version !== process.argv[3]) {
throw new Error('Unexpected native host package identity');
}
NODE
fi
mapfile -t binaries < <(find "$prefix/native" -type f -name opencode -perm /111)
[[ "${#binaries[@]}" == 1 ]] || { echo 'Expected one native host executable'; exit 1; }
binary="$(realpath "${binaries[0]}")"
actual="$("$binary" --version)"
actual="${actual#opencode v}"
[[ "$actual" == "$VERSION" ]] || { echo "Host version mismatch: $actual"; exit 1; }
echo "OPENCODE_E2E_BIN=$binary" >> "$GITHUB_ENV"
- name: Cache-installed plugin E2E
shell: bash
env:
HOST: ${{ matrix.host }}
VERSION: ${{ matrix.version }}
run: |
just test-e2e "$HOST" "$OPENCODE_E2E_BIN" "$VERSION" \
"$RUNNER_TEMP/vimcode-e2e-$HOST" 2>&1 | tee "$RUNNER_TEMP/terminal-control-e2e-$HOST.log"
- name: Verify no-plugin negative control
shell: bash
env:
HOST: ${{ matrix.host }}
VERSION: ${{ matrix.version }}
run: |
set +e
VIMCODE_E2E_NO_PLUGIN=1 just test-e2e "$HOST" "$OPENCODE_E2E_BIN" "$VERSION" \
"$RUNNER_TEMP/vimcode-negative-$HOST" 2>&1 | tee "$RUNNER_TEMP/terminal-control-negative-$HOST.log"
status=${PIPESTATUS[0]}
set -e
[[ "$status" == 1 ]] || { echo "Expected negative-control exit 1, got $status"; exit 1; }
node - "$RUNNER_TEMP/vimcode-negative-$HOST/receipt.json" "$HOST" "$VERSION" <<'NODE'
const assert = require('node:assert/strict');
const receipt = require(process.argv[2]);
assert.equal(receipt.host, process.argv[3]);
assert.equal(receipt.version, process.argv[4]);
assert.equal(receipt.negativeControl, true);
assert.equal(receipt.providersDisabled, true);
assert.deepEqual(receipt.providerObservations, {
'word-motion': 'Build · No provider selected Connect a provider'
});
assert.deepEqual(receipt.cleanupErrors, []);
const failures = receipt.checks.filter(check => check.status === 'fail');
assert.deepEqual(failures.map(check => check.name), ['word-home', 'word-motion']);
assert.deepEqual(failures[0].actual.lines, ['one two three0']);
NODE
- name: Upload synthetic E2E failure evidence
if: failure()
uses: actions/upload-artifact@v4
with:
name: terminal-control-e2e-${{ matrix.host }}-failure
path: |
${{ runner.temp }}/terminal-control-e2e-${{ matrix.host }}.log
${{ runner.temp }}/terminal-control-negative-${{ matrix.host }}.log
${{ runner.temp }}/vimcode-negative-${{ matrix.host }}/receipt.json
${{ runner.temp }}/vimcode-negative-${{ matrix.host }}/word-motion/*.txt
${{ runner.temp }}/vimcode-negative-${{ matrix.host }}/word-motion/failure.json
${{ runner.temp }}/vimcode-negative-${{ matrix.host }}/word-motion/failure.termctrl
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/receipt.json
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/cold-load/*.txt
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/cold-load/failure.json
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/cold-load/failure.termctrl
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/word-motion/*.txt
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/word-motion/failure.json
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/word-motion/failure.termctrl
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/quote-change/*.txt
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/quote-change/failure.json
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/quote-change/failure.termctrl
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/multiline-undo/*.txt
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/multiline-undo/failure.json
${{ runner.temp }}/vimcode-e2e-${{ matrix.host }}/multiline-undo/failure.termctrl
if-no-files-found: warn
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ dist/
.assets/
package-lock.json
bun.lock
.env
.env.*
!.env.template
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ test/
child-session-navigation.test.ts (267) #79: isolated intercept and real OpenTUI keymap navigation regressions
leader.test.ts (125 lines) Unit tests for leader key matching functions
compat/ Optional real-host Python driver and test-only TUI fixture (not packaged)
e2e/ Terminal Control guards: shared harness, *.integration.ts and *.e2e.ts explicit entries (not unit-discovered or packaged)
```

**Data flow:**
Expand Down Expand Up @@ -164,12 +165,14 @@ Text objects (`iw`/`aw`, the quote/bracket pairs, and the `iq`/`ib` aliases) rou
just dev # Launch v1 with the plugin (uses OPENCODE_TUI_CONFIG=dev-tui.json)
just dev2 # Launch pinned v2/local source with isolated .dev2/ state
bun test # Run characterization tests
just check # Lint + tests (used in GitHub Actions)
just check # Lint + existing fast tests (used in GitHub Actions)
just test-int # On-demand SDK/process integration and parser helpers
just test-e2e # Both pinned PTY hosts (v1 1.18.34, v2 2.0.15); optional v1/v2 selector
just compat-unit # Pure checks for the optional host harness
just compat v1 /absolute/opencode 1.18.33 /canonical/empty/output # Isolated installed-artifact check
```

The `dev-tui.json` config is picked up only by `just dev`. Running `opencode` normally in this directory does not load the plugin. `just dev2` runs `scripts/dev2.ts` with the tested v2 package (or an explicit binary), loads local source through the root `tui.ts` shim, and isolates on-disk settings/history under `.dev2/`. The shim/launcher are not distributed; package installs still resolve `exports["./tui"]`. Both dev commands retain arrow navigation and add `ctrl+x j` to open children/picker; v1 uses `h`/`l` to cycle and `k` to return, while v2 uses `h`/`k` and `j`/`l` in the Composer. Visual mode captures the focused editor on entry and re-anchors at the new editor's cursor on the next eligible key after a prompt switch; overlay keys do not change ownership. Horizontal normalization probes `editBuffer.getTextRange()` in host display-cell coordinates and uses lower `editorView` selection calls to retain the renderer's native anchor. The v2 disabled setting is per activation: external storage reconciliation applies on reload, while local `/vim` updates both controller and form guard immediately. `just compat-unit` runs in CI; live-host checks remain optional.
The `dev-tui.json` config is picked up only by `just dev`. Running `opencode` normally in this directory does not load the plugin. `just dev2` runs `scripts/dev2.ts` with the tested v2 package (or an explicit binary), loads local source through the root `tui.ts` shim, and isolates on-disk settings/history under `.dev2/`. The shim/launcher are not distributed; package installs still resolve `exports["./tui"]`. Both dev commands retain arrow navigation and add `ctrl+x j` to open children/picker; v1 uses `h`/`l` to cycle and `k` to return, while v2 uses `h`/`k` and `j`/`l` in the Composer. Visual mode captures the focused editor on entry and re-anchors at the new editor's cursor on the next eligible key after a prompt switch; overlay keys do not change ownership. Horizontal normalization probes `editBuffer.getTextRange()` in host display-cell coordinates and uses lower `editorView` selection calls to retain the renderer's native anchor. The v2 disabled setting is per activation: external storage reconciliation applies on reload, while local `/vim` updates both controller and form guard immediately. `just compat-unit` runs in CI; the Python/tmux live-host checks remain manual. Separate PR jobs run `just test-int` and `just test-e2e` on pinned v1/v2 hosts; harness parser checks are units, driver lifecycle checks are SDK/process integration, and real-host checks are product E2E. These entries use non-discovered `.unit.ts`/`.integration.ts`/`.e2e.ts` suffixes, so ordinary `bun test` and `just check` remain fast and do not launch the guards. `scripts/test-e2e.ts` infers canonical fresh OS-temp outputs, prints receipts, and privately bootstraps exact pins only when no matching PATH/override binary exists. Optional ignored `.env` recognizes only `VIMCODE_E2E_V1_BIN` / `VIMCODE_E2E_V2_BIN` paths (relative to repo root); process values win, CI ignores the file, and unknown/provider keys are never forwarded. Use `bun --env-file=/dev/null run test:e2e`, not bare `bun run`, on Bun 1.3.2 to prevent outer dotenv autoload; synthetic launcher integration tests verify suppression. The advanced `just test-e2e HOST BINARY VERSION OUTPUT` form and CI artifact paths remain supported. See `docs/test-e2e.md` for network/bootstrap limits (local v2 registry access blocked), Linux pending status, and the unchanged private SDK 1.2.1 cleanup boundary.

The experimental v2 adapter uses a raw renderer key listener because v2's public keymap does not expose intercepts; see `docs/opencode-v2-poc.md` for configuration and `docs/opencode-v2-strategy.md` for tested alternatives and remaining compatibility gaps.

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version

## [Unreleased]

### Added

- Added separate PR CI guards via `just test-int` for harness integration and `just test-e2e` for cache-installed OpenCode v1/v2 E2E editing, with exact prompt checks and no-plugin controls; local runs remain on demand and outside the unit suite.
- Added no-argument `just test-e2e` defaults for both pinned hosts, optional single-host selection, fresh temp receipts, private pinned bootstrap, and ignored path-only `.env` binary overrides; existing explicit CI paths remain supported.

## [0.19.1] — 2026-10-04

### Changed
Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
just install # install deps
just dev # launch OpenCode v1 with the plugin loaded
just dev2 # launch pinned v2 with local source and separate .dev2/ state
just check # run lint + tests
just check # run lint + existing fast tests
just test # existing fast suite
just test-int # on-demand harness integration checks (including launcher)
just test-e2e # both pinned hosts; inferred private binaries/temp outputs
```

Running `opencode` directly in this directory won't load the plugin. `just dev` sets `OPENCODE_TUI_CONFIG=dev-tui.json`. `just dev2` uses npm's package runner (`npx`) for OpenCode 2.0.15 and keeps its on-disk settings/history separate under `.dev2/`. To use an existing v2 binary, run `just dev2 /absolute/path/to/opencode-v2` or set `OPENCODE_V2_BIN`. See [the v2 POC notes](docs/opencode-v2-poc.md#local-development).

`just test-e2e` runs v1@1.18.34 and v2@2.0.15; add `v1` or `v2` to select one. It prints fresh canonical temp receipt paths and may download/install the pinned hosts privately. Optional ignored `.env` path keys `VIMCODE_E2E_V1_BIN` / `VIMCODE_E2E_V2_BIN` use existing binaries; process settings win, CI ignores the file, and unknown keys are not forwarded. The advanced `HOST BINARY VERSION OUTPUT` form remains available. See [the runner settings and validation limits](docs/test-e2e.md) before using overrides (Linux runtime remains pending).

## Adding a keybinding

1. Add the key check in the matching mode handler under `src/vim/` (`src/vim/normal.ts` for normal mode keys).
Expand Down Expand Up @@ -39,7 +44,7 @@ Types match commit prefixes: `feat`, `fix`, `refactor`, `chore`, `test`, `docs`.
1. Create a branch: `git checkout -b feat/your-feature`
2. Make changes, run `just check` locally. It must pass with zero errors and zero warnings.
3. Push and open a PR against `main`.
4. CI runs `just check` and `just compat-unit`. Warnings are treated as errors; both checks must pass. Live-host compatibility checks remain manual.
4. CI runs `just check` and `just compat-unit`, plus `just test-int` for harness integration and `just test-e2e` for product E2E on pinned OpenCode v1/v2. Warnings and guard failures fail the build. Locally these guards run only on demand; the existing Python/tmux live-host compatibility harness remains manual. See [the guard commands and limits](docs/test-e2e.md). An owner must configure the new check names as required after their first successful Actions run.
5. PRs are squash-merged. The PR title becomes the commit message on `main`.

## Commit messages
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ On v1, vimcode registers a host key intercept. The experimental v2 adapter uses
3. Open issues for bugs or missing keybindings
4. PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for dev setup and the release process.

Use `just test` for the fast suite, `just test-int` for harness integration and parser helpers, and **`just test-e2e` with no arguments** for both pinned OpenCode hosts (v1 **1.18.34**, v2 **2.0.15**). Optionally select `just test-e2e v1` or `v2`. The real-PTY guard infers fresh temp outputs, prints receipt paths, and bootstraps missing matching hosts privately (network required); optional ignored `.env` binary paths avoid bootstrap. PR builds run all three suites. The latter two stay outside fast-test discovery and run locally only on demand; see [path settings, advanced overrides, and validation limits](docs/test-e2e.md).

## License

MIT
Loading
Loading