pactkit-opencode 3.1.0__tar.gz → 4.1.0__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.
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/PKG-INFO +2 -2
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/pyproject.toml +2 -2
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode/deployer.py +45 -7
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/PKG-INFO +2 -2
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/SOURCES.txt +1 -0
- pactkit_opencode-4.1.0/src/pactkit_opencode.egg-info/requires.txt +1 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_deploy_opencode_parity.py +17 -12
- pactkit_opencode-4.1.0/tests/test_step8_declaration_search_producer.py +190 -0
- pactkit_opencode-3.1.0/src/pactkit_opencode.egg-info/requires.txt +0 -1
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/README.md +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/setup.cfg +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode/__init__.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode/declarations.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode/enforcement.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/dependency_links.txt +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/entry_points.txt +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/top_level.txt +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_a1_config_recovery.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_a2_mcp_retired.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_bug035_opencode_dual_layer.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_capability_contract.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_gate_plugin_deploy.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_host_body_snapshot.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_opencode_enforcement_channel.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_profile_contract.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_step6_self_sufficiency.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story069_opencode_format.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story070_opencode_compliance.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story071_opencode_config_parity.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story073_command_model_routing.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story_work_units.py +0 -0
- {pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_wire_format_contract.py +0 -0
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pactkit-opencode"
|
|
7
|
-
version = "
|
|
7
|
+
version = "4.1.0"
|
|
8
8
|
description = "PactKit adapter for OpenCode IDE"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"pactkit>=
|
|
12
|
+
"pactkit>=4.1.0,<4.2.0",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[project.entry-points."pactkit.deployers"]
|
|
@@ -28,6 +28,7 @@ from pactkit.generators.deployer import (
|
|
|
28
28
|
_deploy_rules,
|
|
29
29
|
_deploy_skills,
|
|
30
30
|
)
|
|
31
|
+
from pactkit.generators.spi import generate_config_if_missing
|
|
31
32
|
from pactkit_opencode import declarations
|
|
32
33
|
from pactkit.utils import atomic_write
|
|
33
34
|
|
|
@@ -132,8 +133,22 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
132
133
|
deployment_claim = declarations.DEPLOYMENT_CLAIM
|
|
133
134
|
input_translation = declarations.INPUT_TRANSLATION
|
|
134
135
|
|
|
135
|
-
def deploy(self, config=None, target=None):
|
|
136
|
+
def deploy(self, config=None, target=None, project_root=None, no_redetect=False):
|
|
137
|
+
"""Deploy PactKit configuration for OpenCode.
|
|
138
|
+
|
|
139
|
+
``project_root`` is **not** the deploy root: OpenCode installs globally
|
|
140
|
+
under ``~/.config/opencode``, and the two MUST NOT be confused. It
|
|
141
|
+
names the project whose config, overlay and CI file are read/written —
|
|
142
|
+
those used to resolve from the caller's cwd, so ``pactkit -C
|
|
143
|
+
<other-project>`` read the wrong project's policy.
|
|
144
|
+
|
|
145
|
+
``no_redetect`` is accepted because the Core dispatcher passes it
|
|
146
|
+
alongside ``project_root``; it is a no-op here — this deployer's
|
|
147
|
+
stack probe only reads the filesystem and never writes the stack
|
|
148
|
+
back into the config.
|
|
149
|
+
"""
|
|
136
150
|
opencode_root = Path(target) if target else Path.home() / ".config" / "opencode"
|
|
151
|
+
project_root = Path(project_root) if project_root is not None else Path.cwd()
|
|
137
152
|
|
|
138
153
|
print("🚀 PactKit OpenCode Deployment")
|
|
139
154
|
|
|
@@ -149,7 +164,7 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
149
164
|
# STORY-slim-008 R1+R2: Load config from project-level pactkit.yaml
|
|
150
165
|
from pactkit.config import find_pactkit_yaml
|
|
151
166
|
|
|
152
|
-
project_yaml = find_pactkit_yaml()
|
|
167
|
+
project_yaml = find_pactkit_yaml(project_root)
|
|
153
168
|
|
|
154
169
|
if config is not None:
|
|
155
170
|
cfg = config
|
|
@@ -160,7 +175,7 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
160
175
|
cfg = load_config(project_yaml)
|
|
161
176
|
else:
|
|
162
177
|
cfg = {}
|
|
163
|
-
cfg = activate_pactkit_maintainer_overlay(cfg,
|
|
178
|
+
cfg = activate_pactkit_maintainer_overlay(cfg, project_root)
|
|
164
179
|
|
|
165
180
|
enabled_agents = cfg.get("agents", sorted(VALID_AGENTS))
|
|
166
181
|
enabled_skills = cfg.get("skills", sorted(VALID_SKILLS))
|
|
@@ -238,7 +253,6 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
238
253
|
|
|
239
254
|
ci_config = cfg.get("ci", {})
|
|
240
255
|
ci_provider = ci_config.get("provider", "none") if isinstance(ci_config, dict) else "none"
|
|
241
|
-
project_root = Path.cwd()
|
|
242
256
|
_deploy_ci(ci_provider, project_root, cfg)
|
|
243
257
|
|
|
244
258
|
# STORY-slim-139 R2: machine-readable deployment manifest
|
|
@@ -263,7 +277,8 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
263
277
|
)
|
|
264
278
|
|
|
265
279
|
if target is None:
|
|
266
|
-
self._generate_project_agents_md()
|
|
280
|
+
self._generate_project_agents_md(project_root)
|
|
281
|
+
self._generate_project_config(project_root)
|
|
267
282
|
|
|
268
283
|
# --- OpenCode-specific helpers ---
|
|
269
284
|
|
|
@@ -289,9 +304,32 @@ class OpenCodeDeployer(DeployerBase):
|
|
|
289
304
|
return 1
|
|
290
305
|
|
|
291
306
|
@staticmethod
|
|
292
|
-
def
|
|
307
|
+
def _generate_project_config(project_root=None):
|
|
308
|
+
"""Write this host's pactkit.yaml when the project has none (Step 8 R-③-1).
|
|
309
|
+
|
|
310
|
+
Before this existed, `.opencode/pactkit.yaml` was **declared** in
|
|
311
|
+
`profile.pactkit_yaml_path` and **searched** by every reader, but no
|
|
312
|
+
code path produced it — the three facts R-③-1 requires to agree did
|
|
313
|
+
not. It could still appear anyway: `init --format classic` in a project
|
|
314
|
+
carrying a `.opencode/` directory wrote CLASSIC's config there, because
|
|
315
|
+
the path fell to `resolve_pactkit_yaml_dir`'s directory-existence
|
|
316
|
+
fallback (measured 2026-09-26: `mkdir .opencode && pactkit init
|
|
317
|
+
--format classic` → `.opencode/pactkit.yaml` containing classic's).
|
|
318
|
+
|
|
319
|
+
`format="opencode"` closes both halves: this host produces its own
|
|
320
|
+
copy, and no other host can produce this one's.
|
|
321
|
+
"""
|
|
322
|
+
# Direct callers may omit the root; the deploy path always passes it.
|
|
323
|
+
project_root = Path(project_root) if project_root is not None else Path.cwd()
|
|
324
|
+
if project_root.resolve() == Path.home().resolve():
|
|
325
|
+
return
|
|
326
|
+
generate_config_if_missing(format="opencode", project_root=project_root)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _generate_project_agents_md(project_root=None):
|
|
293
330
|
"""Generate project-level AGENTS.md if it does not exist (STORY-slim-008 R4)."""
|
|
294
|
-
|
|
331
|
+
# Direct callers may omit the root; the deploy path always passes it.
|
|
332
|
+
project_root = Path(project_root) if project_root is not None else Path.cwd()
|
|
295
333
|
if project_root.resolve() == Path.home().resolve():
|
|
296
334
|
return
|
|
297
335
|
agents_md_path = project_root / "AGENTS.md"
|
|
@@ -20,6 +20,7 @@ tests/test_host_body_snapshot.py
|
|
|
20
20
|
tests/test_opencode_enforcement_channel.py
|
|
21
21
|
tests/test_profile_contract.py
|
|
22
22
|
tests/test_step6_self_sufficiency.py
|
|
23
|
+
tests/test_step8_declaration_search_producer.py
|
|
23
24
|
tests/test_story069_opencode_format.py
|
|
24
25
|
tests/test_story070_opencode_compliance.py
|
|
25
26
|
tests/test_story071_opencode_config_parity.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pactkit<4.2.0,>=4.1.0
|
|
@@ -271,18 +271,23 @@ class TestGenerateConfigIfMissing:
|
|
|
271
271
|
yaml_path = tmp_path / ".claude" / "pactkit.yaml"
|
|
272
272
|
assert yaml_path.exists()
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
# `test_no_format_autodetects` stood here. It asserted that with no `format`
|
|
275
|
+
# argument, Core's generator followed whichever candidate directory existed
|
|
276
|
+
# — "existing behavior preserved", its docstring said. SPEC-4.0-22 Step 8
|
|
277
|
+
# deleted that behaviour (owner 2026-09-26: *reading may discover existing
|
|
278
|
+
# layers; writing must know which format it is writing for*).
|
|
279
|
+
#
|
|
280
|
+
# The contract it holds now — `format=None` resolves to classic's path and
|
|
281
|
+
# consults no directory — is **Core's**, so it is asserted in Core
|
|
282
|
+
# (`tests/unit/test_profiles.py::test_no_format_is_classic_regardless_of_which_dirs_exist`,
|
|
283
|
+
# `test_spec40_22_step8_config_generation.py`). A copy here would buy no
|
|
284
|
+
# coverage and would couple this adapter's CI to Core's release schedule:
|
|
285
|
+
# this repo installs Core from PyPI, so such a copy could only go green once
|
|
286
|
+
# the Step 8 Core was published — a red that says nothing about this repo.
|
|
287
|
+
#
|
|
288
|
+
# What this adapter owns is that IT never relies on that default. See
|
|
289
|
+
# `test_step8_declaration_search_producer.py`: it drives the real `deploy()`
|
|
290
|
+
# and asserts the config lands at this host's own declared path.
|
|
286
291
|
|
|
287
292
|
def test_no_write_when_yaml_exists(self, tmp_path):
|
|
288
293
|
from pactkit.generators.deployer import _generate_config_if_missing
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""SPEC-4.0-22 Step 8 R-③-1: declaration, search and producer agree.
|
|
2
|
+
|
|
3
|
+
Before this, `.opencode/pactkit.yaml` was **declared** in
|
|
4
|
+
`profile.pactkit_yaml_path` and **searched** by every reader, but nothing
|
|
5
|
+
produced it. owner 2026-09-23:
|
|
6
|
+
|
|
7
|
+
> 这不是「缺一个文件」这么简单,而是 registration/profile 在宣称一个生命周期
|
|
8
|
+
> 事实,实际实现却永远不兑现;「声明 + 搜索 + 不生产」会让后续判断把一个不
|
|
9
|
+
> 存在的状态当成正常候选。
|
|
10
|
+
|
|
11
|
+
It could still appear, which was worse than not existing: `init --format classic`
|
|
12
|
+
in a project carrying a `.opencode/` directory wrote **classic's** config there,
|
|
13
|
+
because the path fell to `resolve_pactkit_yaml_dir`'s directory-existence
|
|
14
|
+
fallback (measured 2026-09-26). A file whose contents belong to another host is
|
|
15
|
+
a lifecycle fact nobody can act on.
|
|
16
|
+
|
|
17
|
+
The chosen fix is **A — really generate it** (owner 2026-09-26), so all four
|
|
18
|
+
hosts follow the same model: the project-root `pactkit.yaml` is the hand-written
|
|
19
|
+
shared policy, and each host directory gets a generated override.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
SRC = Path(__file__).resolve().parent.parent / "src"
|
|
31
|
+
|
|
32
|
+
#: String content a config template must contain. `stack: ` keeps its trailing
|
|
33
|
+
#: space on purpose — it is the constant half of `f"stack: {stack}\n"`, and prose
|
|
34
|
+
#: about the defect ("the template wrote `stack: unknown`") is not a string at
|
|
35
|
+
#: all, so it cannot reach this check.
|
|
36
|
+
TEMPLATE_MARKERS = ("version: 0.0.1", "stack: ")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _string_literals(tree: ast.AST) -> list[str]:
|
|
40
|
+
"""Every string this module can build, including f-string constant parts.
|
|
41
|
+
|
|
42
|
+
**AST, never grep.** A text scan cannot tell a template from a sentence
|
|
43
|
+
about one: it fires on the comment documenting the deletion and stays silent
|
|
44
|
+
when someone pastes the real thing back inside a string.
|
|
45
|
+
"""
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
for node in ast.walk(tree):
|
|
48
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
49
|
+
out.append(node.value)
|
|
50
|
+
elif isinstance(node, ast.JoinedStr):
|
|
51
|
+
out.extend(
|
|
52
|
+
part.value for part in node.values
|
|
53
|
+
if isinstance(part, ast.Constant) and isinstance(part.value, str)
|
|
54
|
+
)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestNoTemplateSurvives:
|
|
59
|
+
"""AC-②-2,对**三仓**都成立——包括这一个。
|
|
60
|
+
|
|
61
|
+
本仓从来没有过内联模板,所以这条今天是白送;但 AC-②-2 判的是**状态**不是
|
|
62
|
+
历史,而「多一份模板」正是最容易在别处复制粘贴回来的东西。
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def test_the_shipped_package_contains_no_template_literal(self):
|
|
66
|
+
hits = [
|
|
67
|
+
f"{py.relative_to(SRC.parent).as_posix()}: {marker!r}"
|
|
68
|
+
for py in sorted(SRC.rglob("*.py"))
|
|
69
|
+
for marker in TEMPLATE_MARKERS
|
|
70
|
+
if any(marker in s for s in _string_literals(
|
|
71
|
+
ast.parse(py.read_text(encoding="utf-8"), filename=str(py))
|
|
72
|
+
))
|
|
73
|
+
]
|
|
74
|
+
assert not hits, (
|
|
75
|
+
f"a pactkit.yaml template literal is in the shipped package: {hits}. "
|
|
76
|
+
f"Declare WHERE the file lives and WHETHER it is needed; Core's "
|
|
77
|
+
f"canonical generator owns the content (Step 8 R-②-1/R-②-4)."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def test_the_enumeration_is_not_vacuous(self):
|
|
81
|
+
"""反证:枚举器确实看得见字符串——包括 f-string 的常量半段。"""
|
|
82
|
+
found = _string_literals(ast.parse('a = "version: 0.0.1"\nb = f"stack: {x}\\n"\n'))
|
|
83
|
+
assert "version: 0.0.1" in found
|
|
84
|
+
assert "stack: " in found, f"f-string 的常量半段没被枚举到:{found}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def project(tmp_path, monkeypatch):
|
|
89
|
+
"""Run the REAL `deploy()` path with `Path.home()` pointed at `tmp_path`.
|
|
90
|
+
|
|
91
|
+
`--target` is preview mode and deliberately skips project-level files (the
|
|
92
|
+
same guard covers `AGENTS.md` and codex's project files), so a test that
|
|
93
|
+
passes a target would be asserting about a path production never takes.
|
|
94
|
+
Without a target, `deploy()` writes its host tree to `Path.home()/.opencode`
|
|
95
|
+
— redirected here so the assertion can run without touching the real home.
|
|
96
|
+
"""
|
|
97
|
+
proj = tmp_path / "proj"
|
|
98
|
+
proj.mkdir()
|
|
99
|
+
monkeypatch.chdir(proj)
|
|
100
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
|
|
101
|
+
return proj
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestTheThreeFactsAgree:
|
|
105
|
+
def test_the_declared_path_is_the_searched_path(self):
|
|
106
|
+
"""声明 == 搜索。两者都从 profile 派生,分歧会立刻可见。"""
|
|
107
|
+
from pactkit.profiles import PACTKIT_YAML_CANDIDATES, get_profile
|
|
108
|
+
|
|
109
|
+
declared = get_profile("opencode").pactkit_yaml_path
|
|
110
|
+
assert declared in PACTKIT_YAML_CANDIDATES, (
|
|
111
|
+
f"profile 声明 {declared!r},候选表里却没有它:"
|
|
112
|
+
f"{list(PACTKIT_YAML_CANDIDATES)}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def test_the_declared_path_is_where_this_host_writes(self, project):
|
|
116
|
+
"""声明 == 生产者。这是缺的那一半——前两处一直一致,只有它在缺席。"""
|
|
117
|
+
from pactkit.profiles import get_profile
|
|
118
|
+
|
|
119
|
+
from pactkit_opencode import OpenCodeDeployer
|
|
120
|
+
|
|
121
|
+
OpenCodeDeployer().deploy()
|
|
122
|
+
|
|
123
|
+
declared = project / get_profile("opencode").pactkit_yaml_path
|
|
124
|
+
assert declared.is_file(), (
|
|
125
|
+
f"本宿主声明 {declared!r} 却从不生产它——"
|
|
126
|
+
f"found: {sorted(p.relative_to(project).as_posix() for p in project.rglob('pactkit.yaml'))}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def test_it_is_cores_canonical_output(self, project):
|
|
130
|
+
"""内容来自 core 的 canonical generator,不是本仓的第二份模板。
|
|
131
|
+
|
|
132
|
+
逐字段断言会在「core 改了格式而本仓漂移」时照样通过;比字节不会。
|
|
133
|
+
|
|
134
|
+
**两层各比各的生成器**(STORY-slim-20260928c7b15e04a216 Step 1a):
|
|
135
|
+
共享根对 `generate_default_yaml`,宿主路径对 `generate_sparse_host_yaml`。
|
|
136
|
+
只比其中一层,另一层就能悄悄漂移。
|
|
137
|
+
"""
|
|
138
|
+
from pactkit.cleaners import detect_stacks
|
|
139
|
+
from pactkit.config import generate_default_yaml, generate_sparse_host_yaml
|
|
140
|
+
from pactkit.profiles import get_profile
|
|
141
|
+
|
|
142
|
+
from pactkit_opencode import OpenCodeDeployer
|
|
143
|
+
|
|
144
|
+
OpenCodeDeployer().deploy()
|
|
145
|
+
|
|
146
|
+
shared = (project / "pactkit.yaml").read_text()
|
|
147
|
+
assert shared == generate_default_yaml(stack=detect_stacks(project))
|
|
148
|
+
|
|
149
|
+
host = (project / get_profile("opencode").pactkit_yaml_path).read_text()
|
|
150
|
+
assert host == generate_sparse_host_yaml()
|
|
151
|
+
|
|
152
|
+
def test_the_config_is_valid_and_carries_the_format_version(self, project):
|
|
153
|
+
from pactkit.config import VALID_STACKS
|
|
154
|
+
from pactkit.profiles import get_profile
|
|
155
|
+
|
|
156
|
+
from pactkit_opencode import OpenCodeDeployer
|
|
157
|
+
|
|
158
|
+
OpenCodeDeployer().deploy()
|
|
159
|
+
|
|
160
|
+
data = yaml.safe_load(
|
|
161
|
+
(project / get_profile("opencode").pactkit_yaml_path).read_text()
|
|
162
|
+
)
|
|
163
|
+
assert data["schema_version"] == 1
|
|
164
|
+
# 策略在共享根里;宿主层是**覆盖**,不是策略的第二份副本。
|
|
165
|
+
shared = yaml.safe_load((project / "pactkit.yaml").read_text())
|
|
166
|
+
assert shared["stack"] in VALID_STACKS
|
|
167
|
+
assert "stack" not in data, (
|
|
168
|
+
f"宿主层重复了项目策略:{sorted(data)}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestTheCrossWriteIsClosed:
|
|
173
|
+
def test_another_hosts_init_does_not_create_this_hosts_config(self, tmp_path):
|
|
174
|
+
"""R-③-2:`.opencode/pactkit.yaml` 的内容 MUST NOT 由别的宿主决定。
|
|
175
|
+
|
|
176
|
+
本仓是那个「目录存在就被劫持」的受害者,所以这条断言放在这里:
|
|
177
|
+
只要 core 的生成路径再次丢掉显式 format,这个文件就会带着 classic 的
|
|
178
|
+
内容出现,而本仓的声明仍然指向它。
|
|
179
|
+
"""
|
|
180
|
+
from pactkit.generators.deployer import _generate_config_if_missing
|
|
181
|
+
|
|
182
|
+
(tmp_path / ".opencode").mkdir()
|
|
183
|
+
|
|
184
|
+
_generate_config_if_missing(format="classic", project_root=tmp_path)
|
|
185
|
+
|
|
186
|
+
assert not (tmp_path / ".opencode" / "pactkit.yaml").exists(), (
|
|
187
|
+
"classic 的 init 在 opencode 的目录里写了 pactkit.yaml —— "
|
|
188
|
+
"路径又被目录存在性劫持了(Step 8 R-③-2)"
|
|
189
|
+
)
|
|
190
|
+
assert (tmp_path / ".claude" / "pactkit.yaml").is_file()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pactkit<3.2.0,>=3.0.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/src/pactkit_opencode.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_opencode_enforcement_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story070_opencode_compliance.py
RENAMED
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story071_opencode_config_parity.py
RENAMED
|
File without changes
|
{pactkit_opencode-3.1.0 → pactkit_opencode-4.1.0}/tests/test_story073_command_model_routing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|