workstate-protocol 0.1.7__tar.gz → 0.2.1__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.
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/PKG-INFO +1 -1
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/pyproject.toml +1 -1
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/__init__.py +1 -1
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/bootstrap.py +81 -10
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/PKG-INFO +1 -1
- workstate_protocol-0.2.1/tests/test_plugin_override_schema.py +330 -0
- workstate_protocol-0.1.7/tests/test_plugin_override_schema.py +0 -145
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/README.md +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/setup.cfg +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/branch_naming.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/compaction.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/env_aliases.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/handoff.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/hooks.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/paths.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/py.typed +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/skills.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/SOURCES.txt +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/dependency_links.txt +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/requires.txt +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/top_level.txt +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_bootstrap_manifest.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_branch_grammar_registry.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_branch_naming.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_env_aliases.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_handoff_schema.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_package_metadata.py +0 -0
- {workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_skill_manifest_real_skills.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workstate-protocol
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/darce/workstate
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "workstate-protocol"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -9,9 +9,18 @@ single typed shape rather than per-key heuristics.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from pathlib import PurePosixPath
|
|
12
13
|
from typing import Annotated, Literal
|
|
13
14
|
|
|
14
|
-
from pydantic import
|
|
15
|
+
from pydantic import (
|
|
16
|
+
BaseModel,
|
|
17
|
+
ConfigDict,
|
|
18
|
+
Field,
|
|
19
|
+
StringConstraints,
|
|
20
|
+
WithJsonSchema,
|
|
21
|
+
field_validator,
|
|
22
|
+
model_validator,
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
Sha40 = Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{40}$")]
|
|
@@ -20,6 +29,35 @@ PluginComponentName = Annotated[
|
|
|
20
29
|
str,
|
|
21
30
|
StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]*$"),
|
|
22
31
|
]
|
|
32
|
+
PluginOverrideRelativePath = Annotated[
|
|
33
|
+
str,
|
|
34
|
+
StringConstraints(min_length=1),
|
|
35
|
+
WithJsonSchema(
|
|
36
|
+
{
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"not": {
|
|
40
|
+
"anyOf": [
|
|
41
|
+
{"pattern": "^/"},
|
|
42
|
+
{"pattern": "(^|/)\\.\\.(/|$)"},
|
|
43
|
+
{"pattern": "^\\.?$"},
|
|
44
|
+
{"pattern": "\\\\"},
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_plugin_override_path(value: str) -> str:
|
|
53
|
+
if "\\" in value:
|
|
54
|
+
raise ValueError("override paths must use POSIX '/' separators")
|
|
55
|
+
path = PurePosixPath(value)
|
|
56
|
+
if path.is_absolute() or value == "." or ".." in path.parts:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"override paths must be relative and stay under the override root"
|
|
59
|
+
)
|
|
60
|
+
return path.as_posix()
|
|
23
61
|
|
|
24
62
|
|
|
25
63
|
class OverlaySurface(BaseModel):
|
|
@@ -66,17 +104,31 @@ class PluginSkillOverride(BaseModel):
|
|
|
66
104
|
|
|
67
105
|
model_config = ConfigDict(extra="forbid")
|
|
68
106
|
|
|
69
|
-
mode: Literal["replace", "disable", "add"]
|
|
70
|
-
path:
|
|
107
|
+
mode: Literal["replace", "patch", "disable", "add"]
|
|
108
|
+
path: PluginOverrideRelativePath | None = None
|
|
109
|
+
base_path: PluginOverrideRelativePath | None = None
|
|
71
110
|
upstream_digest: Sha256Digest | None = None
|
|
72
111
|
on_upstream_change: Literal["warn", "error", "ignore"] | None = None
|
|
73
112
|
|
|
113
|
+
@field_validator("path", "base_path")
|
|
114
|
+
@classmethod
|
|
115
|
+
def validate_override_paths(cls, value: str | None) -> str | None:
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
return _normalize_plugin_override_path(value)
|
|
119
|
+
|
|
74
120
|
@model_validator(mode="after")
|
|
75
121
|
def validate_mode_fields(self) -> PluginSkillOverride:
|
|
76
|
-
if self.mode in {"replace", "add"} and not self.path:
|
|
77
|
-
raise ValueError("path is required when mode is replace or add")
|
|
78
|
-
if self.mode
|
|
79
|
-
raise ValueError(
|
|
122
|
+
if self.mode in {"replace", "patch", "add"} and not self.path:
|
|
123
|
+
raise ValueError("path is required when mode is replace, patch, or add")
|
|
124
|
+
if self.mode in {"replace", "patch"} and self.upstream_digest is None:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"upstream_digest is required when mode is replace or patch"
|
|
127
|
+
)
|
|
128
|
+
if self.mode == "patch" and not self.base_path:
|
|
129
|
+
raise ValueError("base_path is required when mode is patch")
|
|
130
|
+
if self.mode != "patch" and self.base_path is not None:
|
|
131
|
+
raise ValueError("base_path is only valid when mode is patch")
|
|
80
132
|
return self
|
|
81
133
|
|
|
82
134
|
|
|
@@ -86,9 +138,16 @@ class PluginMcpServerOverride(BaseModel):
|
|
|
86
138
|
model_config = ConfigDict(extra="forbid")
|
|
87
139
|
|
|
88
140
|
mode: Literal["patch", "disable", "add"]
|
|
89
|
-
patch_path:
|
|
141
|
+
patch_path: PluginOverrideRelativePath | None = None
|
|
90
142
|
requires_trust_ack: bool = False
|
|
91
143
|
|
|
144
|
+
@field_validator("patch_path")
|
|
145
|
+
@classmethod
|
|
146
|
+
def validate_patch_path(cls, value: str | None) -> str | None:
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
return _normalize_plugin_override_path(value)
|
|
150
|
+
|
|
92
151
|
@model_validator(mode="after")
|
|
93
152
|
def validate_mode_fields(self) -> PluginMcpServerOverride:
|
|
94
153
|
if self.mode in {"patch", "add"} and not self.patch_path:
|
|
@@ -182,6 +241,16 @@ class PluginMcpServerPatch(BaseModel):
|
|
|
182
241
|
ops: list[PluginMcpPatchOperation] = Field(min_length=1)
|
|
183
242
|
|
|
184
243
|
|
|
244
|
+
class PluginAcceptUpstreamProvenance(BaseModel):
|
|
245
|
+
"""Recorded ``overrides accept-upstream`` event for one component."""
|
|
246
|
+
|
|
247
|
+
model_config = ConfigDict(extra="forbid")
|
|
248
|
+
|
|
249
|
+
previous_upstream_digest: Sha256Digest
|
|
250
|
+
new_upstream_digest: Sha256Digest
|
|
251
|
+
accepted_at: str = Field(min_length=1)
|
|
252
|
+
|
|
253
|
+
|
|
185
254
|
class PluginOverrideLockEntry(BaseModel):
|
|
186
255
|
"""Tracked provenance for one consumer-owned plugin override."""
|
|
187
256
|
|
|
@@ -191,8 +260,10 @@ class PluginOverrideLockEntry(BaseModel):
|
|
|
191
260
|
name: PluginComponentName
|
|
192
261
|
mode: Literal["replace", "disable", "add", "patch"]
|
|
193
262
|
local_path: str | None = None
|
|
263
|
+
base_path: str | None = None
|
|
194
264
|
patch_path: str | None = None
|
|
195
265
|
upstream_digest: Sha256Digest | None = None
|
|
266
|
+
last_accept_upstream: PluginAcceptUpstreamProvenance | None = None
|
|
196
267
|
|
|
197
268
|
|
|
198
269
|
class PluginOverrideLock(BaseModel):
|
|
@@ -213,9 +284,9 @@ class PluginEffectiveLockEntry(BaseModel):
|
|
|
213
284
|
|
|
214
285
|
component_kind: Literal["skill", "command", "mcp_server"]
|
|
215
286
|
name: PluginComponentName
|
|
216
|
-
mode: Literal["replace", "disable", "add", "patch"]
|
|
287
|
+
mode: Literal["replace", "disable", "add", "patch", "passthrough"]
|
|
217
288
|
effective_digest: Sha256Digest
|
|
218
|
-
status: Literal["stale"] | None = None
|
|
289
|
+
status: Literal["stale", "merge_conflict"] | None = None
|
|
219
290
|
override_path: str | None = None
|
|
220
291
|
recorded_upstream_digest: Sha256Digest | None = None
|
|
221
292
|
current_base_digest: Sha256Digest | None = None
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workstate-protocol
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/darce/workstate
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from workstate_protocol.bootstrap import (
|
|
10
|
+
PluginEffectiveLock,
|
|
11
|
+
PluginMcpServerPatch,
|
|
12
|
+
PluginOverrideLock,
|
|
13
|
+
PluginOverrideManifest,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_override_manifest_accepts_skill_and_mcp_components() -> None:
|
|
18
|
+
manifest = PluginOverrideManifest.model_validate(
|
|
19
|
+
{
|
|
20
|
+
"schema_version": 1,
|
|
21
|
+
"plugin": "workstate-system",
|
|
22
|
+
"components": {
|
|
23
|
+
"skills": {
|
|
24
|
+
"branch-review": {
|
|
25
|
+
"mode": "replace",
|
|
26
|
+
"path": "skills/branch-review/SKILL.md",
|
|
27
|
+
"upstream_digest": "sha256:" + "a" * 64,
|
|
28
|
+
"on_upstream_change": "warn",
|
|
29
|
+
},
|
|
30
|
+
"review-parallel": {
|
|
31
|
+
"mode": "disable",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"mcp_servers": {
|
|
35
|
+
"workstate-handoff-mcp": {
|
|
36
|
+
"mode": "patch",
|
|
37
|
+
"patch_path": "tools/mcp_servers.patch.yaml",
|
|
38
|
+
"requires_trust_ack": True,
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert manifest.plugin == "workstate-system"
|
|
46
|
+
assert manifest.components.skills["branch-review"].mode == "replace"
|
|
47
|
+
assert manifest.components.mcp_servers["workstate-handoff-mcp"].mode == "patch"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_mcp_server_patch_accepts_typed_operations() -> None:
|
|
51
|
+
patch = PluginMcpServerPatch.model_validate(
|
|
52
|
+
{
|
|
53
|
+
"schema_version": 1,
|
|
54
|
+
"target_server": "workstate-handoff-mcp",
|
|
55
|
+
"ops": [
|
|
56
|
+
{"op": "replace_command", "value": "uvx"},
|
|
57
|
+
{
|
|
58
|
+
"op": "replace_args",
|
|
59
|
+
"value": ["mcp-workstate-handoff@0.11.4", "--profile", "consumer"],
|
|
60
|
+
},
|
|
61
|
+
{"op": "upsert_env", "name": "HANDOFF_PROFILE", "value": "consumer"},
|
|
62
|
+
{"op": "remove_env", "name": "LEGACY_FLAG"},
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert [entry.op for entry in patch.ops] == [
|
|
68
|
+
"replace_command",
|
|
69
|
+
"replace_args",
|
|
70
|
+
"upsert_env",
|
|
71
|
+
"remove_env",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_mcp_server_patch_rejects_wildcard_and_unsupported_file_replacement() -> None:
|
|
76
|
+
with pytest.raises(ValidationError):
|
|
77
|
+
PluginMcpServerPatch.model_validate(
|
|
78
|
+
{
|
|
79
|
+
"schema_version": 1,
|
|
80
|
+
"target_server": "*",
|
|
81
|
+
"ops": [
|
|
82
|
+
{
|
|
83
|
+
"op": "replace_file",
|
|
84
|
+
"path": "../../../tmp/override.sh",
|
|
85
|
+
"value": "hacked",
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_lock_models_roundtrip() -> None:
|
|
93
|
+
override_lock = PluginOverrideLock.model_validate(
|
|
94
|
+
{
|
|
95
|
+
"schema_version": 1,
|
|
96
|
+
"plugin": "workstate-system",
|
|
97
|
+
"base_remote_sha": "b" * 40,
|
|
98
|
+
"components": [
|
|
99
|
+
{
|
|
100
|
+
"component_kind": "skill",
|
|
101
|
+
"name": "branch-review",
|
|
102
|
+
"mode": "replace",
|
|
103
|
+
"local_path": "skills/branch-review/SKILL.md",
|
|
104
|
+
"upstream_digest": "sha256:" + "c" * 64,
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
effective_lock = PluginEffectiveLock.model_validate(
|
|
110
|
+
{
|
|
111
|
+
"schema_version": 1,
|
|
112
|
+
"plugin": "workstate-system",
|
|
113
|
+
"base_remote_sha": "d" * 40,
|
|
114
|
+
"effective_root": ".workstate/generated/plugins/workstate-system/effective/claude",
|
|
115
|
+
"components": [
|
|
116
|
+
{
|
|
117
|
+
"component_kind": "skill",
|
|
118
|
+
"name": "branch-review",
|
|
119
|
+
"mode": "replace",
|
|
120
|
+
"effective_digest": "sha256:" + "e" * 64,
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert (
|
|
127
|
+
PluginOverrideLock.model_validate_json(override_lock.model_dump_json())
|
|
128
|
+
== override_lock
|
|
129
|
+
)
|
|
130
|
+
assert (
|
|
131
|
+
PluginEffectiveLock.model_validate_json(effective_lock.model_dump_json())
|
|
132
|
+
== effective_lock
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_skill_patch_mode_roundtrip() -> None:
|
|
137
|
+
manifest = PluginOverrideManifest.model_validate(
|
|
138
|
+
{
|
|
139
|
+
"schema_version": 1,
|
|
140
|
+
"plugin": "workstate-system",
|
|
141
|
+
"components": {
|
|
142
|
+
"skills": {
|
|
143
|
+
"branch-review": {
|
|
144
|
+
"mode": "patch",
|
|
145
|
+
"path": "skills/branch-review/SKILL.md",
|
|
146
|
+
"base_path": "skills/branch-review/SKILL.base.md",
|
|
147
|
+
"upstream_digest": "sha256:" + "a" * 64,
|
|
148
|
+
"on_upstream_change": "warn",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
override = manifest.components.skills["branch-review"]
|
|
156
|
+
assert override.mode == "patch"
|
|
157
|
+
assert override.base_path == "skills/branch-review/SKILL.base.md"
|
|
158
|
+
assert (
|
|
159
|
+
PluginOverrideManifest.model_validate_json(manifest.model_dump_json())
|
|
160
|
+
== manifest
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_skill_patch_mode_requires_path_base_path_and_digest() -> None:
|
|
165
|
+
base = {
|
|
166
|
+
"mode": "patch",
|
|
167
|
+
"path": "skills/branch-review/SKILL.md",
|
|
168
|
+
"base_path": "skills/branch-review/SKILL.base.md",
|
|
169
|
+
"upstream_digest": "sha256:" + "a" * 64,
|
|
170
|
+
}
|
|
171
|
+
for missing in ("path", "base_path", "upstream_digest"):
|
|
172
|
+
payload = {key: value for key, value in base.items() if key != missing}
|
|
173
|
+
with pytest.raises(ValidationError):
|
|
174
|
+
PluginOverrideManifest.model_validate(
|
|
175
|
+
{
|
|
176
|
+
"schema_version": 1,
|
|
177
|
+
"plugin": "workstate-system",
|
|
178
|
+
"components": {"skills": {"branch-review": payload}},
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_skill_non_patch_modes_reject_base_path() -> None:
|
|
184
|
+
with pytest.raises(ValidationError):
|
|
185
|
+
PluginOverrideManifest.model_validate(
|
|
186
|
+
{
|
|
187
|
+
"schema_version": 1,
|
|
188
|
+
"plugin": "workstate-system",
|
|
189
|
+
"components": {
|
|
190
|
+
"skills": {
|
|
191
|
+
"branch-review": {
|
|
192
|
+
"mode": "replace",
|
|
193
|
+
"path": "skills/branch-review/SKILL.md",
|
|
194
|
+
"base_path": "skills/branch-review/SKILL.base.md",
|
|
195
|
+
"upstream_digest": "sha256:" + "a" * 64,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_override_manifest_rejects_paths_outside_override_root() -> None:
|
|
204
|
+
base_skill = {
|
|
205
|
+
"mode": "patch",
|
|
206
|
+
"path": "skills/branch-review/SKILL.md",
|
|
207
|
+
"base_path": "skills/branch-review/SKILL.base.md",
|
|
208
|
+
"upstream_digest": "sha256:" + "a" * 64,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for field, value in (
|
|
212
|
+
("path", "../outside/SKILL.md"),
|
|
213
|
+
("path", "/tmp/SKILL.md"),
|
|
214
|
+
("base_path", "skills/../../outside.base.md"),
|
|
215
|
+
):
|
|
216
|
+
payload = {**base_skill, field: value}
|
|
217
|
+
with pytest.raises(ValidationError):
|
|
218
|
+
PluginOverrideManifest.model_validate(
|
|
219
|
+
{
|
|
220
|
+
"schema_version": 1,
|
|
221
|
+
"plugin": "workstate-system",
|
|
222
|
+
"components": {"skills": {"branch-review": payload}},
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with pytest.raises(ValidationError):
|
|
227
|
+
PluginOverrideManifest.model_validate(
|
|
228
|
+
{
|
|
229
|
+
"schema_version": 1,
|
|
230
|
+
"plugin": "workstate-system",
|
|
231
|
+
"components": {
|
|
232
|
+
"mcp_servers": {
|
|
233
|
+
"workstate-handoff-mcp": {
|
|
234
|
+
"mode": "patch",
|
|
235
|
+
"patch_path": "../server.patch.yaml",
|
|
236
|
+
"requires_trust_ack": True,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_effective_lock_supports_merge_conflict_and_passthrough() -> None:
|
|
245
|
+
effective_lock = PluginEffectiveLock.model_validate(
|
|
246
|
+
{
|
|
247
|
+
"schema_version": 1,
|
|
248
|
+
"plugin": "workstate-system",
|
|
249
|
+
"base_remote_sha": "d" * 40,
|
|
250
|
+
"effective_root": ".workstate/generated/plugins/workstate-system/effective/claude",
|
|
251
|
+
"components": [
|
|
252
|
+
{
|
|
253
|
+
"component_kind": "skill",
|
|
254
|
+
"name": "branch-review",
|
|
255
|
+
"mode": "patch",
|
|
256
|
+
"effective_digest": "sha256:" + "e" * 64,
|
|
257
|
+
"status": "merge_conflict",
|
|
258
|
+
"override_path": "skills/branch-review/SKILL.md",
|
|
259
|
+
"recorded_upstream_digest": "sha256:" + "f" * 64,
|
|
260
|
+
"current_base_digest": "sha256:" + "0" * 64,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"component_kind": "skill",
|
|
264
|
+
"name": "scope",
|
|
265
|
+
"mode": "passthrough",
|
|
266
|
+
"effective_digest": "sha256:" + "1" * 64,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
statuses = [entry.status for entry in effective_lock.components]
|
|
273
|
+
modes = [entry.mode for entry in effective_lock.components]
|
|
274
|
+
assert statuses == ["merge_conflict", None]
|
|
275
|
+
assert modes == ["patch", "passthrough"]
|
|
276
|
+
assert (
|
|
277
|
+
PluginEffectiveLock.model_validate_json(effective_lock.model_dump_json())
|
|
278
|
+
== effective_lock
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_override_lock_accept_upstream_provenance_roundtrip() -> None:
|
|
283
|
+
override_lock = PluginOverrideLock.model_validate(
|
|
284
|
+
{
|
|
285
|
+
"schema_version": 1,
|
|
286
|
+
"plugin": "workstate-system",
|
|
287
|
+
"base_remote_sha": "b" * 40,
|
|
288
|
+
"components": [
|
|
289
|
+
{
|
|
290
|
+
"component_kind": "skill",
|
|
291
|
+
"name": "branch-review",
|
|
292
|
+
"mode": "patch",
|
|
293
|
+
"local_path": "skills/branch-review/SKILL.md",
|
|
294
|
+
"base_path": "skills/branch-review/SKILL.base.md",
|
|
295
|
+
"upstream_digest": "sha256:" + "c" * 64,
|
|
296
|
+
"last_accept_upstream": {
|
|
297
|
+
"previous_upstream_digest": "sha256:" + "9" * 64,
|
|
298
|
+
"new_upstream_digest": "sha256:" + "c" * 64,
|
|
299
|
+
"accepted_at": "2026-06-04T06:00:00Z",
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
entry = override_lock.components[0]
|
|
307
|
+
assert entry.last_accept_upstream is not None
|
|
308
|
+
assert entry.last_accept_upstream.new_upstream_digest == "sha256:" + "c" * 64
|
|
309
|
+
assert (
|
|
310
|
+
PluginOverrideLock.model_validate_json(override_lock.model_dump_json())
|
|
311
|
+
== override_lock
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_generated_schema_artifacts_include_plugin_override_models() -> None:
|
|
316
|
+
artifact_dir = Path(__file__).resolve().parent.parent / "schemas"
|
|
317
|
+
expected = {
|
|
318
|
+
"plugin-override-manifest.json": PluginOverrideManifest,
|
|
319
|
+
"plugin-override-lock.json": PluginOverrideLock,
|
|
320
|
+
"plugin-effective-lock.json": PluginEffectiveLock,
|
|
321
|
+
"plugin-mcp-server-patch.json": PluginMcpServerPatch,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for file_name, model in expected.items():
|
|
325
|
+
schema = model.model_json_schema()
|
|
326
|
+
assert "schema_version" in schema["properties"]
|
|
327
|
+
|
|
328
|
+
on_disk = artifact_dir / file_name
|
|
329
|
+
persisted = json.loads(on_disk.read_text())
|
|
330
|
+
assert "schema_version" in persisted["properties"]
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
from pydantic import ValidationError
|
|
8
|
-
|
|
9
|
-
from workstate_protocol.bootstrap import (
|
|
10
|
-
PluginEffectiveLock,
|
|
11
|
-
PluginMcpServerPatch,
|
|
12
|
-
PluginOverrideLock,
|
|
13
|
-
PluginOverrideManifest,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_override_manifest_accepts_skill_and_mcp_components() -> None:
|
|
18
|
-
manifest = PluginOverrideManifest.model_validate(
|
|
19
|
-
{
|
|
20
|
-
"schema_version": 1,
|
|
21
|
-
"plugin": "workstate-system",
|
|
22
|
-
"components": {
|
|
23
|
-
"skills": {
|
|
24
|
-
"branch-review": {
|
|
25
|
-
"mode": "replace",
|
|
26
|
-
"path": "skills/branch-review/SKILL.md",
|
|
27
|
-
"upstream_digest": "sha256:" + "a" * 64,
|
|
28
|
-
"on_upstream_change": "warn",
|
|
29
|
-
},
|
|
30
|
-
"review-parallel": {
|
|
31
|
-
"mode": "disable",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
"mcp_servers": {
|
|
35
|
-
"workstate-handoff-mcp": {
|
|
36
|
-
"mode": "patch",
|
|
37
|
-
"patch_path": "tools/mcp_servers.patch.yaml",
|
|
38
|
-
"requires_trust_ack": True,
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
assert manifest.plugin == "workstate-system"
|
|
46
|
-
assert manifest.components.skills["branch-review"].mode == "replace"
|
|
47
|
-
assert manifest.components.mcp_servers["workstate-handoff-mcp"].mode == "patch"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_mcp_server_patch_accepts_typed_operations() -> None:
|
|
51
|
-
patch = PluginMcpServerPatch.model_validate(
|
|
52
|
-
{
|
|
53
|
-
"schema_version": 1,
|
|
54
|
-
"target_server": "workstate-handoff-mcp",
|
|
55
|
-
"ops": [
|
|
56
|
-
{"op": "replace_command", "value": "uvx"},
|
|
57
|
-
{
|
|
58
|
-
"op": "replace_args",
|
|
59
|
-
"value": ["mcp-workstate-handoff@0.11.4", "--profile", "consumer"],
|
|
60
|
-
},
|
|
61
|
-
{"op": "upsert_env", "name": "HANDOFF_PROFILE", "value": "consumer"},
|
|
62
|
-
{"op": "remove_env", "name": "LEGACY_FLAG"},
|
|
63
|
-
],
|
|
64
|
-
}
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
assert [entry.op for entry in patch.ops] == [
|
|
68
|
-
"replace_command",
|
|
69
|
-
"replace_args",
|
|
70
|
-
"upsert_env",
|
|
71
|
-
"remove_env",
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_mcp_server_patch_rejects_wildcard_and_unsupported_file_replacement() -> None:
|
|
76
|
-
with pytest.raises(ValidationError):
|
|
77
|
-
PluginMcpServerPatch.model_validate(
|
|
78
|
-
{
|
|
79
|
-
"schema_version": 1,
|
|
80
|
-
"target_server": "*",
|
|
81
|
-
"ops": [
|
|
82
|
-
{
|
|
83
|
-
"op": "replace_file",
|
|
84
|
-
"path": "../../../tmp/override.sh",
|
|
85
|
-
"value": "hacked",
|
|
86
|
-
}
|
|
87
|
-
],
|
|
88
|
-
}
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_lock_models_roundtrip() -> None:
|
|
93
|
-
override_lock = PluginOverrideLock.model_validate(
|
|
94
|
-
{
|
|
95
|
-
"schema_version": 1,
|
|
96
|
-
"plugin": "workstate-system",
|
|
97
|
-
"base_remote_sha": "b" * 40,
|
|
98
|
-
"components": [
|
|
99
|
-
{
|
|
100
|
-
"component_kind": "skill",
|
|
101
|
-
"name": "branch-review",
|
|
102
|
-
"mode": "replace",
|
|
103
|
-
"local_path": "skills/branch-review/SKILL.md",
|
|
104
|
-
"upstream_digest": "sha256:" + "c" * 64,
|
|
105
|
-
}
|
|
106
|
-
],
|
|
107
|
-
}
|
|
108
|
-
)
|
|
109
|
-
effective_lock = PluginEffectiveLock.model_validate(
|
|
110
|
-
{
|
|
111
|
-
"schema_version": 1,
|
|
112
|
-
"plugin": "workstate-system",
|
|
113
|
-
"base_remote_sha": "d" * 40,
|
|
114
|
-
"effective_root": ".workstate/generated/plugins/workstate-system/effective/claude",
|
|
115
|
-
"components": [
|
|
116
|
-
{
|
|
117
|
-
"component_kind": "skill",
|
|
118
|
-
"name": "branch-review",
|
|
119
|
-
"mode": "replace",
|
|
120
|
-
"effective_digest": "sha256:" + "e" * 64,
|
|
121
|
-
}
|
|
122
|
-
],
|
|
123
|
-
}
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
assert PluginOverrideLock.model_validate_json(override_lock.model_dump_json()) == override_lock
|
|
127
|
-
assert PluginEffectiveLock.model_validate_json(effective_lock.model_dump_json()) == effective_lock
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_generated_schema_artifacts_include_plugin_override_models() -> None:
|
|
131
|
-
artifact_dir = Path(__file__).resolve().parent.parent / "schemas"
|
|
132
|
-
expected = {
|
|
133
|
-
"plugin-override-manifest.json": PluginOverrideManifest,
|
|
134
|
-
"plugin-override-lock.json": PluginOverrideLock,
|
|
135
|
-
"plugin-effective-lock.json": PluginEffectiveLock,
|
|
136
|
-
"plugin-mcp-server-patch.json": PluginMcpServerPatch,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for file_name, model in expected.items():
|
|
140
|
-
schema = model.model_json_schema()
|
|
141
|
-
assert "schema_version" in schema["properties"]
|
|
142
|
-
|
|
143
|
-
on_disk = artifact_dir / file_name
|
|
144
|
-
persisted = json.loads(on_disk.read_text())
|
|
145
|
-
assert "schema_version" in persisted["properties"]
|
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol/branch_naming.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/requires.txt
RENAMED
|
File without changes
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/src/workstate_protocol.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
|
{workstate_protocol-0.1.7 → workstate_protocol-0.2.1}/tests/test_skill_manifest_real_skills.py
RENAMED
|
File without changes
|