pactkit-codex 3.0.5__tar.gz → 3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pactkit-codex
3
- Version: 3.0.5
3
+ Version: 3.1.0
4
4
  Summary: PactKit adapter for OpenAI Codex CLI
5
5
  Author: Slim
6
6
  License: MIT
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
21
21
  Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: pactkit<3.1.0,>=3.0.1
24
+ Requires-Dist: pactkit<3.2.0,>=3.0.1
25
25
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
26
26
  Dynamic: license-file
27
27
 
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pactkit-codex"
7
- version = "3.0.5"
7
+ version = "3.1.0"
8
8
  description = "PactKit adapter for OpenAI Codex CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  requires-python = ">=3.10"
12
12
  dependencies = [
13
- "pactkit>=3.0.1,<3.1.0",
13
+ "pactkit>=3.0.1,<3.2.0",
14
14
  "tomli>=1.1.0; python_version < '3.11'",
15
15
  ]
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pactkit-codex
3
- Version: 3.0.5
3
+ Version: 3.1.0
4
4
  Summary: PactKit adapter for OpenAI Codex CLI
5
5
  Author: Slim
6
6
  License: MIT
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
21
21
  Requires-Python: >=3.10
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: pactkit<3.1.0,>=3.0.1
24
+ Requires-Dist: pactkit<3.2.0,>=3.0.1
25
25
  Requires-Dist: tomli>=1.1.0; python_version < "3.11"
26
26
  Dynamic: license-file
27
27
 
@@ -11,4 +11,6 @@ src/pactkit_codex.egg-info/dependency_links.txt
11
11
  src/pactkit_codex.egg-info/entry_points.txt
12
12
  src/pactkit_codex.egg-info/requires.txt
13
13
  src/pactkit_codex.egg-info/top_level.txt
14
- tests/test_host_body_snapshot.py
14
+ tests/test_capability_contract.py
15
+ tests/test_host_body_snapshot.py
16
+ tests/test_profile_contract.py
@@ -1,4 +1,4 @@
1
- pactkit<3.1.0,>=3.0.1
1
+ pactkit<3.2.0,>=3.0.1
2
2
 
3
3
  [:python_version < "3.11"]
4
4
  tomli>=1.1.0
