vis-agent 0.3.5.dev8266__tar.gz → 0.4.3__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.3.5.dev8266 → vis_agent-0.4.3}/PKG-INFO +1 -1
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/agents.json +0 -4
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/gateway.json +124 -1
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/view.json +8 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/pyproject.toml +1 -1
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_contracts.py +16 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_outside.py +7 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_client.py +1 -1
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/grade.py +19 -30
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/extension.py +49 -8
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/extension_package.py +8 -2
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_engine.py +12 -6
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_guards.py +21 -15
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_live_primitives.py +29 -1
- vis_agent-0.4.3/tests/test_private_file.py +77 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_bootstrap.py +3 -2
- vis_agent-0.3.5.dev8266/release-tag +0 -1
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/.gitignore +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/LICENSE +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/README.md +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-stream.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/activity.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/automations.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/common.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/config.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/content.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/council.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/diff.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/evals.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/provider.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/hatch_build.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/__init__.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_host.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_install.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_models.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_speech.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/__init__.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/_gateway.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/agent.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/bench.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/check.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/compare.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/environment.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/harbor.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/judge-rubric.toml +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/judge.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/labels.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/leaderboard.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/lic.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/longmemeval.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/mrcr.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/repeats.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/report.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/reports.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/rounds.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/stages.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/stats.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/trajectory.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/conftest.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/evals_atif.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_activity.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agents.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_api_guides.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_app_attachments.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_app_door.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_attachments.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_authoring.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_catalog.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_client.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_contracts.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_council.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_declarations.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_agent.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_bench.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_check.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_compare.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_environment.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_gateway.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_grade.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_harbor.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_judge.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_labels.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_lic.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_longmemeval.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_mrcr.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_repeats.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_report.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_rounds.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_stages.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_stats.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_tasks.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_trajectory.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_extension_package.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_goals.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_install.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_keybindings.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_live_wait.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_local.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_method_typing.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_models.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_namespace.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_outside.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_providers.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_read_session.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_registration.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_scoped_settings.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_shortcuts_live.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_speech.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_structured_live.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_testing.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_text_list_arguments.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_tools.py +0 -0
- {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vis-agent
|
|
3
|
-
Version: 0.3
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -638,16 +638,12 @@
|
|
|
638
638
|
"type": "integer",
|
|
639
639
|
"minimum": 1
|
|
640
640
|
},
|
|
641
|
-
"kind": {
|
|
642
|
-
"type": "string"
|
|
643
|
-
},
|
|
644
641
|
"content": {
|
|
645
642
|
"type": "string"
|
|
646
643
|
}
|
|
647
644
|
},
|
|
648
645
|
"required": [
|
|
649
646
|
"entry_id",
|
|
650
|
-
"kind",
|
|
651
647
|
"content"
|
|
652
648
|
],
|
|
653
649
|
"additionalProperties": false
|
|
@@ -967,7 +967,8 @@
|
|
|
967
967
|
"operations": {
|
|
968
968
|
"get": {
|
|
969
969
|
"request": "none",
|
|
970
|
-
"response": "json"
|
|
970
|
+
"response": "json",
|
|
971
|
+
"response_schema": "file_preview"
|
|
971
972
|
}
|
|
972
973
|
},
|
|
973
974
|
"path": "/v1/sessions/:sid/fs/file"
|
|
@@ -3735,6 +3736,128 @@
|
|
|
3735
3736
|
"pattern": "^[0-9a-f]{64}$"
|
|
3736
3737
|
}
|
|
3737
3738
|
}
|
|
3739
|
+
},
|
|
3740
|
+
"preview_image_bytes": {
|
|
3741
|
+
"type": "integer",
|
|
3742
|
+
"minimum": 0,
|
|
3743
|
+
"maximum": 8388608,
|
|
3744
|
+
"description": "Bytes of one picture in a file preview. The picture goes as base64 inside JSON, which is 4/3 larger, and a phone decodes it. A larger picture gets a 413 answer."
|
|
3745
|
+
},
|
|
3746
|
+
"file_preview": {
|
|
3747
|
+
"description": "The answer of GET /v1/sessions/:sid/fs/file. The kind tells what the file holds and which keys come back.",
|
|
3748
|
+
"oneOf": [
|
|
3749
|
+
{
|
|
3750
|
+
"type": "object",
|
|
3751
|
+
"title": "Text",
|
|
3752
|
+
"description": "One window of clipped lines around the line that the client named.",
|
|
3753
|
+
"additionalProperties": false,
|
|
3754
|
+
"required": [
|
|
3755
|
+
"path",
|
|
3756
|
+
"kind",
|
|
3757
|
+
"line",
|
|
3758
|
+
"first_line",
|
|
3759
|
+
"lines",
|
|
3760
|
+
"is_truncated",
|
|
3761
|
+
"size_bytes"
|
|
3762
|
+
],
|
|
3763
|
+
"properties": {
|
|
3764
|
+
"path": {
|
|
3765
|
+
"type": "string",
|
|
3766
|
+
"minLength": 1
|
|
3767
|
+
},
|
|
3768
|
+
"kind": {
|
|
3769
|
+
"const": "text"
|
|
3770
|
+
},
|
|
3771
|
+
"line": {
|
|
3772
|
+
"type": "integer",
|
|
3773
|
+
"minimum": 1,
|
|
3774
|
+
"description": "The line that the client named, or 1."
|
|
3775
|
+
},
|
|
3776
|
+
"first_line": {
|
|
3777
|
+
"type": "integer",
|
|
3778
|
+
"minimum": 1,
|
|
3779
|
+
"description": "The number of the first line in lines."
|
|
3780
|
+
},
|
|
3781
|
+
"lines": {
|
|
3782
|
+
"type": "array",
|
|
3783
|
+
"items": {
|
|
3784
|
+
"type": "string"
|
|
3785
|
+
}
|
|
3786
|
+
},
|
|
3787
|
+
"is_truncated": {
|
|
3788
|
+
"type": "boolean",
|
|
3789
|
+
"description": "True when the byte cap stopped the read before the window ended."
|
|
3790
|
+
},
|
|
3791
|
+
"size_bytes": {
|
|
3792
|
+
"type": "integer",
|
|
3793
|
+
"minimum": 0
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
},
|
|
3797
|
+
{
|
|
3798
|
+
"type": "object",
|
|
3799
|
+
"title": "Image",
|
|
3800
|
+
"description": "A picture that a browser draws, typed by its first bytes, not by its file name.",
|
|
3801
|
+
"additionalProperties": false,
|
|
3802
|
+
"required": [
|
|
3803
|
+
"path",
|
|
3804
|
+
"kind",
|
|
3805
|
+
"media_type",
|
|
3806
|
+
"size_bytes",
|
|
3807
|
+
"data"
|
|
3808
|
+
],
|
|
3809
|
+
"properties": {
|
|
3810
|
+
"path": {
|
|
3811
|
+
"type": "string",
|
|
3812
|
+
"minLength": 1
|
|
3813
|
+
},
|
|
3814
|
+
"kind": {
|
|
3815
|
+
"const": "image"
|
|
3816
|
+
},
|
|
3817
|
+
"media_type": {
|
|
3818
|
+
"enum": [
|
|
3819
|
+
"image/png",
|
|
3820
|
+
"image/jpeg",
|
|
3821
|
+
"image/gif",
|
|
3822
|
+
"image/webp",
|
|
3823
|
+
"image/avif"
|
|
3824
|
+
]
|
|
3825
|
+
},
|
|
3826
|
+
"size_bytes": {
|
|
3827
|
+
"$ref": "#/$defs/preview_image_bytes"
|
|
3828
|
+
},
|
|
3829
|
+
"data": {
|
|
3830
|
+
"type": "string",
|
|
3831
|
+
"pattern": "^[A-Za-z0-9+/]*={0,2}$",
|
|
3832
|
+
"description": "The bytes of the picture as standard base64."
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
"type": "object",
|
|
3838
|
+
"title": "Binary",
|
|
3839
|
+
"description": "Another file with a NUL byte. The gateway does not read it.",
|
|
3840
|
+
"additionalProperties": false,
|
|
3841
|
+
"required": [
|
|
3842
|
+
"path",
|
|
3843
|
+
"kind",
|
|
3844
|
+
"size_bytes"
|
|
3845
|
+
],
|
|
3846
|
+
"properties": {
|
|
3847
|
+
"path": {
|
|
3848
|
+
"type": "string",
|
|
3849
|
+
"minLength": 1
|
|
3850
|
+
},
|
|
3851
|
+
"kind": {
|
|
3852
|
+
"const": "binary"
|
|
3853
|
+
},
|
|
3854
|
+
"size_bytes": {
|
|
3855
|
+
"type": "integer",
|
|
3856
|
+
"minimum": 0
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
]
|
|
3738
3861
|
}
|
|
3739
3862
|
}
|
|
3740
3863
|
}
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "vis-agent"
|
|
11
|
-
version = "0.3
|
|
11
|
+
version = "0.4.3"
|
|
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.12"
|
|
@@ -61,6 +61,22 @@ def live_items(key: str, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
61
61
|
return result
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def _button_branch() -> dict[str, Any]:
|
|
65
|
+
return next(
|
|
66
|
+
branch
|
|
67
|
+
for branch in definition("view", "component")["oneOf"]
|
|
68
|
+
if branch["properties"]["type"].get("const") == "button"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def button_variant_error(variant: Any) -> str | None:
|
|
73
|
+
"""Answer why a declared button variant is invalid, or None when it is valid."""
|
|
74
|
+
allowed = _button_branch()["properties"]["variant"]["enum"]
|
|
75
|
+
if variant is None or variant in allowed:
|
|
76
|
+
return None
|
|
77
|
+
return f"button variant must be one of {', '.join(allowed)}, not {variant!r}"
|
|
78
|
+
|
|
79
|
+
|
|
64
80
|
def _defaults(key: str) -> dict[str, Any]:
|
|
65
81
|
properties = definition("view", key)["properties"]
|
|
66
82
|
return {
|
|
@@ -1076,6 +1076,10 @@ def _live_check_node(node, seen, *, is_declaration=True):
|
|
|
1076
1076
|
return f"a {kind} input has no value: only the host holds its handle"
|
|
1077
1077
|
node_id = node.get("id")
|
|
1078
1078
|
if not isinstance(node_id, str) or not node_id.strip():
|
|
1079
|
+
if is_group:
|
|
1080
|
+
from .extension import _LIVE_GROUP_NEEDS_ID
|
|
1081
|
+
|
|
1082
|
+
return _LIVE_GROUP_NEEDS_ID
|
|
1079
1083
|
return f"a {kind} node needs an id"
|
|
1080
1084
|
if node_id in seen:
|
|
1081
1085
|
return f"two nodes answer to {node_id!r}"
|
|
@@ -1129,6 +1133,9 @@ def _live_check_node(node, seen, *, is_declaration=True):
|
|
|
1129
1133
|
return "a button needs a label"
|
|
1130
1134
|
if is_declaration and "clicks" in node:
|
|
1131
1135
|
return "button clicks are stamped by the engine"
|
|
1136
|
+
complaint = _contracts.button_variant_error(node.get("variant"))
|
|
1137
|
+
if complaint:
|
|
1138
|
+
return complaint
|
|
1132
1139
|
node.setdefault("clicks", 0)
|
|
1133
1140
|
node.setdefault("is_disabled", False)
|
|
1134
1141
|
if type(node["clicks"]) is not int or node["clicks"] < 0:
|
|
@@ -1578,7 +1578,7 @@ class ExecutionLayer(ABC):
|
|
|
1578
1578
|
def get_session_fs_file(
|
|
1579
1579
|
self, sid: str, *, query: Query | None = None, timeout: float | None = None
|
|
1580
1580
|
) -> JSONValue:
|
|
1581
|
-
"""GET /v1/sessions/:sid/fs/file —
|
|
1581
|
+
"""GET /v1/sessions/:sid/fs/file — preview one workspace file by its kind."""
|
|
1582
1582
|
response = self._request(
|
|
1583
1583
|
"GET",
|
|
1584
1584
|
"/v1/sessions/:sid/fs/file",
|
|
@@ -52,6 +52,7 @@ CHECK_KEYS = frozenset(
|
|
|
52
52
|
"id",
|
|
53
53
|
"max_form_output_chars",
|
|
54
54
|
"max_patch_calls_per_file",
|
|
55
|
+
"max_patch_refusals",
|
|
55
56
|
"max_total_output_chars",
|
|
56
57
|
"measurement",
|
|
57
58
|
"verify",
|
|
@@ -306,51 +307,27 @@ def council_recovery_evidence(form_outputs, activities, expected):
|
|
|
306
307
|
block.get("text") for block in blocks if block.get("type") == "markdown"
|
|
307
308
|
]
|
|
308
309
|
|
|
309
|
-
|
|
310
|
-
row
|
|
311
|
-
for row in activities
|
|
312
|
-
if row.get("operation") == "council.publish" and row.get("state") == "succeeded"
|
|
313
|
-
]
|
|
314
|
-
roots = [
|
|
315
|
-
row
|
|
316
|
-
for row in publications
|
|
317
|
-
if references(row, "council-entry") == {thread_id}
|
|
318
|
-
and references(row, "council-thread") == {thread_id}
|
|
319
|
-
and bodies(row) == [expected["root_content"]]
|
|
320
|
-
]
|
|
321
|
-
replies = [
|
|
322
|
-
row
|
|
323
|
-
for row in publications
|
|
324
|
-
if references(row, "council-entry") == {entry_id}
|
|
325
|
-
and references(row, "council-thread") == {thread_id}
|
|
326
|
-
and bodies(row) == [expected["reply_content"]]
|
|
327
|
-
]
|
|
310
|
+
# A publication has no Activity row. The repaired read proves both messages.
|
|
328
311
|
reads = [
|
|
329
312
|
row
|
|
330
313
|
for row in activities
|
|
331
314
|
if row.get("operation") == "council.read" and row.get("state") == "succeeded"
|
|
332
315
|
]
|
|
333
|
-
if (
|
|
334
|
-
len(publications) != 2
|
|
335
|
-
or len(roots) != 1
|
|
336
|
-
or len(replies) != 1
|
|
337
|
-
or len(reads) != 1
|
|
338
|
-
):
|
|
316
|
+
if len(reads) != 1:
|
|
339
317
|
return (
|
|
340
|
-
["Council recovery lacks
|
|
318
|
+
["Council recovery lacks one repaired read"],
|
|
341
319
|
set(),
|
|
342
320
|
[],
|
|
343
321
|
)
|
|
344
|
-
|
|
322
|
+
repaired = reads[0]
|
|
345
323
|
failures = []
|
|
346
324
|
if failed.get("state") != "failed" or failed.get("scope") != event.get("scope"):
|
|
347
325
|
failures.append("Council recovery error does not match its failed Activity")
|
|
348
326
|
if any(
|
|
349
|
-
not row.get("scope") or row["scope"] not in order
|
|
350
|
-
for row in (root, reply, failed, repaired)
|
|
327
|
+
not row.get("scope") or row["scope"] not in order for row in (failed, repaired)
|
|
351
328
|
):
|
|
352
329
|
failures.append("Council recovery is missing a form scope")
|
|
353
|
-
if not position(
|
|
330
|
+
if not position(failed) < position(repaired):
|
|
354
331
|
failures.append(
|
|
355
332
|
"Council recovery did not read the thread after the reply-ID error"
|
|
356
333
|
)
|
|
@@ -436,6 +413,18 @@ def structured_failures(sc, work, answer, activities):
|
|
|
436
413
|
failures.append(
|
|
437
414
|
f"{path} was patched {count} times; collect its edits into at most {limit} patch call(s)"
|
|
438
415
|
)
|
|
416
|
+
# User report 2026-10-31: each refused patch cost the agent one more step.
|
|
417
|
+
limit = sc.get("max_patch_refusals")
|
|
418
|
+
if type(limit) is int:
|
|
419
|
+
refused = sum(
|
|
420
|
+
1
|
|
421
|
+
for row in activities
|
|
422
|
+
if row["operation"] == "patch"
|
|
423
|
+
for call in row.get("children") or [row]
|
|
424
|
+
if call.get("state", row.get("state")) == "failed"
|
|
425
|
+
)
|
|
426
|
+
if refused > limit:
|
|
427
|
+
failures.append(f"patch was refused {refused} times; the limit is {limit}")
|
|
439
428
|
forbidden = set(sc.get("forbid_tools") or [])
|
|
440
429
|
for operation in sorted({row["operation"] for row in activities} & forbidden):
|
|
441
430
|
failures.append(f"forbidden tool {operation!r} was used")
|
|
@@ -3886,6 +3886,30 @@ def notify(text, level="info"):
|
|
|
3886
3886
|
_host.notify(str(text), str(level))
|
|
3887
3887
|
|
|
3888
3888
|
|
|
3889
|
+
def write_private_file(path: str | PathLike[str], text: str) -> None:
|
|
3890
|
+
"""Replace `path` with `text` in one atomic step, readable only by its owner.
|
|
3891
|
+
|
|
3892
|
+
A failure keeps the previous file. A symbolic link writes the file it names.
|
|
3893
|
+
Missing parent directories are created owner-only.
|
|
3894
|
+
"""
|
|
3895
|
+
target = Path(path)
|
|
3896
|
+
if target.is_symlink():
|
|
3897
|
+
target = target.resolve(strict=True)
|
|
3898
|
+
for directory in reversed([d for d in target.parents if not d.exists()]):
|
|
3899
|
+
directory.mkdir(mode=0o700)
|
|
3900
|
+
tmp = target.with_name(f".{target.name}.tmp-{os.urandom(8).hex()}")
|
|
3901
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
3902
|
+
try:
|
|
3903
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
3904
|
+
handle.write(text)
|
|
3905
|
+
handle.flush()
|
|
3906
|
+
os.fsync(handle.fileno())
|
|
3907
|
+
os.replace(tmp, target)
|
|
3908
|
+
except BaseException:
|
|
3909
|
+
tmp.unlink(missing_ok=True)
|
|
3910
|
+
raise
|
|
3911
|
+
|
|
3912
|
+
|
|
3889
3913
|
def _shell_options(name, opts):
|
|
3890
3914
|
if not isinstance(opts, dict):
|
|
3891
3915
|
raise TypeError(
|
|
@@ -6277,6 +6301,12 @@ def live(title, nodes, placement=None, key=None, on_event=None, **options):
|
|
|
6277
6301
|
|
|
6278
6302
|
# -- Extension live-view test harness -----------------------------------------
|
|
6279
6303
|
|
|
6304
|
+
# The refusal of a live group without an id, in the recorder and the outside host.
|
|
6305
|
+
_LIVE_GROUP_NEEDS_ID = (
|
|
6306
|
+
"a live view group needs an id, because patches and after= name it: "
|
|
6307
|
+
"write vis.ui.group('actions', ...) or vis.ui.wrap('actions', ...)"
|
|
6308
|
+
)
|
|
6309
|
+
|
|
6280
6310
|
|
|
6281
6311
|
class _LiveRecorder:
|
|
6282
6312
|
"""An isolated in-memory live host for testing any extension live view.
|
|
@@ -6366,6 +6396,12 @@ class _LiveRecorder:
|
|
|
6366
6396
|
node.setdefault("variant", "braille")
|
|
6367
6397
|
node.setdefault("is_active", True)
|
|
6368
6398
|
elif kind == "button":
|
|
6399
|
+
from blockether.vis import _contracts
|
|
6400
|
+
|
|
6401
|
+
complaint = _contracts.button_variant_error(node.get("variant"))
|
|
6402
|
+
if complaint:
|
|
6403
|
+
# Vis raises VisToolError, a RuntimeError, so the recorder does too.
|
|
6404
|
+
raise RuntimeError(f"button {node.get('id')}: {complaint}")
|
|
6369
6405
|
node["clicks"] = 0
|
|
6370
6406
|
node.setdefault("is_disabled", False)
|
|
6371
6407
|
elif kind == "card" and node.get("is_selectable"):
|
|
@@ -6568,12 +6604,9 @@ class _LiveRecorder:
|
|
|
6568
6604
|
if action == "open":
|
|
6569
6605
|
for node in self._nodes(envelope["view"].get("nodes")):
|
|
6570
6606
|
# The engine refuses a live group without an id, so a test must too.
|
|
6607
|
+
# Vis raises VisToolError, a RuntimeError, so the recorder does too.
|
|
6571
6608
|
if node.get("type") == "group" and not node.get("id"):
|
|
6572
|
-
raise
|
|
6573
|
-
"a live view group needs an id, because patches and "
|
|
6574
|
-
"after= name it: write vis.ui.group('actions', ...) "
|
|
6575
|
-
"or vis.ui.wrap('actions', ...)"
|
|
6576
|
-
)
|
|
6609
|
+
raise RuntimeError(_LIVE_GROUP_NEEDS_ID)
|
|
6577
6610
|
self._view = self._materialize(envelope["view"])
|
|
6578
6611
|
self._declared = set(envelope.get("validators") or [])
|
|
6579
6612
|
self._verdicts = {}
|
|
@@ -7077,14 +7110,22 @@ def _ui_spinner(node_id, text="Working", *, variant="braille", is_active=True, *
|
|
|
7077
7110
|
)
|
|
7078
7111
|
|
|
7079
7112
|
|
|
7080
|
-
def _ui_button(node_id, label, *, is_disabled=False, **spec):
|
|
7113
|
+
def _ui_button(node_id, label, *, is_disabled=False, variant=None, **spec):
|
|
7081
7114
|
"""An operator action. Accepted presses increment ``clicks`` in ``view.state()``.
|
|
7082
7115
|
|
|
7083
|
-
|
|
7116
|
+
``variant`` is ``"primary"``, ``"secondary"`` (the default) or ``"destructive"``.
|
|
7117
|
+
Give one primary action per group. No callback or code crosses the wire.
|
|
7084
7118
|
Disabled buttons and completed receipts cannot be activated.
|
|
7085
7119
|
"""
|
|
7120
|
+
from blockether.vis import _contracts
|
|
7121
|
+
|
|
7122
|
+
complaint = _contracts.button_variant_error(variant)
|
|
7123
|
+
if complaint:
|
|
7124
|
+
raise ValueError(f"button {node_id}: {complaint}")
|
|
7086
7125
|
return _live_node(
|
|
7087
|
-
"button",
|
|
7126
|
+
"button",
|
|
7127
|
+
node_id,
|
|
7128
|
+
dict(spec, label=str(label), is_disabled=is_disabled, variant=variant),
|
|
7088
7129
|
)
|
|
7089
7130
|
|
|
7090
7131
|
|
|
@@ -43,6 +43,8 @@ LATEST = "latest"
|
|
|
43
43
|
LOCK_WAIT_S = 300
|
|
44
44
|
MAX_METADATA = 128 * 1024
|
|
45
45
|
_EXCLUDED = {".git", ".venv", "venv", "__pycache__", "node_modules", ".DS_Store"}
|
|
46
|
+
# `.env*` holds secrets. `.venv-*` is an environment that Vis prepares for one interpreter.
|
|
47
|
+
_EXCLUDED_PREFIXES = (".env", ".venv-")
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def _relative(value):
|
|
@@ -51,7 +53,7 @@ def _relative(value):
|
|
|
51
53
|
if len(value) > 512 or any(ord(c) < 32 or ord(c) == 127 for c in value):
|
|
52
54
|
raise ValueError("Project paths are too long or contain control characters")
|
|
53
55
|
if any(
|
|
54
|
-
p in {"", ".", ".."} or p in _EXCLUDED or p.startswith(
|
|
56
|
+
p in {"", ".", ".."} or p in _EXCLUDED or p.startswith(_EXCLUDED_PREFIXES)
|
|
55
57
|
for p in value.split("/")
|
|
56
58
|
):
|
|
57
59
|
raise ValueError("Project contains an unsafe or excluded path")
|
|
@@ -346,7 +348,11 @@ def _checkout(repository, directory, revision):
|
|
|
346
348
|
|
|
347
349
|
def _copy_project(source, destination):
|
|
348
350
|
def excluded(_directory, names):
|
|
349
|
-
return {
|
|
351
|
+
return {
|
|
352
|
+
name
|
|
353
|
+
for name in names
|
|
354
|
+
if name in _EXCLUDED or name.startswith(_EXCLUDED_PREFIXES)
|
|
355
|
+
}
|
|
350
356
|
|
|
351
357
|
count, size = 0, 0
|
|
352
358
|
for base, directories, files in os.walk(source):
|
|
@@ -48,16 +48,22 @@ def sdk_flow(mode):
|
|
|
48
48
|
|
|
49
49
|
def sdk_native_wait(marker: str):
|
|
50
50
|
"Wait inside C until cancellation reclaims this trusted extension worker."
|
|
51
|
+
import _thread
|
|
51
52
|
import os
|
|
52
53
|
import threading
|
|
53
54
|
from pathlib import Path
|
|
54
|
-
|
|
55
|
+
# A raw `_thread` lock acquire stays in C. The runtime slices threading's
|
|
56
|
+
# own waits, so a Condition wait would unwind on the interrupt.
|
|
57
|
+
gate = threading.Lock()
|
|
58
|
+
waiter = _thread.allocate_lock()
|
|
59
|
+
waiter.acquire()
|
|
55
60
|
def notify_waiting():
|
|
56
|
-
with
|
|
61
|
+
with gate:
|
|
57
62
|
Path(marker).write_text(str(os.getpid()))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
gate.acquire()
|
|
64
|
+
threading.Thread(target=notify_waiting, daemon=True).start()
|
|
65
|
+
gate.release()
|
|
66
|
+
waiter.acquire(timeout=120)
|
|
61
67
|
|
|
62
68
|
vis.register_extension(vis.Extension(name="sdk-fixture", description="SDK integration fixture", alias="sdk",
|
|
63
69
|
symbols=[vis.Symbol(sdk_flow, activity=vis.Activity(presenter="observation", label="SDK flow", render=sdk_activity)),
|
|
@@ -602,9 +608,9 @@ def test_real_council_roundtrip(tmp_path, monkeypatch, transport):
|
|
|
602
608
|
"turn.cancelled",
|
|
603
609
|
}:
|
|
604
610
|
break
|
|
611
|
+
# User request: the team notes show each message, so a publication has no row.
|
|
605
612
|
assert sorted(operations.values()) == [
|
|
606
613
|
"council.get",
|
|
607
|
-
"council.publish",
|
|
608
614
|
"council.threads",
|
|
609
615
|
]
|
|
610
616
|
assert turn.wait(timeout=30)["status"] == "completed"
|
|
@@ -1444,6 +1444,22 @@ class EvaluationIntegrationEdgeTest(unittest.TestCase):
|
|
|
1444
1444
|
group = atif.row("group-call-1", "patch", children=split)
|
|
1445
1445
|
self.assertEqual(1, len(run(group)))
|
|
1446
1446
|
|
|
1447
|
+
def test_patch_refusals_stay_within_the_limit(self):
|
|
1448
|
+
# User report 2026-10-31: each refused patch cost the agent one more step.
|
|
1449
|
+
def run(*rows):
|
|
1450
|
+
return grade.structured_failures(
|
|
1451
|
+
{"max_patch_refusals": 0}, ".", "", list(rows)
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
written = atif.row("call-1", "patch", summary="app.py")
|
|
1455
|
+
refused = atif.row("call-2", "patch", "failed", summary="app.py", sequence=2)
|
|
1456
|
+
self.assertEqual([], run(written))
|
|
1457
|
+
self.assertEqual(
|
|
1458
|
+
["patch was refused 1 times; the limit is 0"], run(written, refused)
|
|
1459
|
+
)
|
|
1460
|
+
group = atif.row("group-call-1", "patch", children=[written, refused])
|
|
1461
|
+
self.assertEqual(1, len(run(group)))
|
|
1462
|
+
|
|
1447
1463
|
def test_malformed_run_metrics_are_a_failure_not_a_crash(self):
|
|
1448
1464
|
helper = DiscoveryEvaluationTest()
|
|
1449
1465
|
malformed = {
|
|
@@ -1509,17 +1525,6 @@ class CouncilRecoveryTest(unittest.TestCase):
|
|
|
1509
1525
|
)
|
|
1510
1526
|
|
|
1511
1527
|
return {
|
|
1512
|
-
"root": row(
|
|
1513
|
-
"root", "council.publish", "succeeded", [42], expected["root_content"]
|
|
1514
|
-
),
|
|
1515
|
-
"reply": row(
|
|
1516
|
-
"reply",
|
|
1517
|
-
"council.publish",
|
|
1518
|
-
"succeeded",
|
|
1519
|
-
[43],
|
|
1520
|
-
expected["reply_content"],
|
|
1521
|
-
sequence=2,
|
|
1522
|
-
),
|
|
1523
1528
|
"mistake": row("mistake", "council.read", "failed", []),
|
|
1524
1529
|
"error": {
|
|
1525
1530
|
"type": "error",
|
|
@@ -1531,11 +1536,12 @@ class CouncilRecoveryTest(unittest.TestCase):
|
|
|
1531
1536
|
}
|
|
1532
1537
|
|
|
1533
1538
|
def blocks(self, parts):
|
|
1534
|
-
"""Return the publish, mistake and repair blocks of the run, in that order.
|
|
1539
|
+
"""Return the publish, mistake and repair blocks of the run, in that order.
|
|
1540
|
+
|
|
1541
|
+
A Council publication leaves no Activity row, so the publish block has none.
|
|
1542
|
+
"""
|
|
1535
1543
|
return [
|
|
1536
|
-
atif.block(
|
|
1537
|
-
"t1/i1/f1", activity=atif.receipt(parts["root"], parts["reply"])
|
|
1538
|
-
),
|
|
1544
|
+
atif.block("t1/i1/f1"),
|
|
1539
1545
|
atif.block(
|
|
1540
1546
|
"t1/i2/f1",
|
|
1541
1547
|
error=parts["error"],
|
|
@@ -241,16 +241,44 @@ def test_table_builder_refuses_text_columns():
|
|
|
241
241
|
vis.ui.table("chats", columns=[vis.ui.table_column("chat", "Chat"), "Category"])
|
|
242
242
|
|
|
243
243
|
|
|
244
|
+
def test_button_variant_is_kept_and_a_typo_names_the_variants():
|
|
245
|
+
# #430: a button had no hierarchy, and a misspelled variant reached the host.
|
|
246
|
+
assert vis.ui.button("save", "Save", variant="primary")["variant"] == "primary"
|
|
247
|
+
assert "variant" not in vis.ui.button("save", "Save")
|
|
248
|
+
with pytest.raises(ValueError, match=r"primary, secondary, destructive.*'danger'"):
|
|
249
|
+
vis.ui.button("drop", "Drop", variant="danger")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_outside_refuses_an_unknown_button_variant():
|
|
253
|
+
# #430: outside Vis a raw button with a typo variant was accepted.
|
|
254
|
+
button = {"id": "drop", "type": "button", "label": "Drop", "variant": "danger"}
|
|
255
|
+
view = {"title": "Rows", "nodes": [button]}
|
|
256
|
+
with pytest.raises(RuntimeError, match=r"primary, secondary, destructive"):
|
|
257
|
+
_outside.live(json.dumps({"op": "open", "view": view}))
|
|
258
|
+
|
|
259
|
+
|
|
244
260
|
def test_recorder_refuses_a_live_group_without_an_id(monkeypatch):
|
|
245
261
|
# #428: the recorder hid the buttons of an unnamed group; the engine refuses it.
|
|
262
|
+
# Vis raises VisToolError, a RuntimeError, so the recorder must raise one too.
|
|
246
263
|
monkeypatch.setattr(vis, "_host", vis.testing.LiveRecorder(vis._host))
|
|
247
|
-
with pytest.raises(
|
|
264
|
+
with pytest.raises(
|
|
265
|
+
RuntimeError, match=r"group needs an id.*vis\.ui\.group\('actions'"
|
|
266
|
+
):
|
|
248
267
|
with vis.live(
|
|
249
268
|
"Chats", [vis.ui.wrap(vis.ui.button("save", "Save"))], flush_ms=0
|
|
250
269
|
):
|
|
251
270
|
pass
|
|
252
271
|
|
|
253
272
|
|
|
273
|
+
def test_outside_refuses_a_live_group_without_an_id_with_the_fix():
|
|
274
|
+
# #428: outside Vis the refusal named no fix for an unnamed group.
|
|
275
|
+
view = {"title": "Chats", "nodes": [vis.ui.wrap(vis.ui.button("save", "Save"))]}
|
|
276
|
+
with pytest.raises(
|
|
277
|
+
RuntimeError, match=r"group needs an id.*vis\.ui\.group\('actions'"
|
|
278
|
+
):
|
|
279
|
+
_outside.live(json.dumps({"op": "open", "view": view}))
|
|
280
|
+
|
|
281
|
+
|
|
254
282
|
def test_outside_refused_patch_leaves_state_unchanged():
|
|
255
283
|
opened = json.loads(
|
|
256
284
|
_outside.live(
|