pactkit-codex 3.0.4__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.4
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.4"
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
 
@@ -0,0 +1,141 @@
1
+ """Codex's own declarations (SPEC-4.0-22 Step 6).
2
+
3
+ These lived in Core's transitional seed (`_CORE_CAPABILITIES`, `_CORE_SECURITY`,
4
+ `_CORE_PRETOOL_SURFACES`, `_CORE_DEPLOYMENT_CLAIMS`) until Step 6. While they
5
+ did, this adapter could not answer for itself: `import pactkit_codex` read Core's table
6
+ for its own identity, so removing the seed broke the import rather than the
7
+ deployment.
8
+
9
+ Core still owns the **vocabulary** (`capability.CAPABILITY_VOCABULARY`), the
10
+ four levels, and the hole-filling rule in `resolve_capabilities`. What a given
11
+ Host scores on them is the Host's own declaration — that is the split
12
+ SPEC-4.0-22 R1 draws, and this module is the Codex side of it.
13
+
14
+ Every value here was materialised from the seed Core was carrying, so this is a
15
+ move, not a re-measurement. The `Capability.evidence` strings are preserved
16
+ verbatim for the same reason: they are what the seed said, and re-wording them
17
+ here would quietly change what the registry reports.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pactkit.capability import Capability, CapabilityLevel
23
+ from pactkit.profiles import (
24
+ CLIPolicy,
25
+ DeploymentClaim,
26
+ FormatProfile,
27
+ InputTranslation,
28
+ SecurityDeclaration,
29
+ SecurityFacts,
30
+ )
31
+
32
+ __all__ = [
33
+ "CAPABILITIES",
34
+ "DEPLOYMENT_CLAIM",
35
+ "INPUT_TRANSLATION",
36
+ "PRETOOL_SURFACE",
37
+ "PROFILE",
38
+ "SECURITY",
39
+ ]
40
+
41
+
42
+ def _cap(name: str, level: CapabilityLevel, implemented: bool, evidence: str) -> Capability:
43
+ return Capability(name=name, level=level, implemented=implemented, evidence=evidence)
44
+
45
+
46
+ #: The Host's profile. Core no longer owns it; this module is the source.
47
+ PROFILE = FormatProfile(
48
+ name='codex',
49
+ display_name='Codex CLI',
50
+ global_config_dir='~/.codex',
51
+ project_config_dir='.codex',
52
+ skills_dir='~/.codex/skills',
53
+ agents_dir='~/.codex/agents',
54
+ commands_dir='~/.codex/prompts',
55
+ rules_dir='~/.codex/rules',
56
+ project_instructions_file='AGENTS.md',
57
+ global_instructions_file='AGENTS.md',
58
+ pactkit_yaml_path='.codex/pactkit.yaml',
59
+ agent_format='md',
60
+ rules_import_style='inline',
61
+ excluded_agent_fields=frozenset({'memory', 'permissionMode', 'skills', 'hooks'}),
62
+ excluded_commands=frozenset(),
63
+ supports_model_routing=False,
64
+ skills_path_var='~/.codex/skills',
65
+ command_deploy_form='skill',
66
+ agent_dialect=None,
67
+ command_dialect=None,
68
+ core_bundled=False,
69
+ cli_policy=CLIPolicy.PREFERRED,
70
+ )
71
+
72
+ #: What this Host scores on the capability vocabulary.
73
+ CAPABILITIES: dict[str, Capability] = {
74
+ 'completion_hook': _cap('completion_hook', CapabilityLevel.DEGRADED, False,
75
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
76
+ "unverified is not unavailable)",
77
+ ),
78
+ 'git_hook': _cap('git_hook', CapabilityLevel.NATIVE, True, 'shipped (commit_gate pre-commit chain)',
79
+ ),
80
+ 'global_instruction': _cap('global_instruction', CapabilityLevel.EQUIVALENT, True, 'shipped (~/.codex/AGENTS.md)',
81
+ ),
82
+ 'native_command': _cap('native_command', CapabilityLevel.DEGRADED, False,
83
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
84
+ "unverified is not unavailable)",
85
+ ),
86
+ 'native_skill': _cap('native_skill', CapabilityLevel.DEGRADED, False,
87
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
88
+ "unverified is not unavailable)",
89
+ ),
90
+ 'persistent_session': _cap('persistent_session', CapabilityLevel.DEGRADED, False,
91
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
92
+ "unverified is not unavailable)",
93
+ ),
94
+ 'post_tool_hook': _cap('post_tool_hook', CapabilityLevel.DEGRADED, False,
95
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
96
+ "unverified is not unavailable)",
97
+ ),
98
+ 'pre_tool_hook': _cap('pre_tool_hook', CapabilityLevel.NATIVE, True,
99
+ 'shipped (doctor.check_codex_hook_capability; native hooks engine)',
100
+ ),
101
+ 'project_instruction': _cap('project_instruction', CapabilityLevel.EQUIVALENT, True,
102
+ 'shipped (AGENTS.md at project root)',
103
+ ),
104
+ 'rule_import': _cap('rule_import', CapabilityLevel.EQUIVALENT, True, 'shipped as AGENTS.md (generators.deployer)',
105
+ ),
106
+ 'session_resume': _cap('session_resume', CapabilityLevel.DEGRADED, False,
107
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
108
+ "unverified is not unavailable)",
109
+ ),
110
+ 'shell_execution': _cap('shell_execution', CapabilityLevel.NATIVE, True, 'shipped (native shell tool)',
111
+ ),
112
+ 'structured_tool_result': _cap('structured_tool_result', CapabilityLevel.DEGRADED, False,
113
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
114
+ "unverified is not unavailable)",
115
+ ),
116
+ 'subagent': _cap('subagent', CapabilityLevel.DEGRADED, False,
117
+ "unverified — not measured on this Host; absence of the capability is NOT established (SPEC-4.0-04 R2: "
118
+ "unverified is not unavailable)",
119
+ ),
120
+ }
121
+
122
+ #: Files a gate must treat specially on this Host (SPEC-4.0-22 R8).
123
+ SECURITY = SecurityDeclaration(
124
+ env_capable=SecurityFacts.declared(['.codex/config.toml']),
125
+ protected=SecurityFacts.declared(['.codex/hooks.json']),
126
+ conditional=SecurityFacts.empty(),
127
+ )
128
+
129
+ #: Which mechanism this Host offers for pre-tool interception, when one
130
+ #: has been measured. `None` means unverified — not "cannot".
131
+ PRETOOL_SURFACE = 'native hooks engine'
132
+
133
+ #: How this Host executes and where its verification comes from.
134
+ DEPLOYMENT_CLAIM = DeploymentClaim(
135
+ execution_mode='portable',
136
+ verification_source='native_codex_session',
137
+ )
138
+
139
+ #: Read-side wire-format data, when the Host has one. `None` means it
140
+ #: declares none — the ladder then never asks it to recognise a payload.
141
+ INPUT_TRANSLATION: InputTranslation | None = None
@@ -46,7 +46,6 @@ from pactkit.generators.deployer import (
46
46
  _enforce_deploy_integrity,
47
47
  )
48
48
  from pactkit.generators.spi import render_prompt, render_skill_md
49
- from pactkit.profiles import get_profile
50
49
  from pactkit.prompts.guides import GUIDES_FILES
51
50
  from pactkit.prompts.rules import (
52
51
  COMMAND_CONDITIONAL_RULES_MAP,
@@ -57,6 +56,8 @@ from pactkit.prompts.rules import (
57
56
  )
58
57
  from pactkit.utils import atomic_write
59
58
 
59
+ from pactkit_codex import declarations
60
+
60
61
  # All canonical commands deploy. Sprint degrades to sequential execution when
61
62
  # Codex does not expose team orchestration primitives.
62
63
  CODEX_EXCLUDED_COMMANDS: frozenset[str] = frozenset()
@@ -111,7 +112,13 @@ class CodexDeployer(DeployerBase):
111
112
  - config.toml (not JSON/YAML)
112
113
  """
113
114
 
114
- profile = get_profile("codex")
115
+ # SPEC-4.0-22 Step 6 — the Host's own declarations (see declarations.py).
116
+ profile = declarations.PROFILE
117
+ capabilities = declarations.CAPABILITIES
118
+ security = declarations.SECURITY
119
+ pretool_surface = declarations.PRETOOL_SURFACE
120
+ deployment_claim = declarations.DEPLOYMENT_CLAIM
121
+ input_translation = declarations.INPUT_TRANSLATION
115
122
 
116
123
  @staticmethod
117
124
  def continuation_capabilities(codex_root=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pactkit-codex
3
- Version: 3.0.4
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
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/pactkit_codex/__init__.py
5
+ src/pactkit_codex/declarations.py
5
6
  src/pactkit_codex/deployer.py
6
7
  src/pactkit_codex/enforcement.py
7
8
  src/pactkit_codex.egg-info/PKG-INFO
@@ -10,4 +11,6 @@ src/pactkit_codex.egg-info/dependency_links.txt
10
11
  src/pactkit_codex.egg-info/entry_points.txt
11
12
  src/pactkit_codex.egg-info/requires.txt
12
13
  src/pactkit_codex.egg-info/top_level.txt
13
- 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