From b5a49b497c5bbcca702cd86da58c54d5f239e7b8 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 23 Oct 2026 20:28:48 -0300 Subject: [PATCH] feat(opencode): accept OpenCode 1.19+ 1.x releases --- docs/additional-target-formats.md | 4 +- docs/architecture.md | 7 +- docs/cli-reference.md | 4 +- docs/credential-free-native-testing.md | 4 +- docs/format-compatibility.md | 2 +- docs/opencode-source-exploration.md | 4 +- docs/opencode-v2.md | 6 +- docs/troubleshooting.md | 6 +- scripts/install-native-test-clis.sh | 2 +- src/session_migrate/conversion.py | 9 +- src/session_migrate/formats/opencode.py | 17 +++- .../basic.json | 2 +- .../comprehensive.json | 2 +- tests/native_corpus/route_oracle.py | 5 + tests/test_additional_formats.py | 4 +- tests/test_additional_formats_native.py | 92 +++++++++++-------- tests/test_catalog.py | 2 +- tests/test_cli.py | 6 +- tests/test_inspection.py | 4 +- tests/test_kilo_format.py | 2 +- tests/test_opencode_source.py | 2 +- tests/test_opencode_source_native.py | 2 +- tests/test_opencode_v2.py | 19 +++- tests/test_opencode_v2_review.py | 4 +- tests/test_route_matrix.py | 4 +- tests/test_target_integration.py | 10 +- 26 files changed, 134 insertions(+), 91 deletions(-) rename tests/fixtures/{opencode-1.17.20 => opencode-1.19.4}/basic.json (99%) rename tests/fixtures/{opencode-source-1.17.20 => opencode-source-1.19.4}/comprehensive.json (99%) diff --git a/docs/additional-target-formats.md b/docs/additional-target-formats.md index 7f58d6c..612b566 100644 --- a/docs/additional-target-formats.md +++ b/docs/additional-target-formats.md @@ -8,7 +8,7 @@ and same-format portable-rewrite targets in `session-migrate` 0.9.0. | --- | --- | --- | --- | | Pi | `0.80.6` | Write native v3 JSONL | Stable pinned adapter | | Oh My Pi | `18.0.5` | Write native fixed-title-slot v3 JSONL | Stable pinned adapter | -| OpenCode | `1.17.20` | Official `export`/`import` CLI | Stable pinned adapter | +| OpenCode | `1.19.4` | Official `export`/`import` CLI | Stable pinned adapter | | GitHub Copilot CLI | `1.0.70` | Write public session-event schema | Stable pinned adapter | | Antigravity CLI | `1.1.16` | Clean-room SQLite/protobuf DB | Stable version-pinned adapter | | Cursor Agent | `2026.03.20-44cb435` | Clean-room content-addressed SQLite/protobuf graph | Experimental, text only | @@ -100,7 +100,7 @@ actual TUI trajectory also completed two live turns using a schema-translated copy of existing Codex OAuth; credential translation is test-only and is not a migration feature. -## OpenCode 1.17.20 +## OpenCode 1.19.4 OpenCode exposes the strongest public contract in this group: diff --git a/docs/architecture.md b/docs/architecture.md index bdd6352..d0f4e65 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -188,10 +188,9 @@ and 2.0 public transfer schemas; native import detects the installed CLI, while convert-only defaults to legacy. Exact release warnings are scoped to the selected schema. See [OpenCode v2](opencode-v2.md). -Automatic Kilo, Antigravity, Cursor, and Hermes installation is stricter: metadata -overrides cannot bypass exact runtime version checks. OpenCode requires legacy -1.17.20 or its supported 2.0 schema family and rejects artifact/CLI schema -mismatches. Antigravity verifies its +Automatic Kilo, Antigravity, Cursor, and Hermes installation is stricter: +metadata overrides cannot bypass exact runtime version checks or +artifact/CLI schema mismatches. Antigravity verifies its binary digest. Cursor verifies launcher, main bundle, protobuf-bearing chunk, bundled Node, sizes, SHA-256 values, and reported version. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index bd4c7d4..9653a0b 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -155,8 +155,8 @@ default. Every other source requires an explicit target. An irrelevant target-specific option may be accepted but has no effect. The manifest records the target metadata version and warns when it differs from the validated writer release. Native OpenCode import selects its schema from the -installed CLI unless explicitly overridden: legacy `1.17.20` or the `2.0.x` -family. Conversion without native import defaults to legacy; use +installed CLI unless explicitly overridden: the 1.x `1.19.0+` family or the +`2.0.x` family. Conversion without native import defaults to legacy; use `--target-cli-version 2.0.22` for a v2 bundle. Other native schema series fail closed. See [OpenCode v2](opencode-v2.md) for exact validation scope. Kilo, Antigravity, Cursor, and Hermes installation requires the exact pinned version. diff --git a/docs/credential-free-native-testing.md b/docs/credential-free-native-testing.md index 5c0e16d..04a2702 100644 --- a/docs/credential-free-native-testing.md +++ b/docs/credential-free-native-testing.md @@ -55,7 +55,7 @@ No production credential is read, translated, or written by this workflow. | MastraCode 0.37.1 | OpenAI-compatible override; replay and append | | Muse 0.2.1 | Exact Muse with the pinned OpenRouter adapter pointed locally; replay and append | | OMP 18.0.5 | Local provider; replay, append, and rename | -| OpenCode 1.17.20 | Official import/export plus local provider replay and append | +| OpenCode 1.19.4 | Official import/export plus local provider replay and append | | OpenCode v2 | Stock 2.0.22 locally; stock 2.0.23 CI variant; public import/export, catalog, cold reopen and local continuation | | OpenHands 1.16.0 | OpenAI-compatible override; native creation, reload, TUI replay, and append | | Pi 0.80.6 | Offline RPC with local provider; tools, images, compaction, replay, and append | @@ -91,5 +91,5 @@ The additional `opencode-v2` job pins `@opencode/cli@2.0.23` and sets run without skips, exercising synthetic import/export, collisions, cold reopen and localhost-only continuation. The same native tests accept an explicitly supplied stock 2.0.22 binary and select its observed release in the artifact. Legacy `opencode` remains pinned -to 1.17.20. +to 1.19.4. See [the v2 transfer contract](opencode-v2.md). diff --git a/docs/format-compatibility.md b/docs/format-compatibility.md index 2380d32..8014ad7 100644 --- a/docs/format-compatibility.md +++ b/docs/format-compatibility.md @@ -20,7 +20,7 @@ to separately installed host binaries: | Codex CLI | `0.144.4` | | Pi source and target | `0.80.6` | | Oh My Pi source and target | `18.0.5` | -| OpenCode legacy source and target | `1.17.20` | +| OpenCode legacy source and target | `1.19.0` and later 1.x releases; exact releases beyond `1.19.4` accepted with warnings | | OpenCode v2 transfer adapter | Stock Linux `2.0.22` local gate; `2.0.23` CI pin | | GitHub Copilot CLI source and target | `1.0.70` | | Antigravity CLI source and target | `1.1.16` | diff --git a/docs/opencode-source-exploration.md b/docs/opencode-source-exploration.md index 6d14f1e..77505eb 100644 --- a/docs/opencode-source-exploration.md +++ b/docs/opencode-source-exploration.md @@ -106,13 +106,13 @@ session sizes. The byte limit remains the primary memory bound. ## Native evidence -`tests/fixtures/opencode-source-1.17.20/comprehensive.json` is synthetic and +`tests/fixtures/opencode-source-1.19.4/comprehensive.json` is synthetic and schema-valid. It covers text, system context, an image, reasoning, a completed tool with an image attachment, compacted-result metadata, compaction, and several intentionally nonportable native parts. The native test performs this credential-free trajectory against the exact -installed 1.17.20 binary in a temporary HOME/XDG store: +pinned binary (`opencode.PINNED_OPENCODE_VERSION`) in a temporary HOME/XDG store: 1. official import of the comprehensive bundle; 2. read-only `opencode db` count check: 1 session, 4 messages, 13 top-level diff --git a/docs/opencode-v2.md b/docs/opencode-v2.md index 3f5882b..293df0c 100644 --- a/docs/opencode-v2.md +++ b/docs/opencode-v2.md @@ -1,7 +1,7 @@ # OpenCode 2.0 public session transfers -OpenCode 1.17.20 uses `import/export --pure` and nested message/part bundles. -OpenCode 2.0 uses `session import/export --standalone` and flat typed messages. +Legacy OpenCode 1.x clients use `import/export --pure` and nested message/part +bundles. OpenCode 2.0 uses `session import/export --standalone` and flat typed messages. Native import detects the installed client and selects its transfer schema. Convert-only output remains legacy unless `--target-cli-version 2.0.22` or another accepted 2.0.x release is set. Unsupported schema series fail closed. The `opencode v` version prefix and @@ -16,7 +16,7 @@ results, inline user/tool images, and readable completed compaction summaries. Completed tool results retain the original ordered `content_blocks`, including text segmentation and captions interleaved with multiple images, in portable events and v2 output. Capable targets such as Claude, Codex and Pi consume these -ordered blocks. Legacy OpenCode 1.17.20 and Kilo 7.5.0 have a single output string +ordered blocks. Legacy OpenCode 1.x and Kilo 7.5.0 have a single output string plus separate attachments: flattening the ordered blocks is counted once per result as `tool_result:content_block_order` when it changes their representation. Structured tool errors retain their message as a failed result. Omitted native diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f3d8e55..484a0ae 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -117,8 +117,10 @@ Do not delete native sessions merely to satisfy lookup. ## OpenCode source or target errors -OpenCode source transfer and target import accept legacy `1.17.20` and the -`2.0.x` transfer schema family. Legacy commands use `export`/`import --pure`; +OpenCode source transfer and target import accept 1.x releases `1.19.0` +and later plus the `2.0.x` transfer schema family. Every accepted 1.x release +keeps the same public bundle shape; exact releases beyond the validated +`1.19.4` receive warnings. Legacy commands use `export`/`import --pure`; v2 uses `session export`/`session import --standalone`. Native target import detects the installed CLI; convert-only defaults to legacy unless `--target-cli-version 2.0.22` or another accepted v2 release is selected. diff --git a/scripts/install-native-test-clis.sh b/scripts/install-native-test-clis.sh index bf21eb5..4c798f3 100755 --- a/scripts/install-native-test-clis.sh +++ b/scripts/install-native-test-clis.sh @@ -74,7 +74,7 @@ if want mastracode; then SESSION_MIGRATE_MASTRACODE_BIN node_modules/.bin/mastracode fi if want opencode; then - npm_cli opencode 'opencode-ai@1.17.20' \ + npm_cli opencode 'opencode-ai@1.19.4' \ SESSION_MIGRATE_OPENCODE_BIN node_modules/.bin/opencode fi if want opencode-v2; then diff --git a/src/session_migrate/conversion.py b/src/session_migrate/conversion.py index 4af8f9b..baf9830 100644 --- a/src/session_migrate/conversion.py +++ b/src/session_migrate/conversion.py @@ -196,8 +196,8 @@ def load_opencode_session( observed_version = _opencode_version(cli, values) if not opencode.supported_version(observed_version): raise SessionMigrateError( - "OpenCode source CLI version mismatch: expected " - f"{opencode.PINNED_OPENCODE_VERSION} or supported 2.0.x, observed {observed_version}" + "OpenCode source CLI version mismatch: expected 1.19+ 1.x or supported 2.0.x, " + f"observed {observed_version}" ) temporary_root = values.get("TMPDIR") with tempfile.TemporaryDirectory( @@ -1428,7 +1428,8 @@ def install_opencode_artifact( values.setdefault("OPENCODE_DISABLE_PRUNE", "true") if not opencode.supported_version(artifact.target_cli_version): raise SessionMigrateError( - "automatic OpenCode import requires target metadata version 1.17.20 or supported 2.0.x" + "automatic OpenCode import requires target metadata version 1.19+ 1.x " + "or supported 2.0.x" ) cli = _resolve_opencode_cli(target_cli, values) observed_version = _opencode_version(cli, values) @@ -1860,7 +1861,7 @@ def _opencode_session_ids(cli: Path, environ: Mapping[str, str]) -> set[str]: completed = _run_opencode([str(cli), "session", "list", "--format", "json", *flags], environ) if len(completed.stdout.encode()) > 64 * 1024 * 1024: raise SessionMigrateError("OpenCode session list exceeded the safety limit") - # Pinned OpenCode 1.17.20 emits an empty stream, rather than ``[]``, when + # OpenCode 1.x emits an empty stream, rather than ``[]``, when # its freshly initialized store has no sessions. if not completed.stdout.strip(): return set() diff --git a/src/session_migrate/formats/opencode.py b/src/session_migrate/formats/opencode.py index 6514410..7875e34 100644 --- a/src/session_migrate/formats/opencode.py +++ b/src/session_migrate/formats/opencode.py @@ -1,4 +1,4 @@ -"""OpenCode 1.17.20 and 2.0 public import/export adapters. +"""OpenCode 1.x (1.19+) and 2.0 public import/export adapters. The writer emits the public JSON shape consumed by ``opencode import``. It never writes OpenCode's SQLite database directly. @@ -23,8 +23,12 @@ from session_migrate.jsonl import file_sha256 from session_migrate.model import AgentFormat, Event, EventKind, Provenance, Role, Session -PINNED_OPENCODE_VERSION = "1.17.20" +PINNED_OPENCODE_VERSION = "1.19.4" OPENCODE_NATIVE_IMPORT_SUPPORTED = True +# Legacy 1.x releases keep the same public export, import, and session-list +# shape, so the 1.19+ family is accepted. +_LEGACY_MIN_VERSION = (19, 0) +_LEGACY_VERSION = re.compile(r"(?:opencode v)?1\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?") OPENCODE_IMPORT_MEDIA_TYPES = {"image/gif", "image/jpeg", "image/png", "image/webp"} MAX_NATIVE_BYTES = 256 * 1024 * 1024 MAX_NATIVE_MESSAGES = 1_000_000 @@ -54,8 +58,15 @@ def is_v2(value: str | None) -> bool: return value is not None and opencode_v2.version(value) is not None +def is_legacy(value: str | None) -> bool: + """Whether a 1.x release keeps the pinned legacy public bundle shape.""" + + match = None if value is None else _LEGACY_VERSION.fullmatch(value) + return match is not None and (int(match[1]), int(match[2])) >= _LEGACY_MIN_VERSION + + def supported_version(value: str | None) -> bool: - return value == PINNED_OPENCODE_VERSION or is_v2(value) + return is_legacy(value) or is_v2(value) def session_id_from_uuid(value: str) -> str: diff --git a/tests/fixtures/opencode-1.17.20/basic.json b/tests/fixtures/opencode-1.19.4/basic.json similarity index 99% rename from tests/fixtures/opencode-1.17.20/basic.json rename to tests/fixtures/opencode-1.19.4/basic.json index 6180347..a4af1aa 100644 --- a/tests/fixtures/opencode-1.17.20/basic.json +++ b/tests/fixtures/opencode-1.19.4/basic.json @@ -5,7 +5,7 @@ "projectID": "global", "directory": "/tmp/session-migrate-fixture", "title": "SYNTHETIC_IMPORTED_NAME", - "version": "1.17.20", + "version": "1.19.4", "time": { "created": 1787054400000, "updated": 1787054403000 diff --git a/tests/fixtures/opencode-source-1.17.20/comprehensive.json b/tests/fixtures/opencode-source-1.19.4/comprehensive.json similarity index 99% rename from tests/fixtures/opencode-source-1.17.20/comprehensive.json rename to tests/fixtures/opencode-source-1.19.4/comprehensive.json index ef39400..62b1cf3 100644 --- a/tests/fixtures/opencode-source-1.17.20/comprehensive.json +++ b/tests/fixtures/opencode-source-1.19.4/comprehensive.json @@ -25,7 +25,7 @@ "id": "fixture-model", "providerID": "fixture" }, - "version": "1.17.20", + "version": "1.19.4", "metadata": { "synthetic": true }, diff --git a/tests/native_corpus/route_oracle.py b/tests/native_corpus/route_oracle.py index f35b6be..4af4c47 100644 --- a/tests/native_corpus/route_oracle.py +++ b/tests/native_corpus/route_oracle.py @@ -493,6 +493,11 @@ def assert_artifact_warning_contract(artifact: ConversionArtifact, *, same_forma f"warnings={dropped_warnings!r}, dropped={artifact.dropped!r}" ) expected_other = ["same_format_portable_rewrite"] if same_format else [] + if ( + artifact.source.source_format.value == "opencode" + and artifact.source.cli_version != opencode.PINNED_OPENCODE_VERSION + ): + expected_other.append("unvalidated_source_version") if other_codes != expected_other: raise AssertionError( f"unexpected non-loss warning codes: expected={expected_other!r}, " diff --git a/tests/test_additional_formats.py b/tests/test_additional_formats.py index 8fc2a3d..90f6a6b 100644 --- a/tests/test_additional_formats.py +++ b/tests/test_additional_formats.py @@ -137,7 +137,7 @@ def portable_session(tmp_path: Path, *, compaction: bool = False) -> Session: ), ( opencode.parse, - FIXTURES / "opencode-1.17.20" / "basic.json", + FIXTURES / "opencode-1.19.4" / "basic.json", "ses_11111111111141118111111111111111", 8, ), @@ -571,7 +571,7 @@ def test_opencode_rejects_invalid_json_metadata_and_message_time(tmp_path: Path) with pytest.raises(SessionMigrateError, match="required metadata"): opencode.parse(invalid_metadata) - value = json.loads((FIXTURES / "opencode-1.17.20" / "basic.json").read_text()) + value = json.loads((FIXTURES / "opencode-1.19.4" / "basic.json").read_text()) value["messages"][0]["info"]["time"]["created"] = "yesterday" invalid_time = tmp_path / "time.json" invalid_time.write_text(json.dumps(value), encoding="utf-8") diff --git a/tests/test_additional_formats_native.py b/tests/test_additional_formats_native.py index a868aec..155fe60 100644 --- a/tests/test_additional_formats_native.py +++ b/tests/test_additional_formats_native.py @@ -300,6 +300,52 @@ def log_message(self, format: str, *args: object) -> None: del format, args +def fixture_loopback_config(port: int) -> dict[str, Any]: + return { + "model": "fixture/fixture-model", + "provider": { + "fixture": { + "npm": "@ai-sdk/openai-compatible", + "name": "Synthetic loopback", + "options": { + "baseURL": f"http://127.0.0.1:{port}/v1", + "apiKey": "synthetic-not-a-secret", + }, + "models": { + "fixture-model": { + "name": "Synthetic fixture model", + "attachment": True, + "modalities": {"input": ["text", "image"], "output": ["text"]}, + } + }, + } + }, + } + + +def require_fixture_provider(binary: str, env: dict[str, str]) -> None: + """Skip when a machine-level OpenCode config hides the fixture provider. + + OpenCode merges config from OS-level locations (for example + ``/Library/Application Support/opencode`` on macOS) that cannot be + redirected with environment variables, and an ``enabled_providers`` + whitelist there hides the synthetic loopback provider. + """ + probe_env = {**env, "OPENCODE_CONFIG_CONTENT": json.dumps(fixture_loopback_config(9))} + completed = subprocess.run( + [binary, "models", "--pure"], + env=probe_env, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + if completed.returncode != 0: + pytest.fail(f"opencode models probe failed: {completed.stderr.strip()}") + if "fixture/fixture-model" not in completed.stdout: + pytest.skip("a machine-level OpenCode config hides the fixture provider") + + @contextmanager def loopback_server() -> Iterator[tuple[ThreadingHTTPServer, type[LoopbackHandler]]]: handler = LoopbackHandler @@ -315,11 +361,12 @@ def loopback_server() -> Iterator[tuple[ThreadingHTTPServer, type[LoopbackHandle thread.join(timeout=5) -def test_opencode_11720_official_import_and_loopback_resume(tmp_path: Path) -> None: +def test_opencode_official_import_and_loopback_resume(tmp_path: Path) -> None: binary = exact_binary("opencode", opencode.PINNED_OPENCODE_VERSION, OPENCODE_FALLBACK) work = tmp_path / "work" work.mkdir() env = isolated_env(tmp_path) + require_fixture_provider(binary, env) data, dropped = opencode.serialize( portable_session(work), session_id=TARGET_OPENCODE_ID, @@ -371,30 +418,10 @@ def test_opencode_11720_official_import_and_loopback_resume(tmp_path: Path) -> N with loopback_server() as (server, handler): port = server.server_address[1] - config = { - "model": "fixture/fixture-model", - "provider": { - "fixture": { - "npm": "@ai-sdk/openai-compatible", - "name": "Synthetic loopback", - "options": { - "baseURL": f"http://127.0.0.1:{port}/v1", - "apiKey": "synthetic-not-a-secret", - }, - "models": { - "fixture-model": { - "name": "Synthetic fixture model", - "attachment": True, - "modalities": { - "input": ["text", "image"], - "output": ["text"], - }, - } - }, - } - }, + resume_env = { + **env, + "OPENCODE_CONFIG_CONTENT": json.dumps(fixture_loopback_config(port)), } - resume_env = {**env, "OPENCODE_CONFIG_CONTENT": json.dumps(config)} resumed = subprocess.run( [ binary, @@ -559,6 +586,7 @@ def test_opencode_native_replay_preserves_source_order_with_decreasing_timestamp work = tmp_path / "work" work.mkdir() env = isolated_env(tmp_path) + require_fixture_provider(binary, env) base = portable_session(work) source = replace( base, @@ -603,20 +631,6 @@ def test_opencode_native_replay_preserves_source_order_with_decreasing_timestamp with loopback_server() as (server, handler): port = server.server_address[1] - config = { - "model": "fixture/fixture-model", - "provider": { - "fixture": { - "npm": "@ai-sdk/openai-compatible", - "name": "Synthetic loopback", - "options": { - "baseURL": f"http://127.0.0.1:{port}/v1", - "apiKey": "synthetic-not-a-secret", - }, - "models": {"fixture-model": {"name": "Synthetic fixture model"}}, - } - }, - } resumed = subprocess.run( [ binary, @@ -631,7 +645,7 @@ def test_opencode_native_replay_preserves_source_order_with_decreasing_timestamp "--pure", ], cwd=work, - env={**env, "OPENCODE_CONFIG_CONTENT": json.dumps(config)}, + env={**env, "OPENCODE_CONFIG_CONTENT": json.dumps(fixture_loopback_config(port))}, check=False, capture_output=True, text=True, diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 9da67b1..6cb615d 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -202,7 +202,7 @@ def _insert_opencode_session( updated: int = 1_787_054_400_000, ) -> None: connection.execute( - "INSERT INTO session VALUES (?, 'global', ?, 'synthetic', ?, ?, '1.17.20', ?, ?, ?)", + "INSERT INTO session VALUES (?, 'global', ?, 'synthetic', ?, ?, '1.19.4', ?, ?, ?)", ( session_id, parent_id, diff --git a/tests/test_cli.py b/tests/test_cli.py index 4e1bae4..50621ee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -313,7 +313,7 @@ def test_transfer_requires_native_session_id_metadata(tmp_path: Path, capsys: ob def test_transfer_exports_native_opencode_source( tmp_path: Path, capsys: object, monkeypatch: pytest.MonkeyPatch ) -> None: - fixture = Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" + fixture = Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" source = opencode.parse_session(fixture) assert source.session_id is not None source_cli = tmp_path / "opencode" @@ -571,7 +571,7 @@ def test_transfer_by_title_rejects_ambiguous_exact_matches(tmp_path: Path, capsy def test_transfer_by_catalog_id_exports_virtual_opencode_source( tmp_path: Path, capsys: object, monkeypatch: pytest.MonkeyPatch ) -> None: - fixture = Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" + fixture = Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" source = opencode.parse_session(fixture) assert source.session_id is not None @@ -592,7 +592,7 @@ def test_transfer_by_catalog_id_exports_virtual_opencode_source( ) connection.execute( "INSERT INTO session VALUES (?, 'global', NULL, 'synthetic', ?, ?, " - "'1.17.20', 1787054400000, 1787054404000, NULL)", + "'1.19.4', 1787054400000, 1787054404000, NULL)", (source.session_id, str(tmp_path), "Synthetic OpenCode source"), ) connection.commit() diff --git a/tests/test_inspection.py b/tests/test_inspection.py index 281bbe3..84b21b4 100644 --- a/tests/test_inspection.py +++ b/tests/test_inspection.py @@ -200,7 +200,7 @@ def test_inspects_opencode_export_document_without_printing_content(tmp_path: Pa "id": "ses_11111111111141118111111111111111", "directory": "/work", "title": "private title", - "version": "1.17.20", + "version": "1.19.4", "time": {"created": 1787054400000, "updated": 1787054401000}, }, "messages": [ @@ -246,7 +246,7 @@ def test_inspects_opencode_export_document_without_printing_content(tmp_path: Pa def test_inspects_kilo_export_document_with_explicit_format(tmp_path: Path) -> None: source = json.loads( ( - Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" + Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" ).read_text() ) source["info"]["version"] = kilo.PINNED_KILO_VERSION diff --git a/tests/test_kilo_format.py b/tests/test_kilo_format.py index 3af85da..65c9738 100644 --- a/tests/test_kilo_format.py +++ b/tests/test_kilo_format.py @@ -9,7 +9,7 @@ from session_migrate.formats import kilo, opencode from session_migrate.model import AgentFormat -FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" +FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" def test_kilo_source_projects_official_export_bundle() -> None: diff --git a/tests/test_opencode_source.py b/tests/test_opencode_source.py index d21ad45..fccbdaf 100644 --- a/tests/test_opencode_source.py +++ b/tests/test_opencode_source.py @@ -8,7 +8,7 @@ from session_migrate.formats import opencode from session_migrate.model import AgentFormat, EventKind, Role -FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" +FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" def fixture_value() -> dict[str, object]: diff --git a/tests/test_opencode_source_native.py b/tests/test_opencode_source_native.py index f94304a..3ade42b 100644 --- a/tests/test_opencode_source_native.py +++ b/tests/test_opencode_source_native.py @@ -9,7 +9,7 @@ from session_migrate.formats import opencode from session_migrate.model import EventKind -FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.17.20" / "comprehensive.json" +FIXTURE = Path(__file__).parent / "fixtures" / "opencode-source-1.19.4" / "comprehensive.json" OPENCODE_FALLBACK = Path.home() / ".opencode/bin/opencode" LOOPBACK_UUID = "44444444-4444-4444-8444-444444444444" diff --git a/tests/test_opencode_v2.py b/tests/test_opencode_v2.py index 2a42baf..413277b 100644 --- a/tests/test_opencode_v2.py +++ b/tests/test_opencode_v2.py @@ -72,7 +72,8 @@ def test_streamed_tool_source_retains_partial_input_through_portable_target(tmp_ @pytest.mark.parametrize( ("release", "validated", "warning_expected"), [ - ("1.17.20", "1.17.20", False), + ("1.19.4", "1.19.4", False), + ("1.19.5", "1.19.4", True), ("2.0.22", "2.0.23", False), ("2.0.23", "2.0.23", False), ("opencode v2.0.23", "2.0.23", False), @@ -99,8 +100,11 @@ def test_target_version_warning_matches_selected_schema( if warning_expected: assert warnings[0]["validated"] == validated assert warnings[0]["observed"] == release - assert "selected OpenCode 2.0 transfer schema" in warnings[0]["message"] - assert "remains pinned" not in warnings[0]["message"] + if opencode.is_v2(release): + assert "selected OpenCode 2.0 transfer schema" in warnings[0]["message"] + assert "remains pinned" not in warnings[0]["message"] + else: + assert "remains pinned" in warnings[0]["message"] native = json.loads(target.native_bytes) assert ("location" in native["info"]) is opencode.is_v2(release) @@ -292,7 +296,14 @@ def test_v2_private_provider_state_accounted(tmp_path): ("2.1.0", False), ("3.0.0", False), ("9.9.9", False), - ("1.17.20", True), + ("1.19.4", True), + ("1.19.0", True), + ("opencode v1.19.4", True), + ("1.19.4-nightly.abc123", True), + ("1.17.20", False), + ("1.18.9", False), + ("1.0.0", False), + ("2.0.22", True), ], ) def test_version_schema_gate(value, expected): diff --git a/tests/test_opencode_v2_review.py b/tests/test_opencode_v2_review.py index 187981f..4d6e8da 100644 --- a/tests/test_opencode_v2_review.py +++ b/tests/test_opencode_v2_review.py @@ -164,7 +164,7 @@ def test_legacy_order_degradation_is_counted(tmp_path, target): source, ConversionOptions( target, - target_cli_version="1.17.20" if target == TargetFormat.OPENCODE else "7.5.0", + target_cli_version="1.19.4" if target == TargetFormat.OPENCODE else "7.5.0", cwd=tmp_path, ), ) @@ -458,7 +458,7 @@ def test_native_safety_limits_cover_content_erased_by_projection(tmp_path, monke def test_legacy_input_cannot_forge_native_content_carrier(tmp_path): bundle = source_bundle(tmp_path) _, _, source = rewrite(bundle, tmp_path) - artifact = convert_session(source, options(tmp_path, "1.17.20")) + artifact = convert_session(source, options(tmp_path, "1.19.4")) legacy = json.loads(artifact.native_bytes) state = next(p["state"] for m in legacy["messages"] for p in m["parts"] if p["type"] == "tool") state["content"] = mixed_blocks() diff --git a/tests/test_route_matrix.py b/tests/test_route_matrix.py index d898563..4c9fc2e 100644 --- a/tests/test_route_matrix.py +++ b/tests/test_route_matrix.py @@ -47,7 +47,7 @@ def source_sessions(tmp_path: Path) -> dict[str, Session]: "pi": pi.parse_session(FIXTURES / "pi-0.80.6" / "basic.jsonl"), "omp": omp.parse_session(FIXTURES / "omp-18.0.5" / "basic.jsonl"), "opencode": opencode.parse_session( - FIXTURES / "opencode-source-1.17.20" / "comprehensive.json" + FIXTURES / "opencode-source-1.19.4" / "comprehensive.json" ), "copilot": copilot.parse_session( FIXTURES / "copilot-source-1.0.70" / "copilot-source-native-events.jsonl" @@ -533,7 +533,7 @@ def test_codex_target_explicitly_counts_nonportable_tool_error_status(tmp_path: session_id="ses_synthetic", cwd=tmp_path, started_at="2026-08-20T12:00:00Z", - cli_version="1.17.20", + cli_version="1.19.4", model=None, title=None, raw_record_count=3, diff --git a/tests/test_target_integration.py b/tests/test_target_integration.py index a3b165c..650b4a3 100644 --- a/tests/test_target_integration.py +++ b/tests/test_target_integration.py @@ -99,7 +99,7 @@ def session_ids(path: Path, env: dict[str, str]) -> set[str]: def test_load_opencode_session_uses_official_export_and_virtual_source_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - fixture = FIXTURES / "opencode-source-1.17.20" / "comprehensive.json" + fixture = FIXTURES / "opencode-source-1.19.4" / "comprehensive.json" source_id = "ses_33333333333343338333333333333333" cli = tmp_path / "opencode" monkeypatch.setattr(conversion, "_resolve_opencode_cli", lambda path, env: cli) @@ -793,7 +793,7 @@ def test_opencode_empty_official_session_list_means_no_collisions( monkeypatch: pytest.MonkeyPatch, ) -> None: completed = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") - monkeypatch.setattr(conversion, "_opencode_version", lambda cli, env: "1.17.20") + monkeypatch.setattr(conversion, "_opencode_version", lambda cli, env: "1.19.4") monkeypatch.setattr(conversion, "_run_opencode", lambda command, env: completed) assert conversion._opencode_session_ids(Path("/synthetic/opencode"), {}) == set() @@ -829,7 +829,7 @@ def resolve(path: Path | None, env: dict[str, str]) -> Path: monkeypatch.setattr(conversion, "_resolve_opencode_cli", resolve) - with pytest.raises(SessionMigrateError, match="requires target metadata version 1.17.20"): + with pytest.raises(SessionMigrateError, match="requires target metadata version 1.19\\+ 1.x"): install_opencode_artifact( artifact, manifest_path=tmp_path / "manifest.json", @@ -879,7 +879,7 @@ def test_opencode_cli_target_version_override_cannot_bypass_pinned_import( assert status == 2 error = capsys.readouterr().err - assert "requires target metadata version 1.17.20" in error + assert "requires target metadata version 1.19+ 1.x" in error assert "was not found" not in error @@ -908,7 +908,7 @@ def install( return target_cli or Path("opencode") monkeypatch.setattr(cli_module, "install_opencode_artifact", install) - monkeypatch.setattr(cli_module, "detect_opencode_version", lambda cli: "1.17.20") + monkeypatch.setattr(cli_module, "detect_opencode_version", lambda cli: "1.19.4") monkeypatch.setenv("XDG_STATE_HOME", str(state)) status = main(