vis-agent 0.2.35.dev7414__tar.gz → 0.2.37__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.
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/PKG-INFO +1 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/gateway.json +1 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/toggle.json +1 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/pyproject.toml +1 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/_contracts.py +51 -5
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/_outside.py +8 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_client.py +2 -2
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/extension.py +19 -6
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/extension_package.py +57 -23
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_contracts.py +6 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_extension_package.py +101 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_goals.py +1 -1
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_scoped_settings.py +26 -0
- vis_agent-0.2.37/tests/test_text_list_arguments.py +89 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/.gitignore +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/LICENSE +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/README.md +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/activity.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/agents.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/automations.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/common.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/config.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/content.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/council.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/diff.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/improve.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/provider.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/contracts/schema/view.json +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/hatch_build.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/__init__.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/rooms.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/conftest.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_activity.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_agent.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_agents.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_api_guides.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_authoring.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_catalog.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_client.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_council.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_declarations.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_engine.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_live_wait.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_local.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_method_typing.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_namespace.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_outside.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_providers.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_registration.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_rooms.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_structured_live.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_testing.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_tools.py +0 -0
- {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vis-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.37
|
|
4
4
|
Summary: The Vis Python SDK — author extensions and control local or remote engines.
|
|
5
5
|
Project-URL: Homepage, https://vis.blockether.com
|
|
6
6
|
Project-URL: Documentation, https://vis.blockether.com
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "vis-agent"
|
|
11
|
-
version = "0.2.
|
|
11
|
+
version = "0.2.37"
|
|
12
12
|
description = "The Vis Python SDK — author extensions and control local or remote engines."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -74,16 +74,62 @@ def _json_value(item):
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def _problem(error) -> str:
|
|
78
|
+
"""One readable line for a schema error: field, rule and limit, never the value."""
|
|
79
|
+
field = ".".join(str(part) for part in error.absolute_path) or "declaration"
|
|
80
|
+
rule, limit = error.validator, error.validator_value
|
|
81
|
+
if rule in ("maxLength", "minLength"):
|
|
82
|
+
bound = "maximum" if rule == "maxLength" else "minimum"
|
|
83
|
+
fix = f" Shorten the {field}." if rule == "maxLength" else ""
|
|
84
|
+
return (
|
|
85
|
+
f"{field} is {len(error.instance)} characters; {bound} is {limit} ({rule})."
|
|
86
|
+
+ fix
|
|
87
|
+
)
|
|
88
|
+
if rule == "required":
|
|
89
|
+
missing = ", ".join(key for key in limit if key not in error.instance)
|
|
90
|
+
return f"{missing} is missing (required)."
|
|
91
|
+
if rule == "enum":
|
|
92
|
+
return f"{field} must be one of: {', '.join(map(str, limit))} (enum)."
|
|
93
|
+
if rule == "pattern":
|
|
94
|
+
if (
|
|
95
|
+
limit
|
|
96
|
+
== definition("toggle", "contribution")["properties"]["description"][
|
|
97
|
+
"pattern"
|
|
98
|
+
]
|
|
99
|
+
):
|
|
100
|
+
return f"{field} must be one line without line breaks (pattern)."
|
|
101
|
+
return f"{field} does not match the pattern {limit} (pattern)."
|
|
102
|
+
return f"{field} breaks the {rule} rule."
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def problems(name: str, definition: str, value: Any) -> list[str]:
|
|
106
|
+
"""Readable rule failures of `value`, one line for each. Empty when it is valid.
|
|
107
|
+
|
|
108
|
+
Each line names the field, the rule and its limit. It never echoes the value.
|
|
109
|
+
"""
|
|
110
|
+
if not _json_value(value):
|
|
111
|
+
return ["the value is not JSON data."]
|
|
112
|
+
errors = _validator(name, definition).iter_errors(value)
|
|
113
|
+
return [
|
|
114
|
+
_problem(error)
|
|
115
|
+
for error in sorted(errors, key=lambda e: list(map(str, e.absolute_path)))
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
77
119
|
def validate(name: str, definition: str, value: Any) -> Any:
|
|
78
120
|
"""Validate portable JSON data, returning it or raising a payload-free ValueError.
|
|
79
121
|
|
|
80
|
-
|
|
122
|
+
The error names each failed field, rule and limit, never the value. Schemas
|
|
123
|
+
resolve only from the installed package. Runtime lifecycle, IO and
|
|
81
124
|
authorization belong to the engine, not JSON Schema.
|
|
82
125
|
"""
|
|
83
126
|
try:
|
|
84
|
-
|
|
127
|
+
found = problems(name, definition, value)
|
|
85
128
|
except (RecursionError, TypeError, ValueError):
|
|
86
|
-
|
|
87
|
-
if
|
|
88
|
-
|
|
129
|
+
found = [""]
|
|
130
|
+
if found:
|
|
131
|
+
detail = " ".join(problem for problem in found if problem)
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"invalid {name}.{definition} contract" + (f": {detail}" if detail else "")
|
|
134
|
+
)
|
|
89
135
|
return value
|
|
@@ -91,13 +91,20 @@ def workspace_root():
|
|
|
91
91
|
|
|
92
92
|
def setting_declaration(spec):
|
|
93
93
|
"""Use the package's own JSON Schemas for outside-host declarations."""
|
|
94
|
-
from ._contracts import definition, validate
|
|
94
|
+
from ._contracts import definition, problems, validate
|
|
95
95
|
|
|
96
96
|
spec = dict(spec)
|
|
97
97
|
spec.setdefault(
|
|
98
98
|
"scopes",
|
|
99
99
|
definition("toggle", "contribution")["properties"]["scopes"]["default"],
|
|
100
100
|
)
|
|
101
|
+
found = problems("toggle", "contribution", spec)
|
|
102
|
+
if found:
|
|
103
|
+
name = spec.get("id")
|
|
104
|
+
label = (
|
|
105
|
+
f"Invalid setting {name}" if isinstance(name, str) else "Invalid setting"
|
|
106
|
+
)
|
|
107
|
+
raise ValueError(f"{label}: {' '.join(found)}")
|
|
101
108
|
return validate("toggle", "contribution", spec)
|
|
102
109
|
|
|
103
110
|
|
|
@@ -3251,8 +3251,8 @@ class Session:
|
|
|
3251
3251
|
replies. The last iteration can run its tools, and then the limit blocks
|
|
3252
3252
|
the next model request. Token usage is recorded only as a statistic.
|
|
3253
3253
|
"""
|
|
3254
|
-
if not isinstance(objective, str) or not 1 <= len(objective.strip()) <=
|
|
3255
|
-
raise ValueError("objective must contain 1–
|
|
3254
|
+
if not isinstance(objective, str) or not 1 <= len(objective.strip()) <= 44000:
|
|
3255
|
+
raise ValueError("objective must contain 1–44000 characters")
|
|
3256
3256
|
if iteration_budget is not None and (
|
|
3257
3257
|
type(iteration_budget) is not int
|
|
3258
3258
|
or not 1 <= iteration_budget <= 9007199254740991
|
|
@@ -1388,10 +1388,11 @@ def _call_arguments(fn, args, kwargs):
|
|
|
1388
1388
|
An argument crosses the host boundary as JSON, so a record arrives as a dict of its
|
|
1389
1389
|
public fields. A parameter annotated with a dataclass gets that dataclass back,
|
|
1390
1390
|
built through its constructor. This also works inside a union, list, tuple, set or
|
|
1391
|
-
dict annotation (Blockether/vis#289).
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
that
|
|
1391
|
+
dict annotation (Blockether/vis#289). One bare string for a sequence of strings
|
|
1392
|
+
becomes one item, not one item for each character (Blockether/vis#324).
|
|
1393
|
+
Annotations resolve statically, as for tool contracts. A value that does not fit
|
|
1394
|
+
its annotation stays as it arrived. So does every argument of a call that the
|
|
1395
|
+
signature cannot bind: the call itself reports that mistake.
|
|
1395
1396
|
"""
|
|
1396
1397
|
try:
|
|
1397
1398
|
# A wrapper around a bound method must not unwrap past the binding.
|
|
@@ -1561,9 +1562,10 @@ def _holds_record(shape):
|
|
|
1561
1562
|
def _rebuilt_value(shape, value):
|
|
1562
1563
|
"""Return `value` with the records that `shape` expects rebuilt.
|
|
1563
1564
|
|
|
1565
|
+
One bare string for a sequence of strings becomes that sequence with one item.
|
|
1564
1566
|
A value that does not fit stays as is.
|
|
1565
1567
|
"""
|
|
1566
|
-
if not _holds_record(shape):
|
|
1568
|
+
if not (_holds_record(shape) or isinstance(value, str)):
|
|
1567
1569
|
return value
|
|
1568
1570
|
result = _fitted_argument(shape, value)
|
|
1569
1571
|
return value if result is _UNFIT else result
|
|
@@ -1588,7 +1590,11 @@ def _fitted_argument(shape, value):
|
|
|
1588
1590
|
if kind == "record":
|
|
1589
1591
|
return _rebuilt_record(shape[1], value, shape[2])
|
|
1590
1592
|
if kind == "union":
|
|
1591
|
-
|
|
1593
|
+
members = shape[1]
|
|
1594
|
+
if isinstance(value, str):
|
|
1595
|
+
# A member that takes the string as it is wins over one that wraps it.
|
|
1596
|
+
members = sorted(members, key=lambda member: member[0] == "sequence")
|
|
1597
|
+
for member in members:
|
|
1592
1598
|
result = _fitted_argument(member, value)
|
|
1593
1599
|
if result is not _UNFIT:
|
|
1594
1600
|
return result
|
|
@@ -1599,6 +1605,13 @@ def _fitted_argument(shape, value):
|
|
|
1599
1605
|
if not _holds_record(shape):
|
|
1600
1606
|
return value
|
|
1601
1607
|
return {key: _rebuilt_value(shape[1], item) for key, item in value.items()}
|
|
1608
|
+
if kind == "sequence" and isinstance(value, str):
|
|
1609
|
+
# A string is a sequence of its characters; the caller named one item. An
|
|
1610
|
+
# item of any type also takes the whole string, so it stays as it is.
|
|
1611
|
+
if shape[2] in (("any",), ("plain", object)):
|
|
1612
|
+
return _UNFIT
|
|
1613
|
+
item = _fitted_argument(shape[2], value)
|
|
1614
|
+
return _UNFIT if item is _UNFIT else shape[1]((item,))
|
|
1602
1615
|
if not isinstance(value, (list, tuple)):
|
|
1603
1616
|
return _UNFIT
|
|
1604
1617
|
if kind == "fixed":
|
|
@@ -954,6 +954,59 @@ def install(
|
|
|
954
954
|
return _admit(source, directory, subdirectory, revision, vis_version, release)
|
|
955
955
|
|
|
956
956
|
|
|
957
|
+
def _replace_managed(directory, active, current, release, vis_version):
|
|
958
|
+
"""Activate a managed version and keep its sync ownership in the same operation."""
|
|
959
|
+
name = current["name"]
|
|
960
|
+
lock = directory / ".sync-lock"
|
|
961
|
+
fd = _acquire(lock)
|
|
962
|
+
try:
|
|
963
|
+
records = _sync_records(directory)
|
|
964
|
+
owned = records.get(name)
|
|
965
|
+
destination = _destination(directory, name)
|
|
966
|
+
if owned is not None and not _sync_owned(destination, owned):
|
|
967
|
+
raise ValueError(
|
|
968
|
+
"Existing extension was changed externally; no files replaced"
|
|
969
|
+
)
|
|
970
|
+
spec = _sync_spec(owned["spec"]) if owned is not None else None
|
|
971
|
+
previous_target = str(active)
|
|
972
|
+
snapshot = destination.parent / release["version"]
|
|
973
|
+
receipt_path = snapshot / "receipt.json"
|
|
974
|
+
previous_receipt = receipt_path.read_bytes() if receipt_path.is_file() else None
|
|
975
|
+
result = _admit(
|
|
976
|
+
current["repository_url"],
|
|
977
|
+
directory,
|
|
978
|
+
current["subdirectory"],
|
|
979
|
+
release["revision"],
|
|
980
|
+
vis_version,
|
|
981
|
+
release,
|
|
982
|
+
name,
|
|
983
|
+
active.name,
|
|
984
|
+
)
|
|
985
|
+
if spec is None:
|
|
986
|
+
return result
|
|
987
|
+
# Configuration pins remain authoritative on the next reconciliation.
|
|
988
|
+
if spec["revision"] is not None:
|
|
989
|
+
spec["revision"] = result["revision"]
|
|
990
|
+
elif spec["version"] not in (None, LATEST):
|
|
991
|
+
spec["version"] = result["version"]
|
|
992
|
+
try:
|
|
993
|
+
_save_sync_records(
|
|
994
|
+
directory,
|
|
995
|
+
{
|
|
996
|
+
**records,
|
|
997
|
+
name: {"spec": spec, "target": result["path"], "result": result},
|
|
998
|
+
},
|
|
999
|
+
)
|
|
1000
|
+
except BaseException:
|
|
1001
|
+
_restore_saved_link(directory, name, result["path"], previous_target)
|
|
1002
|
+
if previous_receipt is not None:
|
|
1003
|
+
receipt_path.write_bytes(previous_receipt)
|
|
1004
|
+
raise
|
|
1005
|
+
return result
|
|
1006
|
+
finally:
|
|
1007
|
+
_release(lock, fd)
|
|
1008
|
+
|
|
1009
|
+
|
|
957
1010
|
def update(
|
|
958
1011
|
source, directory, trust=False, version=None, vis_version=None, subdirectory=None
|
|
959
1012
|
):
|
|
@@ -988,16 +1041,7 @@ def update(
|
|
|
988
1041
|
}
|
|
989
1042
|
if Version(release["version"]) == Version(current["version"]):
|
|
990
1043
|
raise ValueError("A published version cannot change its approved commit")
|
|
991
|
-
return
|
|
992
|
-
current["repository_url"],
|
|
993
|
-
directory,
|
|
994
|
-
current["subdirectory"],
|
|
995
|
-
release["revision"],
|
|
996
|
-
vis_version,
|
|
997
|
-
release,
|
|
998
|
-
name,
|
|
999
|
-
active.name,
|
|
1000
|
-
)
|
|
1044
|
+
return _replace_managed(directory, active, current, release, vis_version)
|
|
1001
1045
|
|
|
1002
1046
|
|
|
1003
1047
|
def rollback(
|
|
@@ -1016,7 +1060,6 @@ def rollback(
|
|
|
1016
1060
|
folder = None
|
|
1017
1061
|
active, current = _installed_repository(directory, repository, folder)
|
|
1018
1062
|
assert active is not None and current is not None # A required lookup raises.
|
|
1019
|
-
name = current["name"]
|
|
1020
1063
|
if version is not None:
|
|
1021
1064
|
release = _select(
|
|
1022
1065
|
_releases(current["repository_url"], current["subdirectory"]), version
|
|
@@ -1031,24 +1074,15 @@ def rollback(
|
|
|
1031
1074
|
raise ValueError(
|
|
1032
1075
|
"No previous installation; choose an approved older --version"
|
|
1033
1076
|
)
|
|
1034
|
-
release = _receipt(active.parent / _version(previous), name)
|
|
1077
|
+
release = _receipt(active.parent / _version(previous), current["name"])
|
|
1035
1078
|
if (
|
|
1036
1079
|
release.get("repository_url") != current["repository_url"]
|
|
1037
1080
|
or release.get("subdirectory") != current["subdirectory"]
|
|
1038
|
-
or release.get("name") != name
|
|
1081
|
+
or release.get("name") != current["name"]
|
|
1039
1082
|
or not re.fullmatch(r"[0-9a-f]{40}", release.get("revision", ""))
|
|
1040
1083
|
):
|
|
1041
1084
|
raise ValueError("Invalid previous package receipt")
|
|
1042
|
-
return
|
|
1043
|
-
current["repository_url"],
|
|
1044
|
-
directory,
|
|
1045
|
-
current["subdirectory"],
|
|
1046
|
-
release["revision"],
|
|
1047
|
-
vis_version,
|
|
1048
|
-
release,
|
|
1049
|
-
name,
|
|
1050
|
-
active.name,
|
|
1051
|
-
)
|
|
1085
|
+
return _replace_managed(directory, active, current, release, vis_version)
|
|
1052
1086
|
|
|
1053
1087
|
|
|
1054
1088
|
def _sync_spec(spec):
|
|
@@ -178,6 +178,12 @@ def test_payload_free_validation():
|
|
|
178
178
|
with pytest.raises(ValueError) as error:
|
|
179
179
|
_contracts.validate("activity", "declaration", bad)
|
|
180
180
|
assert "do-not-echo" not in str(error.value)
|
|
181
|
+
with pytest.raises(
|
|
182
|
+
ValueError, match=r": label does not match the pattern .* \(pattern\)"
|
|
183
|
+
):
|
|
184
|
+
_contracts.validate(
|
|
185
|
+
"activity", "declaration", {"presenter": "tests", "label": "one\ntwo"}
|
|
186
|
+
)
|
|
181
187
|
with pytest.raises(ValueError):
|
|
182
188
|
_contracts.validate("view", "missing-definition", {})
|
|
183
189
|
|
|
@@ -1075,6 +1075,107 @@ def test_sync_pins_updates_refresh_and_rollback(releases):
|
|
|
1075
1075
|
)
|
|
1076
1076
|
|
|
1077
1077
|
|
|
1078
|
+
def test_explicit_update_and_rollback_keep_sync_ownership(releases):
|
|
1079
|
+
metadata, target, _ = releases
|
|
1080
|
+
newer = metadata.pop(1)
|
|
1081
|
+
configured = {
|
|
1082
|
+
"vis-greeter": {"source": REPOSITORY, "subdirectory": "plugins/greeting"}
|
|
1083
|
+
}
|
|
1084
|
+
package.sync(configured, target, trust=True)
|
|
1085
|
+
metadata.append(newer)
|
|
1086
|
+
package.update(REPOSITORY, target, trust=True)
|
|
1087
|
+
updated = package.sync(configured, target, trust=True)[0]
|
|
1088
|
+
assert (updated["status"], updated.get("version")) == ("cached", "1.1.0")
|
|
1089
|
+
package.rollback(REPOSITORY, target, trust=True)
|
|
1090
|
+
restored = package.sync(configured, target, trust=True)[0]
|
|
1091
|
+
assert (restored["status"], restored.get("version")) == ("cached", "1.0.0")
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
@pytest.mark.parametrize("operation", ["update", "rollback"])
|
|
1095
|
+
def test_managed_activation_restores_ownership_when_sync_receipt_write_fails(
|
|
1096
|
+
releases, monkeypatch, operation
|
|
1097
|
+
):
|
|
1098
|
+
metadata, target, _ = releases
|
|
1099
|
+
newer = metadata.pop(1)
|
|
1100
|
+
configured = {
|
|
1101
|
+
"vis-greeter": {"source": REPOSITORY, "subdirectory": "plugins/greeting"}
|
|
1102
|
+
}
|
|
1103
|
+
package.sync(configured, target, trust=True)
|
|
1104
|
+
metadata.append(newer)
|
|
1105
|
+
if operation == "rollback":
|
|
1106
|
+
package.update(REPOSITORY, target, trust=True)
|
|
1107
|
+
pointer = target / "vis-greeter/current"
|
|
1108
|
+
previous_target = pointer.resolve()
|
|
1109
|
+
previous_records = (target / ".sync.json").read_bytes()
|
|
1110
|
+
receipt_path = target / "vis-greeter/1.0.0/receipt.json"
|
|
1111
|
+
previous_receipt = receipt_path.read_bytes()
|
|
1112
|
+
|
|
1113
|
+
def fail(*_):
|
|
1114
|
+
raise OSError("Fixture receipt write failure")
|
|
1115
|
+
|
|
1116
|
+
monkeypatch.setattr(package, "_save_sync_records", fail)
|
|
1117
|
+
with pytest.raises(OSError, match="Fixture receipt write failure"):
|
|
1118
|
+
getattr(package, operation)(REPOSITORY, target, trust=True)
|
|
1119
|
+
assert pointer.resolve() == previous_target
|
|
1120
|
+
assert (target / ".sync.json").read_bytes() == previous_records
|
|
1121
|
+
assert receipt_path.read_bytes() == previous_receipt
|
|
1122
|
+
assert package._sync_owned(pointer, package._sync_records(target)["vis-greeter"])
|
|
1123
|
+
assert package.sync(configured, target, trust=True)[0]["status"] == "cached"
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
@pytest.mark.parametrize("operation", ["update", "rollback"])
|
|
1127
|
+
def test_managed_activation_refuses_an_externally_changed_sync_pointer(
|
|
1128
|
+
releases, operation
|
|
1129
|
+
):
|
|
1130
|
+
metadata, target, _ = releases
|
|
1131
|
+
newer = metadata.pop(1)
|
|
1132
|
+
configured = {
|
|
1133
|
+
"vis-greeter": {"source": REPOSITORY, "subdirectory": "plugins/greeting"}
|
|
1134
|
+
}
|
|
1135
|
+
package.sync(configured, target, trust=True)
|
|
1136
|
+
metadata.append(newer)
|
|
1137
|
+
package.update(REPOSITORY, target, trust=True)
|
|
1138
|
+
pointer = target / "vis-greeter/current"
|
|
1139
|
+
pointer.unlink()
|
|
1140
|
+
pointer.symlink_to("1.0.0")
|
|
1141
|
+
before = (target / ".sync.json").read_bytes()
|
|
1142
|
+
kwargs = {"version": "1.1.0"} if operation == "update" else {}
|
|
1143
|
+
with pytest.raises(ValueError):
|
|
1144
|
+
getattr(package, operation)(REPOSITORY, target, trust=True, **kwargs)
|
|
1145
|
+
assert pointer.resolve().name == "1.0.0"
|
|
1146
|
+
assert (target / ".sync.json").read_bytes() == before
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
@pytest.mark.parametrize("pin", ["version", "revision"])
|
|
1150
|
+
def test_explicit_update_does_not_override_a_configuration_pin(releases, pin):
|
|
1151
|
+
metadata, target, _ = releases
|
|
1152
|
+
spec = {"source": REPOSITORY, "subdirectory": "plugins/greeting"}
|
|
1153
|
+
spec[pin] = metadata[0][pin]
|
|
1154
|
+
configured = {"vis-greeter": spec}
|
|
1155
|
+
package.sync(configured, target, trust=True)
|
|
1156
|
+
package.update(REPOSITORY, target, trust=True)
|
|
1157
|
+
restored = package.sync(configured, target, trust=True)[0]
|
|
1158
|
+
assert (restored["status"], restored.get("version")) == ("updated", "1.0.0")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def test_update_validates_sync_spec_before_changing_the_active_version(releases):
|
|
1162
|
+
metadata, target, _ = releases
|
|
1163
|
+
newer = metadata.pop(1)
|
|
1164
|
+
configured = {
|
|
1165
|
+
"vis-greeter": {"source": REPOSITORY, "subdirectory": "plugins/greeting"}
|
|
1166
|
+
}
|
|
1167
|
+
package.sync(configured, target, trust=True)
|
|
1168
|
+
metadata.append(newer)
|
|
1169
|
+
records = package._sync_records(target)
|
|
1170
|
+
records["vis-greeter"]["spec"] = {}
|
|
1171
|
+
package._save_sync_records(target, records)
|
|
1172
|
+
before = (target / ".sync.json").read_bytes()
|
|
1173
|
+
with pytest.raises(ValueError, match="source"):
|
|
1174
|
+
package.update(REPOSITORY, target, trust=True)
|
|
1175
|
+
assert (target / "vis-greeter/current").resolve().name == "1.0.0"
|
|
1176
|
+
assert (target / ".sync.json").read_bytes() == before
|
|
1177
|
+
|
|
1178
|
+
|
|
1078
1179
|
def test_sync_latest_tracks_approved_stable_releases(releases):
|
|
1079
1180
|
metadata, target, commands = releases
|
|
1080
1181
|
newer = metadata.pop(1)
|
|
@@ -68,6 +68,32 @@ def test_parent_nests_a_setting():
|
|
|
68
68
|
vis.Setting(id="test_detail", label="Test detail", default=False, parent=" ")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
# Regression for #322 and #323: a 300-character description is valid, and a
|
|
72
|
+
# longer one names the setting, the field, the rule and the limit.
|
|
73
|
+
def test_description_limit_and_actionable_error():
|
|
74
|
+
def mode(description):
|
|
75
|
+
return vis.Setting(
|
|
76
|
+
id="steering_mode",
|
|
77
|
+
label="Steering mode",
|
|
78
|
+
type="enum",
|
|
79
|
+
choices=["vibe", "control"],
|
|
80
|
+
default="vibe",
|
|
81
|
+
description=description,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert mode("x" * 300).description == "x" * 300
|
|
85
|
+
with pytest.raises(ValueError) as error:
|
|
86
|
+
mode("x" * 301)
|
|
87
|
+
assert str(error.value) == (
|
|
88
|
+
"Invalid setting steering_mode: description is 301 characters; "
|
|
89
|
+
"maximum is 300 (maxLength). Shorten the description."
|
|
90
|
+
)
|
|
91
|
+
with pytest.raises(ValueError, match="one line without line breaks"):
|
|
92
|
+
mode("two\nlines")
|
|
93
|
+
with pytest.raises(ValueError, match="minimum is 1"):
|
|
94
|
+
mode("")
|
|
95
|
+
|
|
96
|
+
|
|
71
97
|
def test_application_bridge_rejects_host_only_settings_before_connecting():
|
|
72
98
|
declaration = vis.Extension(
|
|
73
99
|
name="Settings test",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Regression coverage for #324: one bare string for a list of strings is one item."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Literal, Optional
|
|
8
|
+
|
|
9
|
+
import blockether.vis.extension as vis
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Tools:
|
|
13
|
+
"""The parameter shapes of the `py.*` and `clj.*` language tools."""
|
|
14
|
+
|
|
15
|
+
def lint_code(
|
|
16
|
+
self,
|
|
17
|
+
paths: Annotated[Sequence[str], "Files or directories to lint."] = (),
|
|
18
|
+
*,
|
|
19
|
+
cwd: str = "",
|
|
20
|
+
namespaces: Annotated[Sequence[str], "Test namespaces to run."] = (),
|
|
21
|
+
) -> tuple:
|
|
22
|
+
return tuple(paths), tuple(namespaces)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rebuilt(fn, *args, **kwargs):
|
|
26
|
+
return vis._call_arguments(fn, list(args), kwargs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_bare_string_for_a_string_sequence_is_one_item():
|
|
30
|
+
# Regression for Blockether/vis#324: `py.lint_code("/src/app.py")` named one
|
|
31
|
+
# path for each character, and `/` made the lint walk the whole filesystem.
|
|
32
|
+
tools = Tools()
|
|
33
|
+
args, kwargs = rebuilt(tools.lint_code, "/src/app.py", namespaces="my.ns-test")
|
|
34
|
+
assert args == [["/src/app.py"]]
|
|
35
|
+
assert kwargs == {"namespaces": ["my.ns-test"]}
|
|
36
|
+
assert tools.lint_code(*args, **kwargs) == (("/src/app.py",), ("my.ns-test",))
|
|
37
|
+
assert rebuilt(tools.lint_code, paths="a.py")[1] == {"paths": ["a.py"]}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_lists_and_other_parameters_stay_as_they_arrived():
|
|
41
|
+
tools = Tools()
|
|
42
|
+
paths = ["a.py", "b.py"]
|
|
43
|
+
args, kwargs = rebuilt(tools.lint_code, paths, cwd="/src")
|
|
44
|
+
assert args[0] is paths
|
|
45
|
+
assert kwargs == {"cwd": "/src"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_container_and_item_annotations_decide_the_shape():
|
|
49
|
+
def files(
|
|
50
|
+
paths: list[str], tags: set[str] = frozenset(), pair: tuple[str, ...] = ()
|
|
51
|
+
):
|
|
52
|
+
return paths, tags, pair
|
|
53
|
+
|
|
54
|
+
def optional(paths: Optional[list[str]] = None, more: list[str] | None = None): # noqa: UP045
|
|
55
|
+
return paths, more
|
|
56
|
+
|
|
57
|
+
def literal(modes: list[Literal["fast", "full"]] = ()):
|
|
58
|
+
return modes
|
|
59
|
+
|
|
60
|
+
assert rebuilt(files, "a.py", tags="x", pair="y") == (
|
|
61
|
+
[["a.py"]],
|
|
62
|
+
{"tags": {"x"}, "pair": ("y",)},
|
|
63
|
+
)
|
|
64
|
+
assert rebuilt(optional, "a.py", more="b.py") == ([["a.py"]], {"more": ["b.py"]})
|
|
65
|
+
assert rebuilt(literal, "fast") == ([["fast"]], {})
|
|
66
|
+
assert rebuilt(literal, "other") == (["other"], {})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_member_that_takes_the_string_as_it_is_wins():
|
|
70
|
+
def either(paths: list[str] | str = (), path: str | list[str] = ""):
|
|
71
|
+
return paths, path
|
|
72
|
+
|
|
73
|
+
def anything(
|
|
74
|
+
values: list = (), items: Sequence[object] = (), raw: list[bytes] = ()
|
|
75
|
+
):
|
|
76
|
+
return values, items, raw
|
|
77
|
+
|
|
78
|
+
assert rebuilt(either, "a.py", path="b.py") == (["a.py"], {"path": "b.py"})
|
|
79
|
+
assert rebuilt(anything, "ab", items="cd", raw="ef") == (
|
|
80
|
+
["ab"],
|
|
81
|
+
{"items": "cd", "raw": "ef"},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_path_items_take_a_bare_string_unchanged():
|
|
86
|
+
def walk(paths: list[str | Path] = ()):
|
|
87
|
+
return paths
|
|
88
|
+
|
|
89
|
+
assert rebuilt(walk, "src") == ([["src"]], {})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|