@@ -0,0 +1,112 @@
1
+ """SPEC-4.0-22 Step 7 — 本宿主的 capability / deployment claim / pretool 契约。
2
+
3
+ **为什么这份测试在本仓。** 这三张表(能力声明、部署声明、pre-tool 机制名)
4
+ 自 Step 6/7 起由本 adapter 自带并经 entry-point 注册。core 侧只保留它随包发布
5
+ 的 `classic`。所以「本宿主能做什么、怎么执行、pre-tool 机制叫什么」的断言属于
6
+ **本仓**——生产里的事实归谁,断言就归谁。
7
+
8
+ core 侧对应断言(`test_spec40_22_capability_registry.py` 的宿主值行)已随之删除。
9
+ **是搬迁,不是复制**:两处各留一份就是给同一个事实造两个真相源。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pactkit.capability import resolve_capabilities
15
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
16
+ from pactkit.profiles import registered_adapters
17
+
18
+ HOST = "codex"
19
+ PRETOOL = 'native hooks engine'
20
+ CLAIM = ('portable', 'native_codex_session')
21
+ ENV_CAPABLE = {'.codex/config.toml'}
22
+ PROTECTED = {'.codex/hooks.json'}
23
+ CONDITIONAL = set()
24
+ PRESENT = ['git_hook', 'global_instruction', 'pre_tool_hook', 'project_instruction', 'rule_import', 'shell_execution']
25
+
26
+
27
+ def _registration():
28
+ _ensure_entry_point_deployers()
29
+ return registered_adapters()[HOST]
30
+
31
+
32
+ class TestTheDeclarationReachesTheRegistry:
33
+ def test_the_registry_carries_this_hosts_claim(self):
34
+ claim = _registration().deployment_claim
35
+ assert (claim.execution_mode, claim.verification_source) == CLAIM
36
+
37
+ def test_the_registry_carries_this_hosts_pretool_mechanism(self):
38
+ assert _registration().pretool_surface == PRETOOL
39
+
40
+
41
+ class TestTheCapabilityDeclaration:
42
+ def test_the_present_set_is_exactly_this(self):
43
+ """**逐项**钉住,不是只钉个数——多一项少一项都要红。
44
+
45
+ 迁移前基线是 6 项;这条断言的是同一批名字,所以它既防漏声明,
46
+ 也防「为了让表看起来完整」而虚报一项。
47
+ """
48
+ resolved = resolve_capabilities(HOST)
49
+ assert resolved.known
50
+ present = sorted(n for n, e in resolved.entries.items() if e.present)
51
+ assert present == PRESENT
52
+
53
+ def test_every_declared_entry_carries_evidence(self):
54
+ for name, entry in resolve_capabilities(HOST).entries.items():
55
+ assert entry.evidence, f"{name} 没有证据来源——那就是一个断言,不是声明"
56
+
57
+ def test_the_declaration_is_not_empty(self):
58
+ assert _registration().capabilities, (
59
+ "本宿主没有声明任何能力——registry 会把每一项都填成 Degraded"
60
+ )
61
+
62
+
63
+ class TestTheContractHasTeeth:
64
+ """反证:改坏本仓的声明,上面必须有测试变红。"""
65
+
66
+ def test_mutating_the_claim_moves_the_answer(self):
67
+ import dataclasses
68
+
69
+ from pactkit.profiles import register_adapter
70
+ import pactkit_codex.declarations as decl
71
+
72
+ entry = _registration()
73
+ broken = dataclasses.replace(
74
+ entry.deployment_claim, verification_source="mutated"
75
+ )
76
+ register_adapter(HOST, deployer=entry.deployer, profile=entry.profile,
77
+ deployment_claim=broken)
78
+ try:
79
+ assert _registration().deployment_claim.verification_source == "mutated", (
80
+ "改掉本仓的声明后 registry 没变——core 侧还有第二份真相源"
81
+ )
82
+ finally:
83
+ register_adapter(HOST, deployer=entry.deployer, profile=entry.profile,
84
+ deployment_claim=decl.DEPLOYMENT_CLAIM)
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # 安全事实(SPEC-4.0-22 R8 / Step 7)
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ class TestTheSecurityFacts:
93
+ """这三类的定义域是 **PactKit gate-relative**:`env` 能到达 gate 进程的文件、
94
+ gate 不许任何人编辑的文件、移除 gate 条目算篡改的文件。
95
+
96
+ 所以它们是**本宿主的声明**——core 侧那张跨宿主表已随 Step 7 删除,这里的
97
+ 断言跟事实走。
98
+ """
99
+
100
+ def test_the_declared_files_are_exactly_these(self):
101
+ s = _registration().security
102
+ assert set(s.env_capable.files) == ENV_CAPABLE
103
+ assert set(s.protected.files) == PROTECTED
104
+ assert set(s.conditional.files) == CONDITIONAL
105
+
106
+ def test_the_declaration_is_complete(self):
107
+ """`missing`(宿主没说话)与 `empty`(宿主说了它没有)是两件事。
108
+
109
+ 声明不完整时,需要 tamper_guard 授权的写操作会 fail closed——那是
110
+ R8 的设计,不是缺陷。所以「完整」本身是要被钉住的事实。
111
+ """
112
+ assert _registration().security.complete
@@ -0,0 +1,186 @@
1
+ """SPEC-4.0-22 Step 7 — 本宿主的 profile 契约。
2
+
3
+ **为什么这份测试在本仓。** codex 的 `FormatProfile` 自 Step 6/7 起由本 adapter
4
+ 自带并经 entry-point 注册;core 侧只保留它随包发布的 `classic`。所以「codex 的
5
+ 全局配置目录是什么」「它的命令以什么形态落盘」这类断言属于**本仓**——生产里的
6
+ 事实在谁那里,断言就在谁那里。
7
+
8
+ core 侧对应的 `test_story_slim060_codex_profile.py` / `test_story059_codex_removal.py`
9
+ 已随之删除。**是搬迁,不是复制**:两处各留一份,就是给同一个事实造两个真相源,
10
+ 而漂移的那天两边都会绿。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import cast
16
+
17
+ import pytest
18
+
19
+ from pactkit.profiles import (
20
+ CLIPolicy,
21
+ FormatProfile,
22
+ get_profile,
23
+ )
24
+ from pactkit_codex.declarations import PROFILE
25
+
26
+ SPEC = cast(FormatProfile, PROFILE)
27
+
28
+
29
+ class TestTheProfileIsReachable:
30
+ """注册真的发生,且生产读路径(registry)能拿到它。"""
31
+
32
+ def test_it_is_registered(self):
33
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
34
+
35
+ _ensure_entry_point_deployers()
36
+ assert get_profile("codex").name == "codex"
37
+
38
+ def test_get_profile_returns_the_same_object(self):
39
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
40
+ from pactkit.profiles import get_registration
41
+
42
+ _ensure_entry_point_deployers()
43
+ assert get_registration("codex").profile is SPEC, (
44
+ "registry 里那份不是本仓声明的这个对象——说明还有第二个真相源"
45
+ )
46
+
47
+ def test_it_is_a_format_profile(self):
48
+ assert isinstance(SPEC, FormatProfile)
49
+
50
+ def test_it_is_frozen(self):
51
+ with pytest.raises(AttributeError):
52
+ SPEC.name = "modified"
53
+
54
+
55
+ class TestThePathFacts:
56
+ """本宿主的目录布局——**实测值**,不是从 core 抄来的默认值。"""
57
+
58
+ @pytest.mark.parametrize(
59
+ "field,expected",
60
+ [
61
+ ("name", "codex"),
62
+ ("display_name", "Codex CLI"),
63
+ ("global_config_dir", "~/.codex"),
64
+ ("project_config_dir", ".codex"),
65
+ ("skills_dir", "~/.codex/skills"),
66
+ ("agents_dir", "~/.codex/agents"),
67
+ ("rules_dir", "~/.codex/rules"),
68
+ ("project_instructions_file", "AGENTS.md"),
69
+ ("pactkit_yaml_path", ".codex/pactkit.yaml"),
70
+ ("agent_format", "md"),
71
+ ("rules_import_style", "inline"),
72
+ ("skills_path_var", "~/.codex/skills"),
73
+ ],
74
+ )
75
+ def test_field(self, field, expected):
76
+ assert getattr(SPEC, field) == expected
77
+
78
+
79
+ class TestTheMeasuredLayoutFact:
80
+ def test_commands_deploy_as_skills(self):
81
+ """**实测**(2026-09-22):codex 的命令落在 `skills/{name}/SKILL.md`,
82
+ 不是 `commands_dir` 指的 `~/.codex/prompts`(那个目录根本不存在)。
83
+
84
+ 这一条是本仓最该守的布局事实之一:`commands_dir` 是一个**没人写的路径**,
85
+ 把它当布局用会得到一份永远不会被发现的产物。
86
+ """
87
+ assert SPEC.command_deploy_form == "skill", (
88
+ "codex 的命令以 skill 形态落盘(adapter 自己的 docstring 与实测一致);"
89
+ "改成 `flat`/`prompt` 会让产物落到一个没人读的目录"
90
+ )
91
+
92
+ def test_commands_dir_is_not_the_layout_fact(self):
93
+ """`commands_dir` 仍在(字段合法性),但它**不是**布局事实。"""
94
+ assert SPEC.commands_dir == "~/.codex/prompts"
95
+ assert SPEC.command_deploy_form != "flat", (
96
+ "若两者相等,说明有人把 `commands_dir` 当成了落盘位置"
97
+ )
98
+
99
+
100
+ class TestTheCLIPolicy:
101
+ def test_codex_prefers_the_pactkit_cli(self):
102
+ assert SPEC.cli_policy is CLIPolicy.PREFERRED
103
+
104
+ def test_has_pactkit_cli_is_derived_from_the_policy(self):
105
+ assert SPEC.has_pactkit_cli is True
106
+
107
+
108
+ class TestTheDeclaredDialects:
109
+ """codex 自带 writer、不调共享 API,所以方言字段**没有消费者**。
110
+
111
+ 为了让表看起来完整而填 `"claude"`,就是造一条死通道。
112
+ """
113
+
114
+ @pytest.mark.parametrize("field", ["agent_dialect", "command_dialect"])
115
+ def test_it_declares_nothing(self, field):
116
+ assert getattr(SPEC, field) is None, (
117
+ f"{field} 被填了值——codex 不调共享 writer,这个值没有消费者"
118
+ )
119
+
120
+
121
+ class TestTheLiveViewsSeeIt:
122
+ """core 的活视图(`VALID_FORMATS` / `PACTKIT_YAML_CANDIDATES`)在注册后
123
+ 必须包含本宿主——**这是 core 的机制**,但只有本仓能把它端到端证明。"""
124
+
125
+ def test_it_is_in_valid_formats(self):
126
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
127
+ from pactkit.profiles import VALID_FORMATS
128
+
129
+ _ensure_entry_point_deployers()
130
+ assert "codex" in VALID_FORMATS
131
+
132
+ def test_its_config_path_is_a_discovery_candidate(self):
133
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
134
+ from pactkit.profiles import PACTKIT_YAML_CANDIDATES
135
+
136
+ _ensure_entry_point_deployers()
137
+ assert SPEC.pactkit_yaml_path in PACTKIT_YAML_CANDIDATES
138
+
139
+
140
+ class TestTheContractHasTeeth:
141
+ """反证:改坏声明,上面必须有测试变红。
142
+
143
+ 没有这一条,「搬过来了」只是两个仓各自为绿的巧合。
144
+ """
145
+
146
+ def test_the_profile_is_what_get_profile_answers(self):
147
+ """把本仓的声明换掉,`get_profile` 必须跟着变。
148
+
149
+ 若它不变,说明 core 还留着一份——那正是「复制一份、core 还留一份」的形态。
150
+ """
151
+ import dataclasses
152
+
153
+ import pactkit_codex.declarations as decl
154
+ from pactkit.generators.deploy_base import _ensure_entry_point_deployers
155
+ from pactkit.profiles import get_registration, register_adapter
156
+
157
+ _ensure_entry_point_deployers()
158
+ broken = dataclasses.replace(SPEC, display_name="Not Codex")
159
+ register_adapter(
160
+ "codex",
161
+ deployer=get_registration("codex").deployer,
162
+ profile=broken,
163
+ )
164
+ try:
165
+ assert get_profile("codex").display_name == "Not Codex", (
166
+ "改掉本仓的声明后 get_profile 没变——core 侧还有第二份真相源"
167
+ )
168
+ finally:
169
+ register_adapter(
170
+ "codex",
171
+ deployer=get_registration("codex").deployer,
172
+ profile=decl.PROFILE,
173
+ )
174
+
175
+
176
+ class TestTheCLIPolicyIsDeclaredHere:
177
+ """`cli_policy` 是**本宿主的**声明:它决定部署产物要不要依赖 PactKit CLI。
178
+
179
+ core 侧对应断言(`test_story_slim084.py::TestHasPactkitCli`)已随之删除。
180
+ """
181
+
182
+ def test_the_policy(self):
183
+ assert SPEC.cli_policy is CLIPolicy.PREFERRED
184
+
185
+ def test_has_pactkit_cli_is_derived(self):
186
+ assert SPEC.has_pactkit_cli is True
File without changes
File without changes
File without changes