py-opencode-wrapper 0.1.3__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/PKG-INFO +10 -7
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/README.md +8 -5
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/opencode_wrapper/config.py +5 -2
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/PKG-INFO +10 -7
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/SOURCES.txt +2 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/pyproject.toml +2 -2
- py_opencode_wrapper-0.1.5/tests/test_config_instructions.py +83 -0
- py_opencode_wrapper-0.1.5/tests/test_integration_instructions.py +166 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/opencode_wrapper/__init__.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/opencode_wrapper/client.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/opencode_wrapper/errors.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/opencode_wrapper/events.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/dependency_links.txt +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/requires.txt +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/top_level.txt +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/setup.cfg +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_client_async.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_config_permission.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_event_parser.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_integration_external_directory.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_integration_multi_agent_weather.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_integration_opencode.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_integration_parallel.py +0 -0
- {py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_run_result_fuzzy_text.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-opencode-wrapper
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Async Python wrapper for OpenCode CLI (opencode run --format json)
|
|
5
5
|
Project-URL: Homepage, https://github.com/idailylife/oc_py_wrapper
|
|
6
6
|
Project-URL: Repository, https://github.com/idailylife/oc_py_wrapper
|
|
7
7
|
Project-URL: Issues, https://github.com/idailylife/oc_py_wrapper/issues
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Provides-Extra: dev
|
|
11
11
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
@@ -101,12 +101,13 @@ Per-call JSON is merged and passed as `OPENCODE_CONFIG_CONTENT` (see [OpenCode c
|
|
|
101
101
|
|
|
102
102
|
| Field | Purpose |
|
|
103
103
|
|--------|---------|
|
|
104
|
-
| `permission` | `permission` map (`allow` / `
|
|
104
|
+
| `permission` | `permission` map (`allow` / `deny`, patterns) |
|
|
105
105
|
| `mcp` | MCP server definitions |
|
|
106
106
|
| `tools` | Enable/disable tools (including MCP globs) |
|
|
107
107
|
| `config_overrides` | Any extra top-level config keys to deep-merge |
|
|
108
108
|
|
|
109
109
|
Optional env tuning: `disable_autoupdate=True` sets `OPENCODE_DISABLE_AUTOUPDATE=1`.
|
|
110
|
+
Note: `ask` is intentionally rejected in subprocess mode (no interactive terminal); use `allow` or `deny`.
|
|
110
111
|
|
|
111
112
|
## CLI arguments
|
|
112
113
|
|
|
@@ -147,13 +148,15 @@ Default `pytest -q` runs **all** tests; use `-m "not integration"` in CI without
|
|
|
147
148
|
|
|
148
149
|
## Concurrency notes
|
|
149
150
|
|
|
150
|
-
When running many tasks with `asyncio.gather`,
|
|
151
|
+
When running many tasks with `asyncio.gather`, three protections are enabled by default:
|
|
151
152
|
|
|
152
|
-
**Startup serialisation** — `
|
|
153
|
+
**Startup serialisation** — `startup_concurrency=1` and `startup_delay_s=0.3` limit how many processes enter SQLite startup at once, reducing WAL-initialisation race crashes.
|
|
153
154
|
|
|
154
|
-
**
|
|
155
|
+
**DB isolation** — `isolate_db=True` gives each run a private `XDG_DATA_HOME`, so concurrent runs do not contend on the same `opencode.db` during tool execution.
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
**Automatic retry** — `async_run(max_retries=2, retry_delay_s=1.0)` retries known SQLite-startup crashes with short backoff. Non-SQLite failures still fail fast.
|
|
158
|
+
|
|
159
|
+
Set `startup_concurrency=0`, `isolate_db=False`, and `max_retries=0` to opt out.
|
|
157
160
|
|
|
158
161
|
## Notes
|
|
159
162
|
|
|
@@ -88,12 +88,13 @@ Per-call JSON is merged and passed as `OPENCODE_CONFIG_CONTENT` (see [OpenCode c
|
|
|
88
88
|
|
|
89
89
|
| Field | Purpose |
|
|
90
90
|
|--------|---------|
|
|
91
|
-
| `permission` | `permission` map (`allow` / `
|
|
91
|
+
| `permission` | `permission` map (`allow` / `deny`, patterns) |
|
|
92
92
|
| `mcp` | MCP server definitions |
|
|
93
93
|
| `tools` | Enable/disable tools (including MCP globs) |
|
|
94
94
|
| `config_overrides` | Any extra top-level config keys to deep-merge |
|
|
95
95
|
|
|
96
96
|
Optional env tuning: `disable_autoupdate=True` sets `OPENCODE_DISABLE_AUTOUPDATE=1`.
|
|
97
|
+
Note: `ask` is intentionally rejected in subprocess mode (no interactive terminal); use `allow` or `deny`.
|
|
97
98
|
|
|
98
99
|
## CLI arguments
|
|
99
100
|
|
|
@@ -134,13 +135,15 @@ Default `pytest -q` runs **all** tests; use `-m "not integration"` in CI without
|
|
|
134
135
|
|
|
135
136
|
## Concurrency notes
|
|
136
137
|
|
|
137
|
-
When running many tasks with `asyncio.gather`,
|
|
138
|
+
When running many tasks with `asyncio.gather`, three protections are enabled by default:
|
|
138
139
|
|
|
139
|
-
**Startup serialisation** — `
|
|
140
|
+
**Startup serialisation** — `startup_concurrency=1` and `startup_delay_s=0.3` limit how many processes enter SQLite startup at once, reducing WAL-initialisation race crashes.
|
|
140
141
|
|
|
141
|
-
**
|
|
142
|
+
**DB isolation** — `isolate_db=True` gives each run a private `XDG_DATA_HOME`, so concurrent runs do not contend on the same `opencode.db` during tool execution.
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
**Automatic retry** — `async_run(max_retries=2, retry_delay_s=1.0)` retries known SQLite-startup crashes with short backoff. Non-SQLite failures still fail fast.
|
|
145
|
+
|
|
146
|
+
Set `startup_concurrency=0`, `isolate_db=False`, and `max_retries=0` to opt out.
|
|
144
147
|
|
|
145
148
|
## Notes
|
|
146
149
|
|
|
@@ -5,13 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Mapping
|
|
8
|
+
from typing import Any, Dict, Mapping
|
|
9
9
|
|
|
10
10
|
# Permission values accepted by OpenCode
|
|
11
11
|
PermissionAction = str # "allow" | "ask" | "deny"
|
|
12
12
|
|
|
13
13
|
# Nested permission maps: tool name -> action or pattern -> action
|
|
14
|
-
PermissionMap =
|
|
14
|
+
PermissionMap = Dict[str, Any]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _deep_merge(base: dict[str, Any], override: Mapping[str, Any]) -> dict[str, Any]:
|
|
@@ -55,6 +55,7 @@ class RunConfig:
|
|
|
55
55
|
permission: PermissionMap | None = None
|
|
56
56
|
mcp: dict[str, Any] | None = None
|
|
57
57
|
tools: dict[str, Any] | None = None
|
|
58
|
+
instructions: list[str] | None = None
|
|
58
59
|
config_overrides: dict[str, Any] | None = None
|
|
59
60
|
|
|
60
61
|
def build_opencode_config_dict(self) -> dict[str, Any]:
|
|
@@ -68,6 +69,8 @@ class RunConfig:
|
|
|
68
69
|
merged = _deep_merge(merged, {"mcp": dict(self.mcp)})
|
|
69
70
|
if self.tools is not None:
|
|
70
71
|
merged = _deep_merge(merged, {"tools": dict(self.tools)})
|
|
72
|
+
if self.instructions is not None:
|
|
73
|
+
merged = _deep_merge(merged, {"instructions": list(self.instructions)})
|
|
71
74
|
return merged
|
|
72
75
|
|
|
73
76
|
def opencode_config_content_json(self) -> str | None:
|
{py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/PKG-INFO
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-opencode-wrapper
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Async Python wrapper for OpenCode CLI (opencode run --format json)
|
|
5
5
|
Project-URL: Homepage, https://github.com/idailylife/oc_py_wrapper
|
|
6
6
|
Project-URL: Repository, https://github.com/idailylife/oc_py_wrapper
|
|
7
7
|
Project-URL: Issues, https://github.com/idailylife/oc_py_wrapper/issues
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Provides-Extra: dev
|
|
11
11
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
@@ -101,12 +101,13 @@ Per-call JSON is merged and passed as `OPENCODE_CONFIG_CONTENT` (see [OpenCode c
|
|
|
101
101
|
|
|
102
102
|
| Field | Purpose |
|
|
103
103
|
|--------|---------|
|
|
104
|
-
| `permission` | `permission` map (`allow` / `
|
|
104
|
+
| `permission` | `permission` map (`allow` / `deny`, patterns) |
|
|
105
105
|
| `mcp` | MCP server definitions |
|
|
106
106
|
| `tools` | Enable/disable tools (including MCP globs) |
|
|
107
107
|
| `config_overrides` | Any extra top-level config keys to deep-merge |
|
|
108
108
|
|
|
109
109
|
Optional env tuning: `disable_autoupdate=True` sets `OPENCODE_DISABLE_AUTOUPDATE=1`.
|
|
110
|
+
Note: `ask` is intentionally rejected in subprocess mode (no interactive terminal); use `allow` or `deny`.
|
|
110
111
|
|
|
111
112
|
## CLI arguments
|
|
112
113
|
|
|
@@ -147,13 +148,15 @@ Default `pytest -q` runs **all** tests; use `-m "not integration"` in CI without
|
|
|
147
148
|
|
|
148
149
|
## Concurrency notes
|
|
149
150
|
|
|
150
|
-
When running many tasks with `asyncio.gather`,
|
|
151
|
+
When running many tasks with `asyncio.gather`, three protections are enabled by default:
|
|
151
152
|
|
|
152
|
-
**Startup serialisation** — `
|
|
153
|
+
**Startup serialisation** — `startup_concurrency=1` and `startup_delay_s=0.3` limit how many processes enter SQLite startup at once, reducing WAL-initialisation race crashes.
|
|
153
154
|
|
|
154
|
-
**
|
|
155
|
+
**DB isolation** — `isolate_db=True` gives each run a private `XDG_DATA_HOME`, so concurrent runs do not contend on the same `opencode.db` during tool execution.
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
**Automatic retry** — `async_run(max_retries=2, retry_delay_s=1.0)` retries known SQLite-startup crashes with short backoff. Non-SQLite failures still fail fast.
|
|
158
|
+
|
|
159
|
+
Set `startup_concurrency=0`, `isolate_db=False`, and `max_retries=0` to opt out.
|
|
157
160
|
|
|
158
161
|
## Notes
|
|
159
162
|
|
{py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/SOURCES.txt
RENAMED
|
@@ -11,9 +11,11 @@ py_opencode_wrapper.egg-info/dependency_links.txt
|
|
|
11
11
|
py_opencode_wrapper.egg-info/requires.txt
|
|
12
12
|
py_opencode_wrapper.egg-info/top_level.txt
|
|
13
13
|
tests/test_client_async.py
|
|
14
|
+
tests/test_config_instructions.py
|
|
14
15
|
tests/test_config_permission.py
|
|
15
16
|
tests/test_event_parser.py
|
|
16
17
|
tests/test_integration_external_directory.py
|
|
18
|
+
tests/test_integration_instructions.py
|
|
17
19
|
tests/test_integration_multi_agent_weather.py
|
|
18
20
|
tests/test_integration_opencode.py
|
|
19
21
|
tests/test_integration_parallel.py
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "py-opencode-wrapper"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5"
|
|
4
4
|
description = "Async Python wrapper for OpenCode CLI (opencode run --format json)"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
7
|
dependencies = []
|
|
8
8
|
|
|
9
9
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tests for ``RunConfig.instructions`` field."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from opencode_wrapper.client import build_env
|
|
8
|
+
from opencode_wrapper.config import RunConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_instructions_single_file() -> None:
|
|
12
|
+
cfg = RunConfig(instructions=["AGENT.md"])
|
|
13
|
+
d = cfg.build_opencode_config_dict()
|
|
14
|
+
assert d["instructions"] == ["AGENT.md"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_instructions_multiple_files() -> None:
|
|
18
|
+
cfg = RunConfig(instructions=["CONTRIBUTING.md", "docs/guidelines.md"])
|
|
19
|
+
d = cfg.build_opencode_config_dict()
|
|
20
|
+
assert d["instructions"] == ["CONTRIBUTING.md", "docs/guidelines.md"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_instructions_glob_pattern() -> None:
|
|
24
|
+
cfg = RunConfig(instructions=[".cursor/rules/*.md"])
|
|
25
|
+
d = cfg.build_opencode_config_dict()
|
|
26
|
+
assert d["instructions"] == [".cursor/rules/*.md"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_instructions_none_omits_key() -> None:
|
|
30
|
+
cfg = RunConfig(instructions=None)
|
|
31
|
+
d = cfg.build_opencode_config_dict()
|
|
32
|
+
assert "instructions" not in d
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_instructions_empty_list() -> None:
|
|
36
|
+
cfg = RunConfig(instructions=[])
|
|
37
|
+
d = cfg.build_opencode_config_dict()
|
|
38
|
+
assert d["instructions"] == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_instructions_in_opencode_config_content_json() -> None:
|
|
42
|
+
cfg = RunConfig(instructions=["AGENT.md", "docs/*.md"])
|
|
43
|
+
env = build_env(cfg, base={})
|
|
44
|
+
payload = json.loads(env["OPENCODE_CONFIG_CONTENT"])
|
|
45
|
+
assert payload["instructions"] == ["AGENT.md", "docs/*.md"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_instructions_deep_merges_with_config_overrides() -> None:
|
|
49
|
+
"""instructions field wins over config_overrides when both set."""
|
|
50
|
+
cfg = RunConfig(
|
|
51
|
+
config_overrides={"instructions": ["old.md"]},
|
|
52
|
+
instructions=["new.md"],
|
|
53
|
+
)
|
|
54
|
+
d = cfg.build_opencode_config_dict()
|
|
55
|
+
# instructions applied after config_overrides, so it overwrites
|
|
56
|
+
assert d["instructions"] == ["new.md"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_instructions_config_overrides_used_when_no_instructions_field() -> None:
|
|
60
|
+
cfg = RunConfig(config_overrides={"instructions": ["via_overrides.md"]})
|
|
61
|
+
d = cfg.build_opencode_config_dict()
|
|
62
|
+
assert d["instructions"] == ["via_overrides.md"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_instructions_combined_with_permission_and_mcp() -> None:
|
|
66
|
+
cfg = RunConfig(
|
|
67
|
+
permission={"bash": "allow"},
|
|
68
|
+
mcp={"my-server": {"type": "local", "command": ["npx", "my-mcp"]}},
|
|
69
|
+
instructions=["AGENT.md"],
|
|
70
|
+
)
|
|
71
|
+
d = cfg.build_opencode_config_dict()
|
|
72
|
+
assert d["instructions"] == ["AGENT.md"]
|
|
73
|
+
assert d["permission"]["bash"] == "allow"
|
|
74
|
+
assert d["mcp"]["my-server"]["type"] == "local"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_instructions_not_mutated() -> None:
|
|
78
|
+
"""build_opencode_config_dict returns a copy; original list is not mutated."""
|
|
79
|
+
original = ["AGENT.md"]
|
|
80
|
+
cfg = RunConfig(instructions=original)
|
|
81
|
+
d = cfg.build_opencode_config_dict()
|
|
82
|
+
d["instructions"].append("extra.md")
|
|
83
|
+
assert cfg.instructions == ["AGENT.md"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Integration test: verify ``RunConfig.instructions`` is actually loaded by opencode.
|
|
2
|
+
|
|
3
|
+
Two complementary strategies:
|
|
4
|
+
|
|
5
|
+
Strategy A — "probe keyword":
|
|
6
|
+
The instruction defines a magic keyword → fixed reply mapping.
|
|
7
|
+
Send the keyword; assert the exact reply appears.
|
|
8
|
+
Send the keyword WITHOUT the instruction; assert the reply is absent.
|
|
9
|
+
This is the most reliable approach: the response is deterministic and the
|
|
10
|
+
keyword is meaningless without the instruction, so false positives are impossible.
|
|
11
|
+
|
|
12
|
+
Strategy B — "canary prefix" (kept as a cross-check):
|
|
13
|
+
The instruction mandates a unique token at the start of every response.
|
|
14
|
+
Ask the model directly what the token is; assert it appears.
|
|
15
|
+
|
|
16
|
+
Requires: real ``opencode`` on PATH + configured provider.
|
|
17
|
+
Run::
|
|
18
|
+
|
|
19
|
+
pytest -m integration -q tests/test_integration_instructions.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
from opencode_wrapper import AsyncOpenCodeClient, RunConfig
|
|
30
|
+
|
|
31
|
+
PROBE_KEYWORD = "__probe_instruction__"
|
|
32
|
+
PROBE_REPLY = "MAKE AGENTS GREAT AGAIN!"
|
|
33
|
+
|
|
34
|
+
# Unique enough that the model won't emit it by accident.
|
|
35
|
+
CANARY = "XCANARY_INST_77Z"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def probe_instruction_file(integration_workspace: Path) -> Path:
|
|
40
|
+
"""Write the probe instruction file inside the workspace so the model can read it."""
|
|
41
|
+
f = integration_workspace / "probe_instructions.md"
|
|
42
|
+
f.write_text(
|
|
43
|
+
f'When the user inputs exactly "{PROBE_KEYWORD}", '
|
|
44
|
+
f'reply with exactly "{PROBE_REPLY}" and nothing else.\n',
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
)
|
|
47
|
+
return f
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def canary_instruction_file(integration_workspace: Path) -> Path:
|
|
52
|
+
"""Write the canary instruction file inside the workspace so the model can read it."""
|
|
53
|
+
f = integration_workspace / "canary_instructions.md"
|
|
54
|
+
f.write_text(
|
|
55
|
+
f"Always begin every response with the exact token: {CANARY}\n",
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
return f
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.integration
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_probe_keyword_triggers_defined_reply(
|
|
64
|
+
opencode_path: str,
|
|
65
|
+
integration_workspace: Path,
|
|
66
|
+
probe_instruction_file: Path,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Magic keyword defined in the instruction file produces the exact expected reply."""
|
|
69
|
+
client = AsyncOpenCodeClient(binary=opencode_path)
|
|
70
|
+
timeout = float(os.environ.get("OPENCODE_INTEGRATION_TIMEOUT_S", "300"))
|
|
71
|
+
|
|
72
|
+
result = await client.async_run(
|
|
73
|
+
PROBE_KEYWORD,
|
|
74
|
+
integration_workspace,
|
|
75
|
+
run_cfg=RunConfig(
|
|
76
|
+
disable_autoupdate=True,
|
|
77
|
+
instructions=[str(probe_instruction_file)],
|
|
78
|
+
),
|
|
79
|
+
timeout_s=timeout,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert result.exit_code == 0
|
|
83
|
+
blob = (result.final_text or "") + "\n".join(str(e) for e in result.events)
|
|
84
|
+
assert PROBE_REPLY in blob, (
|
|
85
|
+
f"Expected reply {PROBE_REPLY!r} not found — instruction file may not have been loaded.\n"
|
|
86
|
+
f"final_text={result.final_text!r}\nstderr={result.stderr!r}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.integration
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_probe_keyword_no_reply_without_instruction(
|
|
93
|
+
opencode_path: str,
|
|
94
|
+
integration_workspace: Path,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Same keyword WITHOUT the instruction does not produce the probe reply."""
|
|
97
|
+
client = AsyncOpenCodeClient(binary=opencode_path)
|
|
98
|
+
timeout = float(os.environ.get("OPENCODE_INTEGRATION_TIMEOUT_S", "300"))
|
|
99
|
+
|
|
100
|
+
result = await client.async_run(
|
|
101
|
+
PROBE_KEYWORD,
|
|
102
|
+
integration_workspace,
|
|
103
|
+
run_cfg=RunConfig(agent="plan", disable_autoupdate=True),
|
|
104
|
+
timeout_s=timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
assert result.exit_code == 0
|
|
108
|
+
blob = (result.final_text or "") + "\n".join(str(e) for e in result.events)
|
|
109
|
+
assert PROBE_REPLY not in blob, (
|
|
110
|
+
f"Probe reply appeared without instruction — false positive.\n"
|
|
111
|
+
f"final_text={result.final_text!r}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.integration
|
|
116
|
+
@pytest.mark.asyncio
|
|
117
|
+
async def test_canary_instruction_file_is_loaded(
|
|
118
|
+
opencode_path: str,
|
|
119
|
+
integration_workspace: Path,
|
|
120
|
+
canary_instruction_file: Path,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Model response contains canary token when instruction file is provided."""
|
|
123
|
+
client = AsyncOpenCodeClient(binary=opencode_path)
|
|
124
|
+
timeout = float(os.environ.get("OPENCODE_INTEGRATION_TIMEOUT_S", "300"))
|
|
125
|
+
|
|
126
|
+
result = await client.async_run(
|
|
127
|
+
"Your instructions define a mandatory response prefix token. Output that token and nothing else.",
|
|
128
|
+
integration_workspace,
|
|
129
|
+
run_cfg=RunConfig(
|
|
130
|
+
disable_autoupdate=True,
|
|
131
|
+
instructions=[str(canary_instruction_file)],
|
|
132
|
+
),
|
|
133
|
+
timeout_s=timeout,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert result.exit_code == 0
|
|
137
|
+
blob = (result.final_text or "") + "\n".join(str(e) for e in result.events)
|
|
138
|
+
assert CANARY in blob, (
|
|
139
|
+
f"Canary token {CANARY!r} not found — instruction file may not have been loaded.\n"
|
|
140
|
+
f"final_text={result.final_text!r}\nstderr={result.stderr!r}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.integration
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_canary_instruction_absent_without_field(
|
|
147
|
+
opencode_path: str,
|
|
148
|
+
integration_workspace: Path,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Same prompt without instruction file does NOT produce the canary token."""
|
|
151
|
+
client = AsyncOpenCodeClient(binary=opencode_path)
|
|
152
|
+
timeout = float(os.environ.get("OPENCODE_INTEGRATION_TIMEOUT_S", "300"))
|
|
153
|
+
|
|
154
|
+
result = await client.async_run(
|
|
155
|
+
"Your instructions define a mandatory response prefix token. Output that token and nothing else.",
|
|
156
|
+
integration_workspace,
|
|
157
|
+
run_cfg=RunConfig(agent="plan", disable_autoupdate=True),
|
|
158
|
+
timeout_s=timeout,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
assert result.exit_code == 0
|
|
162
|
+
blob = (result.final_text or "") + "\n".join(str(e) for e in result.events)
|
|
163
|
+
assert CANARY not in blob, (
|
|
164
|
+
f"Canary token appeared without instruction — something is wrong.\n"
|
|
165
|
+
f"final_text={result.final_text!r}"
|
|
166
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/requires.txt
RENAMED
|
File without changes
|
{py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/py_opencode_wrapper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_opencode_wrapper-0.1.3 → py_opencode_wrapper-0.1.5}/tests/test_integration_external_directory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|