testerkit 0.4.0__py3-none-any.whl
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.
- testerkit/__init__.py +68 -0
- testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
- testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
- testerkit/_docs/concepts/configuration/capabilities.md +447 -0
- testerkit/_docs/concepts/configuration/fixtures.md +335 -0
- testerkit/_docs/concepts/configuration/index.md +14 -0
- testerkit/_docs/concepts/configuration/parts.md +276 -0
- testerkit/_docs/concepts/configuration/stations.md +214 -0
- testerkit/_docs/concepts/data/data-stores.md +199 -0
- testerkit/_docs/concepts/data/event-log.md +185 -0
- testerkit/_docs/concepts/data/event-sourcing.md +76 -0
- testerkit/_docs/concepts/data/flight-streaming.md +58 -0
- testerkit/_docs/concepts/data/index.md +17 -0
- testerkit/_docs/concepts/data/sessions.md +78 -0
- testerkit/_docs/concepts/data/three-verbs.md +260 -0
- testerkit/_docs/concepts/execution/index.md +13 -0
- testerkit/_docs/concepts/execution/outcomes.md +199 -0
- testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
- testerkit/_docs/concepts/execution/step-manifest.md +115 -0
- testerkit/_docs/concepts/index.md +40 -0
- testerkit/_docs/concepts/overview/ai-integration.md +57 -0
- testerkit/_docs/concepts/overview/architecture.md +357 -0
- testerkit/_docs/concepts/overview/index.md +14 -0
- testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
- testerkit/_docs/concepts/overview/pytest.md +53 -0
- testerkit/_docs/concepts/overview/tiers.md +46 -0
- testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
- testerkit/_docs/how-to/catalog/index.md +11 -0
- testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
- testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
- testerkit/_docs/how-to/configuration/index.md +13 -0
- testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
- testerkit/_docs/how-to/data/benchmarking.md +120 -0
- testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
- testerkit/_docs/how-to/data/capture-waveform.md +110 -0
- testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
- testerkit/_docs/how-to/data/compare-runs.md +120 -0
- testerkit/_docs/how-to/data/export-results.md +104 -0
- testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
- testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
- testerkit/_docs/how-to/data/index.md +23 -0
- testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
- testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
- testerkit/_docs/how-to/data/querying-channels.md +117 -0
- testerkit/_docs/how-to/data/querying-events.md +85 -0
- testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
- testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
- testerkit/_docs/how-to/execution/index.md +21 -0
- testerkit/_docs/how-to/execution/limits.md +193 -0
- testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
- testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
- testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
- testerkit/_docs/how-to/execution/profiles.md +304 -0
- testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
- testerkit/_docs/how-to/execution/test-context.md +212 -0
- testerkit/_docs/how-to/execution/traceability.md +252 -0
- testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
- testerkit/_docs/how-to/execution/writing-tests.md +363 -0
- testerkit/_docs/how-to/index.md +59 -0
- testerkit/_docs/how-to/overview/index.md +11 -0
- testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
- testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
- testerkit/_docs/integration/data/grafana.md +162 -0
- testerkit/_docs/integration/data/index.md +15 -0
- testerkit/_docs/integration/data/lakehouse-import.md +226 -0
- testerkit/_docs/integration/data/logging.md +131 -0
- testerkit/_docs/integration/data/results-api.md +51 -0
- testerkit/_docs/integration/index.md +20 -0
- testerkit/_docs/integration/runtime/harness.md +281 -0
- testerkit/_docs/integration/runtime/index.md +13 -0
- testerkit/_docs/integration/runtime/instruments.md +284 -0
- testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
- testerkit/_docs/reference/catalog/cookbook.md +389 -0
- testerkit/_docs/reference/catalog/index.md +11 -0
- testerkit/_docs/reference/catalog/schema.md +375 -0
- testerkit/_docs/reference/cli.md +660 -0
- testerkit/_docs/reference/configuration.md +406 -0
- testerkit/_docs/reference/data/channels-schema.md +145 -0
- testerkit/_docs/reference/data/event-types.md +554 -0
- testerkit/_docs/reference/data/events-schema.md +120 -0
- testerkit/_docs/reference/data/files-schema.md +141 -0
- testerkit/_docs/reference/data/index.md +18 -0
- testerkit/_docs/reference/data/models.md +2229 -0
- testerkit/_docs/reference/data/outputs.md +82 -0
- testerkit/_docs/reference/data/parquet-schema.md +529 -0
- testerkit/_docs/reference/data/performance-limits.md +224 -0
- testerkit/_docs/reference/data/query-api.md +247 -0
- testerkit/_docs/reference/index.md +69 -0
- testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
- testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
- testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
- testerkit/_docs/reference/operator-ui/designer.md +167 -0
- testerkit/_docs/reference/operator-ui/events.md +89 -0
- testerkit/_docs/reference/operator-ui/files.md +74 -0
- testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
- testerkit/_docs/reference/operator-ui/instruments.md +126 -0
- testerkit/_docs/reference/operator-ui/launch.md +79 -0
- testerkit/_docs/reference/operator-ui/live.md +102 -0
- testerkit/_docs/reference/operator-ui/measurements.md +159 -0
- testerkit/_docs/reference/operator-ui/metrics.md +217 -0
- testerkit/_docs/reference/operator-ui/parts.md +98 -0
- testerkit/_docs/reference/operator-ui/profiles.md +103 -0
- testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
- testerkit/_docs/reference/operator-ui/results/list.md +106 -0
- testerkit/_docs/reference/operator-ui/stations.md +98 -0
- testerkit/_docs/reference/operator-ui/tests.md +173 -0
- testerkit/_docs/reference/operator-ui/uuts.md +67 -0
- testerkit/_docs/reference/overview/index.md +11 -0
- testerkit/_docs/reference/overview/pytest-native.md +142 -0
- testerkit/_docs/reference/overview/skills.md +177 -0
- testerkit/_docs/reference/pytest/fixtures.md +342 -0
- testerkit/_docs/reference/pytest/index.md +12 -0
- testerkit/_docs/reference/pytest/markers.md +257 -0
- testerkit/_docs/reference/runtime/api.md +246 -0
- testerkit/_docs/reference/runtime/client.md +280 -0
- testerkit/_docs/reference/runtime/connect.md +140 -0
- testerkit/_docs/reference/runtime/index.md +13 -0
- testerkit/_docs/tutorial/01-first-test.md +113 -0
- testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
- testerkit/_docs/tutorial/03-fixtures.md +169 -0
- testerkit/_docs/tutorial/04-limits.md +139 -0
- testerkit/_docs/tutorial/05-configuration.md +194 -0
- testerkit/_docs/tutorial/06-specifications.md +273 -0
- testerkit/_docs/tutorial/07-real-instruments.md +245 -0
- testerkit/_docs/tutorial/08-capabilities.md +223 -0
- testerkit/_docs/tutorial/09-production.md +374 -0
- testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
- testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
- testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
- testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
- testerkit/_docs/tutorial/index.md +61 -0
- testerkit/_docs/tutorial/quickstart.md +282 -0
- testerkit/analysis/__init__.py +9 -0
- testerkit/analysis/_common.py +24 -0
- testerkit/analysis/measurement_facets.py +524 -0
- testerkit/analysis/measurements_query.py +1458 -0
- testerkit/analysis/metrics.py +389 -0
- testerkit/analysis/runs_query.py +434 -0
- testerkit/analysis/steps_query.py +413 -0
- testerkit/api/__init__.py +6 -0
- testerkit/api/_mime.py +71 -0
- testerkit/api/app.py +1171 -0
- testerkit/api/dialogs/__init__.py +36 -0
- testerkit/api/dialogs/manager.py +617 -0
- testerkit/api/dialogs/models.py +79 -0
- testerkit/api/models.py +80 -0
- testerkit/api/responses.py +205 -0
- testerkit/api/runner.py +222 -0
- testerkit/api/schemas.py +311 -0
- testerkit/benchmark/__init__.py +34 -0
- testerkit/benchmark/concurrency.py +248 -0
- testerkit/benchmark/core.py +232 -0
- testerkit/benchmark/runner.py +628 -0
- testerkit/benchmark/scenario.py +313 -0
- testerkit/benchmark/system.py +191 -0
- testerkit/benchmark/workloads.py +507 -0
- testerkit/catalog/__init__.py +6 -0
- testerkit/catalog/generic/generic_dmm.yaml +61 -0
- testerkit/catalog/generic/generic_eload.yaml +31 -0
- testerkit/catalog/generic/generic_fgen.yaml +56 -0
- testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
- testerkit/catalog/generic/generic_psu.yaml +77 -0
- testerkit/catalog/generic/generic_smu.yaml +73 -0
- testerkit/channels.py +538 -0
- testerkit/cli/__init__.py +32 -0
- testerkit/cli/_common.py +113 -0
- testerkit/cli/_time.py +179 -0
- testerkit/cli/benchmark_cmd.py +56 -0
- testerkit/cli/catalog_cmd.py +39 -0
- testerkit/cli/daemon.py +157 -0
- testerkit/cli/data_cmd.py +1040 -0
- testerkit/cli/discover_cmd.py +94 -0
- testerkit/cli/docs_cmd.py +100 -0
- testerkit/cli/instrument.py +172 -0
- testerkit/cli/mcp_cmd.py +37 -0
- testerkit/cli/metrics.py +366 -0
- testerkit/cli/project.py +312 -0
- testerkit/cli/root.py +14 -0
- testerkit/cli/runs.py +412 -0
- testerkit/cli/schema_cmd.py +87 -0
- testerkit/cli/serve_cmd.py +50 -0
- testerkit/cli/setup_cmd.py +604 -0
- testerkit/cli/station.py +294 -0
- testerkit/cli/validate.py +112 -0
- testerkit/client.py +463 -0
- testerkit/connect.py +635 -0
- testerkit/data/__init__.py +8 -0
- testerkit/data/_accumulator_pool.py +455 -0
- testerkit/data/_atomic.py +59 -0
- testerkit/data/_collection_indices.py +67 -0
- testerkit/data/_daemon_lifecycle.py +535 -0
- testerkit/data/_duckdb_daemon.py +695 -0
- testerkit/data/_duckdb_flight_server.py +813 -0
- testerkit/data/_event_filters.py +15 -0
- testerkit/data/_event_reader.py +101 -0
- testerkit/data/_flight_errors.py +199 -0
- testerkit/data/_flight_query.py +202 -0
- testerkit/data/_flight_retry.py +148 -0
- testerkit/data/_flight_subscribe.py +67 -0
- testerkit/data/_index_epoch.py +342 -0
- testerkit/data/_ipc_writer.py +185 -0
- testerkit/data/_json_safe.py +63 -0
- testerkit/data/_process.py +18 -0
- testerkit/data/_push_relay.py +124 -0
- testerkit/data/_runs_duckdb_daemon.py +2704 -0
- testerkit/data/_session_reaper.py +110 -0
- testerkit/data/_sql_helpers.py +30 -0
- testerkit/data/_store.py +24 -0
- testerkit/data/backends/__init__.py +7 -0
- testerkit/data/backends/_event_accumulator.py +957 -0
- testerkit/data/backends/_row_helpers.py +1172 -0
- testerkit/data/backends/parquet.py +1143 -0
- testerkit/data/backends/protocol.py +35 -0
- testerkit/data/channels/__init__.py +8 -0
- testerkit/data/channels/_flight_daemon.py +75 -0
- testerkit/data/channels/client.py +242 -0
- testerkit/data/channels/flight_manager.py +88 -0
- testerkit/data/channels/index.py +664 -0
- testerkit/data/channels/models.py +418 -0
- testerkit/data/channels/server.py +162 -0
- testerkit/data/channels/store.py +1481 -0
- testerkit/data/data_dir.py +61 -0
- testerkit/data/duckdb_manager.py +79 -0
- testerkit/data/event_log.py +384 -0
- testerkit/data/event_store.py +768 -0
- testerkit/data/events.py +1025 -0
- testerkit/data/exporters/__init__.py +6 -0
- testerkit/data/exporters/_helpers.py +40 -0
- testerkit/data/exporters/csv_exporter.py +168 -0
- testerkit/data/exporters/hdf5.py +234 -0
- testerkit/data/exporters/json_exporter.py +229 -0
- testerkit/data/exporters/mdf4.py +201 -0
- testerkit/data/exporters/stdf.py +255 -0
- testerkit/data/exporters/tdms.py +292 -0
- testerkit/data/files/__init__.py +71 -0
- testerkit/data/files/_backend.py +207 -0
- testerkit/data/files/_catalog_daemon.py +84 -0
- testerkit/data/files/catalog.py +218 -0
- testerkit/data/files/catalog_manager.py +325 -0
- testerkit/data/files/models.py +60 -0
- testerkit/data/files/serializers.py +414 -0
- testerkit/data/files/store.py +463 -0
- testerkit/data/files/streaming.py +783 -0
- testerkit/data/models.py +604 -0
- testerkit/data/ref.py +152 -0
- testerkit/data/retention.py +297 -0
- testerkit/data/run_store.py +367 -0
- testerkit/data/runs_duckdb_manager.py +103 -0
- testerkit/data/schema_dispatch.py +191 -0
- testerkit/data/schema_migrate.py +76 -0
- testerkit/data/schema_versions.py +85 -0
- testerkit/data/schemas.py +288 -0
- testerkit/data/subscribers/__init__.py +48 -0
- testerkit/data/subscribers/_base.py +20 -0
- testerkit/data/subscribers/_output_file.py +25 -0
- testerkit/data/subscribers/replay.py +55 -0
- testerkit/environment.py +84 -0
- testerkit/execution/__init__.py +11 -0
- testerkit/execution/_git.py +190 -0
- testerkit/execution/_state.py +740 -0
- testerkit/execution/accessors.py +54 -0
- testerkit/execution/audit.py +51 -0
- testerkit/execution/cascade.py +85 -0
- testerkit/execution/connections.py +491 -0
- testerkit/execution/harness.py +1931 -0
- testerkit/execution/instrument_events.py +42 -0
- testerkit/execution/limits.py +154 -0
- testerkit/execution/metadata.py +102 -0
- testerkit/execution/mocks.py +63 -0
- testerkit/execution/profiles.py +616 -0
- testerkit/execution/run_scope.py +1331 -0
- testerkit/execution/session_scope.py +196 -0
- testerkit/execution/sidecar.py +280 -0
- testerkit/execution/site_runner.py +705 -0
- testerkit/execution/sites.py +183 -0
- testerkit/execution/sync.py +294 -0
- testerkit/execution/uut_provider.py +231 -0
- testerkit/execution/vectors.py +205 -0
- testerkit/execution/verify.py +396 -0
- testerkit/expand.py +75 -0
- testerkit/files.py +235 -0
- testerkit/fixtures/__init__.py +6 -0
- testerkit/fixtures/manager.py +357 -0
- testerkit/grafana/__init__.py +1 -0
- testerkit/grafana/bootstrap.py +61 -0
- testerkit/grafana/cli.py +314 -0
- testerkit/grafana/dashboards/asset_utilization.json +92 -0
- testerkit/grafana/dashboards/channel_explorer.json +77 -0
- testerkit/grafana/dashboards/event_log.json +92 -0
- testerkit/grafana/dashboards/failure_pareto.json +104 -0
- testerkit/grafana/dashboards/measurement_distribution.json +97 -0
- testerkit/grafana/dashboards/measurement_trend.json +161 -0
- testerkit/grafana/dashboards/station_comparison.json +95 -0
- testerkit/grafana/dashboards/test_duration.json +104 -0
- testerkit/grafana/dashboards/unit_traceability.json +73 -0
- testerkit/grafana/dashboards/yield_overview.json +189 -0
- testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
- testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
- testerkit/grafana/server.py +282 -0
- testerkit/init.py +724 -0
- testerkit/instruments/__init__.py +10 -0
- testerkit/instruments/base.py +82 -0
- testerkit/instruments/discovery/__init__.py +62 -0
- testerkit/instruments/discovery/_base.py +174 -0
- testerkit/instruments/discovery/lxi.py +152 -0
- testerkit/instruments/discovery/ni.py +95 -0
- testerkit/instruments/discovery/serial.py +73 -0
- testerkit/instruments/discovery/visa.py +98 -0
- testerkit/instruments/lifecycle.py +213 -0
- testerkit/instruments/loader.py +79 -0
- testerkit/instruments/locks.py +178 -0
- testerkit/instruments/mocks.py +230 -0
- testerkit/instruments/observer.py +270 -0
- testerkit/instruments/observers/__init__.py +36 -0
- testerkit/instruments/observers/_base.py +68 -0
- testerkit/instruments/observers/daqmx.py +70 -0
- testerkit/instruments/observers/descriptor.py +69 -0
- testerkit/instruments/observers/generic.py +92 -0
- testerkit/instruments/observers/lantz.py +31 -0
- testerkit/instruments/observers/modbus.py +96 -0
- testerkit/instruments/observers/motion.py +68 -0
- testerkit/instruments/observers/ni_modular.py +73 -0
- testerkit/instruments/observers/ophyd.py +80 -0
- testerkit/instruments/observers/pymeasure.py +120 -0
- testerkit/instruments/observers/qcodes.py +66 -0
- testerkit/instruments/observers/scpi.py +88 -0
- testerkit/instruments/observers/tektronix.py +57 -0
- testerkit/instruments/observers/visa.py +103 -0
- testerkit/instruments/pool.py +459 -0
- testerkit/instruments/proxy.py +59 -0
- testerkit/instruments/route_manager.py +329 -0
- testerkit/instruments/routed_proxy.py +88 -0
- testerkit/instruments/server.py +508 -0
- testerkit/instruments/switch.py +43 -0
- testerkit/instruments/visa.py +294 -0
- testerkit/matching/__init__.py +6 -0
- testerkit/matching/service.py +969 -0
- testerkit/mcp/__init__.py +6 -0
- testerkit/mcp/server.py +681 -0
- testerkit/mcp/tools.py +1868 -0
- testerkit/models/__init__.py +18 -0
- testerkit/models/capability.py +598 -0
- testerkit/models/catalog.py +76 -0
- testerkit/models/data_options.py +153 -0
- testerkit/models/enums.py +334 -0
- testerkit/models/instrument.py +152 -0
- testerkit/models/instrument_asset.py +25 -0
- testerkit/models/part.py +271 -0
- testerkit/models/part_manifest.py +96 -0
- testerkit/models/project.py +114 -0
- testerkit/models/station.py +140 -0
- testerkit/models/test_config.py +748 -0
- testerkit/ontology/__init__.py +39 -0
- testerkit/ontology/schema.py +188 -0
- testerkit/ontology/testerkit.yaml +1420 -0
- testerkit/parts/__init__.py +8 -0
- testerkit/parts/context.py +181 -0
- testerkit/parts/folder.py +251 -0
- testerkit/parts/loader.py +27 -0
- testerkit/prompts/__init__.py +17 -0
- testerkit/prompts/core.py +133 -0
- testerkit/pytest_plugin/__init__.py +1287 -0
- testerkit/pytest_plugin/autouse.py +369 -0
- testerkit/pytest_plugin/helpers.py +354 -0
- testerkit/pytest_plugin/hooks.py +1982 -0
- testerkit/pytest_plugin/markers.py +203 -0
- testerkit/pytest_plugin/retry.py +37 -0
- testerkit/pytest_plugin/sweeps.py +117 -0
- testerkit/queries.py +78 -0
- testerkit/reports/__init__.py +6 -0
- testerkit/reports/core.py +492 -0
- testerkit/reports/datasheet.py +831 -0
- testerkit/reports/templates/datasheet.html +383 -0
- testerkit/reports/templates/default.html +153 -0
- testerkit/sbom.py +142 -0
- testerkit/schema_export.py +101 -0
- testerkit/signals.py +63 -0
- testerkit/skills/README.md +58 -0
- testerkit/skills/templates/project-instructions.md +125 -0
- testerkit/skills/testerkit-analysis/SKILL.md +87 -0
- testerkit/skills/testerkit-capture/SKILL.md +114 -0
- testerkit/skills/testerkit-data/SKILL.md +138 -0
- testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
- testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
- testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
- testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
- testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
- testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
- testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
- testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
- testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
- testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
- testerkit/skills/testerkit-debug/SKILL.md +127 -0
- testerkit/skills/testerkit-interactive/SKILL.md +188 -0
- testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
- testerkit/skills/testerkit-mocks/SKILL.md +128 -0
- testerkit/skills/testerkit-parts/SKILL.md +126 -0
- testerkit/skills/testerkit-profiles/SKILL.md +139 -0
- testerkit/skills/testerkit-sites/SKILL.md +140 -0
- testerkit/skills/testerkit-stations/SKILL.md +146 -0
- testerkit/skills/testerkit-tests/SKILL.md +136 -0
- testerkit/store.py +1639 -0
- testerkit/ui/__init__.py +71 -0
- testerkit/ui/_asgi.py +162 -0
- testerkit/ui/app.py +32 -0
- testerkit/ui/components/__init__.py +13 -0
- testerkit/ui/components/artifact_viewer.py +250 -0
- testerkit/ui/components/channel_values.py +159 -0
- testerkit/ui/components/event_timeline.py +222 -0
- testerkit/ui/components/execution_gantt.py +210 -0
- testerkit/ui/components/file_streams.py +147 -0
- testerkit/ui/components/instrument_activity.py +146 -0
- testerkit/ui/components/session_table.py +118 -0
- testerkit/ui/pages/__init__.py +25 -0
- testerkit/ui/pages/channels/__init__.py +4 -0
- testerkit/ui/pages/channels/detail.py +932 -0
- testerkit/ui/pages/channels/list.py +515 -0
- testerkit/ui/pages/dashboard.py +236 -0
- testerkit/ui/pages/designer/__init__.py +5 -0
- testerkit/ui/pages/designer/graph.py +635 -0
- testerkit/ui/pages/designer/matching.py +663 -0
- testerkit/ui/pages/designer/page.py +593 -0
- testerkit/ui/pages/designer/properties.py +591 -0
- testerkit/ui/pages/designer/state.py +468 -0
- testerkit/ui/pages/docs/__init__.py +6 -0
- testerkit/ui/pages/docs/index.py +95 -0
- testerkit/ui/pages/docs/page.py +698 -0
- testerkit/ui/pages/events/__init__.py +3 -0
- testerkit/ui/pages/events/list.py +263 -0
- testerkit/ui/pages/explore.py +1108 -0
- testerkit/ui/pages/files/__init__.py +4 -0
- testerkit/ui/pages/files/detail.py +426 -0
- testerkit/ui/pages/files/list.py +447 -0
- testerkit/ui/pages/fixtures/__init__.py +8 -0
- testerkit/ui/pages/fixtures/detail.py +280 -0
- testerkit/ui/pages/fixtures/edit.py +370 -0
- testerkit/ui/pages/fixtures/list.py +180 -0
- testerkit/ui/pages/fixtures/new.py +195 -0
- testerkit/ui/pages/instruments/__init__.py +4 -0
- testerkit/ui/pages/instruments/detail.py +320 -0
- testerkit/ui/pages/instruments/edit.py +482 -0
- testerkit/ui/pages/instruments/list.py +249 -0
- testerkit/ui/pages/instruments/new.py +200 -0
- testerkit/ui/pages/launch.py +261 -0
- testerkit/ui/pages/live.py +124 -0
- testerkit/ui/pages/metrics_page.py +1229 -0
- testerkit/ui/pages/parts/__init__.py +11 -0
- testerkit/ui/pages/parts/detail.py +202 -0
- testerkit/ui/pages/parts/edit.py +310 -0
- testerkit/ui/pages/parts/list.py +160 -0
- testerkit/ui/pages/parts/new.py +170 -0
- testerkit/ui/pages/parts/requirements.py +153 -0
- testerkit/ui/pages/parts/stations.py +199 -0
- testerkit/ui/pages/profiles/__init__.py +4 -0
- testerkit/ui/pages/profiles/detail.py +113 -0
- testerkit/ui/pages/profiles/list.py +86 -0
- testerkit/ui/pages/results/__init__.py +4 -0
- testerkit/ui/pages/results/detail.py +631 -0
- testerkit/ui/pages/results/list.py +230 -0
- testerkit/ui/pages/stations/__init__.py +4 -0
- testerkit/ui/pages/stations/detail.py +256 -0
- testerkit/ui/pages/stations/edit.py +273 -0
- testerkit/ui/pages/stations/list.py +180 -0
- testerkit/ui/pages/stations/new.py +311 -0
- testerkit/ui/pages/tests/__init__.py +4 -0
- testerkit/ui/pages/tests/detail.py +152 -0
- testerkit/ui/pages/tests/list.py +194 -0
- testerkit/ui/pages/uuts/__init__.py +4 -0
- testerkit/ui/pages/uuts/list.py +97 -0
- testerkit/ui/shared/__init__.py +8 -0
- testerkit/ui/shared/components.py +1826 -0
- testerkit/ui/shared/dialogs.py +105 -0
- testerkit/ui/shared/event_binding.py +194 -0
- testerkit/ui/shared/layout.py +314 -0
- testerkit/ui/shared/services.py +1719 -0
- testerkit/ui/shared/timestamps.py +21 -0
- testerkit/ui/static/branding/favicon-16.png +0 -0
- testerkit/ui/static/branding/favicon-32.png +0 -0
- testerkit/ui/static/branding/favicon-48.png +0 -0
- testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
- testerkit/ui/static/branding/testerkit-mark.svg +1 -0
- testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
- testerkit/ui/static/global.css +465 -0
- testerkit/utils/__init__.py +17 -0
- testerkit/utils/enum_meta.py +800 -0
- testerkit/utils/paths.py +75 -0
- testerkit/utils/ranges.py +257 -0
- testerkit/validation.py +82 -0
- testerkit/verbs.py +142 -0
- testerkit-0.4.0.dist-info/METADATA +250 -0
- testerkit-0.4.0.dist-info/RECORD +521 -0
- testerkit-0.4.0.dist-info/WHEEL +4 -0
- testerkit-0.4.0.dist-info/entry_points.txt +5 -0
- testerkit-0.4.0.dist-info/licenses/LICENSE +191 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Operator dialog system — UI-layer rendering of operator prompts.
|
|
2
|
+
|
|
3
|
+
Internal to ``testerkit serve``. Test code never imports this module
|
|
4
|
+
directly — it goes through :func:`testerkit.prompts.ask`. When the UI is
|
|
5
|
+
running, ``register_as_prompt_handler()`` installs a bridge so prompts
|
|
6
|
+
route through the dialog queue + UI; otherwise prompts fall back to
|
|
7
|
+
terminal / auto-confirm.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from testerkit.api.dialogs.manager import (
|
|
11
|
+
DialogManager,
|
|
12
|
+
get_dialog_manager,
|
|
13
|
+
register_as_prompt_handler,
|
|
14
|
+
)
|
|
15
|
+
from testerkit.api.dialogs.models import (
|
|
16
|
+
ChoiceDialog,
|
|
17
|
+
ConfirmDialog,
|
|
18
|
+
Dialog,
|
|
19
|
+
DialogResponse,
|
|
20
|
+
DialogType,
|
|
21
|
+
ImageDialog,
|
|
22
|
+
InputDialog,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Dialog",
|
|
27
|
+
"DialogType",
|
|
28
|
+
"DialogResponse",
|
|
29
|
+
"ConfirmDialog",
|
|
30
|
+
"ChoiceDialog",
|
|
31
|
+
"InputDialog",
|
|
32
|
+
"ImageDialog",
|
|
33
|
+
"DialogManager",
|
|
34
|
+
"get_dialog_manager",
|
|
35
|
+
"register_as_prompt_handler",
|
|
36
|
+
]
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""Dialog manager for coordinating operator interactions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from testerkit.api.dialogs.models import (
|
|
13
|
+
ChoiceDialog,
|
|
14
|
+
ConfirmDialog,
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogResponse,
|
|
17
|
+
ImageDialog,
|
|
18
|
+
InputDialog,
|
|
19
|
+
)
|
|
20
|
+
from testerkit.data.events import DialogOpened, DialogResponded
|
|
21
|
+
from testerkit.execution._state import get_current_run_scope
|
|
22
|
+
from testerkit.prompts.core import TESTERKIT_AUTO_CONFIRM
|
|
23
|
+
|
|
24
|
+
_logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DialogManager:
|
|
28
|
+
"""Manages operator dialogs across test runs.
|
|
29
|
+
|
|
30
|
+
The dialog manager coordinates between test code requesting dialogs
|
|
31
|
+
and the UI displaying them. It supports two modes:
|
|
32
|
+
|
|
33
|
+
1. In-process mode (default): Uses asyncio events for local coordination
|
|
34
|
+
2. HTTP mode: For test subprocesses to communicate with the server
|
|
35
|
+
|
|
36
|
+
Usage from test code (in-process):
|
|
37
|
+
manager = get_dialog_manager()
|
|
38
|
+
response = await manager.confirm("Is UUT connected?")
|
|
39
|
+
if response.confirmed:
|
|
40
|
+
# proceed with test
|
|
41
|
+
|
|
42
|
+
Usage from test subprocess (HTTP mode):
|
|
43
|
+
manager = get_dialog_manager() # auto-detects TESTERKIT_SERVER_URL
|
|
44
|
+
response = await manager.confirm("Is UUT connected?")
|
|
45
|
+
# This POSTs to server and polls for response
|
|
46
|
+
|
|
47
|
+
Usage from UI:
|
|
48
|
+
manager = get_dialog_manager()
|
|
49
|
+
dialog = manager.get_pending_dialog(run_id)
|
|
50
|
+
if dialog:
|
|
51
|
+
# display dialog
|
|
52
|
+
manager.respond(dialog.id, DialogResponse(...))
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, server_url: str | None = None, auto_respond: str | None = None):
|
|
56
|
+
"""Initialize dialog manager.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
server_url: If provided, use HTTP mode to communicate with server.
|
|
60
|
+
If None, uses in-process mode with asyncio events.
|
|
61
|
+
auto_respond: Auto-respond mode. Values:
|
|
62
|
+
- "confirm": Auto-confirm all dialogs
|
|
63
|
+
- "cancel": Auto-cancel all dialogs
|
|
64
|
+
- None: Check TESTERKIT_AUTO_CONFIRM env var, then use normal behavior
|
|
65
|
+
"""
|
|
66
|
+
self.server_url = server_url
|
|
67
|
+
self._auto_respond = auto_respond
|
|
68
|
+
self._pending: dict[UUID, Dialog] = {}
|
|
69
|
+
self._responses: dict[UUID, DialogResponse] = {}
|
|
70
|
+
self._events: dict[UUID, asyncio.Event] = {}
|
|
71
|
+
self._listeners: list[Callable[[Dialog], None]] = []
|
|
72
|
+
self._preset_responses: deque[DialogResponse] = deque()
|
|
73
|
+
self._open_times: dict[UUID, float] = {}
|
|
74
|
+
|
|
75
|
+
def add_listener(self, callback: Callable[[Dialog], None]) -> None:
|
|
76
|
+
"""Add a listener for new dialogs."""
|
|
77
|
+
self._listeners.append(callback)
|
|
78
|
+
|
|
79
|
+
def remove_listener(self, callback: Callable[[Dialog], None]) -> None:
|
|
80
|
+
"""Remove a dialog listener."""
|
|
81
|
+
if callback in self._listeners:
|
|
82
|
+
self._listeners.remove(callback)
|
|
83
|
+
|
|
84
|
+
def _notify_listeners(self, dialog: Dialog) -> None:
|
|
85
|
+
"""Notify all listeners of a new dialog."""
|
|
86
|
+
for listener in self._listeners:
|
|
87
|
+
try:
|
|
88
|
+
listener(dialog)
|
|
89
|
+
except Exception:
|
|
90
|
+
# Don't let listener errors break dialog flow, but
|
|
91
|
+
# surface them to debug logs so silent UI-callback
|
|
92
|
+
# failures aren't invisible.
|
|
93
|
+
_logger.debug("Dialog listener raised", exc_info=True)
|
|
94
|
+
|
|
95
|
+
def preset_response(
|
|
96
|
+
self,
|
|
97
|
+
confirmed: bool = True,
|
|
98
|
+
choice: int | None = 0,
|
|
99
|
+
choices: list[int] | None = None,
|
|
100
|
+
value: str = "",
|
|
101
|
+
cancelled: bool = False,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Queue a preset response for the next dialog.
|
|
104
|
+
|
|
105
|
+
Preset responses are consumed in FIFO order. Use this in tests
|
|
106
|
+
to avoid waiting for operator input.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
confirmed: Whether to confirm the dialog
|
|
110
|
+
choice: Selected choice index (for choice dialogs)
|
|
111
|
+
choices: Selected choice indices (for multi-select)
|
|
112
|
+
value: Input value (for input dialogs)
|
|
113
|
+
cancelled: Whether the dialog was cancelled
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
manager.preset_response(confirmed=True)
|
|
117
|
+
response = await manager.confirm("Ready?") # Returns immediately
|
|
118
|
+
assert response.confirmed
|
|
119
|
+
"""
|
|
120
|
+
# dialog_id will be set when consumed
|
|
121
|
+
response = DialogResponse(
|
|
122
|
+
dialog_id=UUID("00000000-0000-0000-0000-000000000000"),
|
|
123
|
+
confirmed=confirmed,
|
|
124
|
+
choice=choice,
|
|
125
|
+
choices=choices,
|
|
126
|
+
value=value,
|
|
127
|
+
cancelled=cancelled,
|
|
128
|
+
timed_out=False,
|
|
129
|
+
)
|
|
130
|
+
self._preset_responses.append(response)
|
|
131
|
+
|
|
132
|
+
def clear_preset_responses(self) -> None:
|
|
133
|
+
"""Clear all queued preset responses."""
|
|
134
|
+
self._preset_responses.clear()
|
|
135
|
+
|
|
136
|
+
def _get_auto_response(self, dialog: Dialog) -> DialogResponse | None:
|
|
137
|
+
"""Check for auto-respond mode and return immediate response if enabled.
|
|
138
|
+
|
|
139
|
+
Checks in order:
|
|
140
|
+
1. Preset responses queue
|
|
141
|
+
2. Instance auto_respond setting
|
|
142
|
+
3. TESTERKIT_AUTO_CONFIRM environment variable
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
DialogResponse if auto-respond is enabled, None otherwise.
|
|
146
|
+
"""
|
|
147
|
+
# Check preset responses first
|
|
148
|
+
if self._preset_responses:
|
|
149
|
+
response = self._preset_responses.popleft()
|
|
150
|
+
# Update dialog_id to match actual dialog
|
|
151
|
+
return DialogResponse(
|
|
152
|
+
dialog_id=dialog.id,
|
|
153
|
+
confirmed=response.confirmed,
|
|
154
|
+
choice=response.choice,
|
|
155
|
+
choices=response.choices,
|
|
156
|
+
value=response.value,
|
|
157
|
+
cancelled=response.cancelled,
|
|
158
|
+
timed_out=False,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Check instance setting, then environment variable
|
|
162
|
+
auto_mode = self._auto_respond or os.environ.get(TESTERKIT_AUTO_CONFIRM)
|
|
163
|
+
if not auto_mode:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# Generate appropriate auto-response based on mode
|
|
167
|
+
auto_mode = auto_mode.lower()
|
|
168
|
+
|
|
169
|
+
if auto_mode == "cancel":
|
|
170
|
+
return DialogResponse(dialog_id=dialog.id, cancelled=True)
|
|
171
|
+
|
|
172
|
+
# Default to confirm mode. ``value=""`` matches
|
|
173
|
+
# :func:`testerkit.prompts.core._auto_confirm` so input prompts
|
|
174
|
+
# see the same auto-confirm value whether they go through the
|
|
175
|
+
# dialog UI bridge or fall through to the bare auto-confirm
|
|
176
|
+
# chain in ``prompts.ask``.
|
|
177
|
+
return DialogResponse(
|
|
178
|
+
dialog_id=dialog.id,
|
|
179
|
+
confirmed=True,
|
|
180
|
+
choice=0, # First choice for choice dialogs
|
|
181
|
+
value="", # Empty input — matches prompts._auto_confirm
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _emit_dialog_opened(self, dialog: Dialog) -> None:
|
|
185
|
+
"""Emit a DialogOpened event if inside a test run.
|
|
186
|
+
|
|
187
|
+
Standalone UI / API contexts (where dialogs run without a test
|
|
188
|
+
run scope) silently no-op the parquet-event side; the dialog
|
|
189
|
+
itself still fires.
|
|
190
|
+
"""
|
|
191
|
+
run_scope = get_current_run_scope()
|
|
192
|
+
if run_scope is None or run_scope.event_log is None:
|
|
193
|
+
return
|
|
194
|
+
run_scope.event_log.emit(
|
|
195
|
+
DialogOpened(
|
|
196
|
+
session_id=run_scope._session_id,
|
|
197
|
+
run_id=run_scope.test_run.id,
|
|
198
|
+
dialog_id=dialog.id,
|
|
199
|
+
dialog_type=dialog.type.value,
|
|
200
|
+
title=dialog.title,
|
|
201
|
+
message=dialog.message,
|
|
202
|
+
step_name=dialog.step_name,
|
|
203
|
+
blocking=dialog.blocking,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _emit_dialog_responded(
|
|
208
|
+
self, dialog: Dialog, response: DialogResponse, duration: float
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Emit a DialogResponded event if inside a test run."""
|
|
211
|
+
run_scope = get_current_run_scope()
|
|
212
|
+
if run_scope is None or run_scope.event_log is None:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if response.timed_out:
|
|
216
|
+
response_type = "timed_out"
|
|
217
|
+
elif response.cancelled:
|
|
218
|
+
response_type = "cancelled"
|
|
219
|
+
else:
|
|
220
|
+
# "answered" covers all non-cancelled, non-timeout responses:
|
|
221
|
+
# confirm-clicked, choice-selected, input-entered. The dialog
|
|
222
|
+
# type is already on the event for downstream filtering.
|
|
223
|
+
response_type = "answered"
|
|
224
|
+
|
|
225
|
+
run_scope.event_log.emit(
|
|
226
|
+
DialogResponded(
|
|
227
|
+
session_id=run_scope._session_id,
|
|
228
|
+
run_id=run_scope.test_run.id,
|
|
229
|
+
dialog_id=dialog.id,
|
|
230
|
+
dialog_type=dialog.type.value,
|
|
231
|
+
response_type=response_type,
|
|
232
|
+
duration_seconds=duration,
|
|
233
|
+
value=response.value,
|
|
234
|
+
choice=response.choice,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def show(self, dialog: Dialog) -> DialogResponse:
|
|
239
|
+
"""Show a dialog and wait for response.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
dialog: The dialog to display.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
The operator's response.
|
|
246
|
+
"""
|
|
247
|
+
t0 = time.monotonic()
|
|
248
|
+
self._emit_dialog_opened(dialog)
|
|
249
|
+
|
|
250
|
+
# Check for auto-respond mode first
|
|
251
|
+
auto_response = self._get_auto_response(dialog)
|
|
252
|
+
if auto_response is not None:
|
|
253
|
+
self._emit_dialog_responded(dialog, auto_response, time.monotonic() - t0)
|
|
254
|
+
return auto_response
|
|
255
|
+
|
|
256
|
+
if self.server_url:
|
|
257
|
+
response = await self._show_http(dialog)
|
|
258
|
+
else:
|
|
259
|
+
response = await self._show_local(dialog)
|
|
260
|
+
|
|
261
|
+
self._emit_dialog_responded(dialog, response, time.monotonic() - t0)
|
|
262
|
+
return response
|
|
263
|
+
|
|
264
|
+
async def _show_http(self, dialog: Dialog) -> DialogResponse:
|
|
265
|
+
"""Show dialog via HTTP (for test subprocesses)."""
|
|
266
|
+
import httpx
|
|
267
|
+
|
|
268
|
+
timeout = dialog.timeout_seconds or 300
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
async with httpx.AsyncClient(timeout=timeout + 10) as client:
|
|
272
|
+
# POST the dialog to server. The server's create endpoint
|
|
273
|
+
# accepts the full Dialog shape; subclass-specific fields
|
|
274
|
+
# (choices, placeholder, etc.) flow through ``model_dump``.
|
|
275
|
+
create_resp = await client.post(
|
|
276
|
+
f"{self.server_url}/api/dialogs",
|
|
277
|
+
json=dialog.model_dump(mode="json"),
|
|
278
|
+
)
|
|
279
|
+
create_data = create_resp.json()
|
|
280
|
+
dialog_id = create_data.get("dialog_id", str(dialog.id))
|
|
281
|
+
|
|
282
|
+
# Wait for response
|
|
283
|
+
wait_resp = await client.get(
|
|
284
|
+
f"{self.server_url}/api/dialogs/{dialog_id}/wait",
|
|
285
|
+
params={"timeout": timeout},
|
|
286
|
+
)
|
|
287
|
+
resp_data = wait_resp.json()
|
|
288
|
+
except httpx.TimeoutException:
|
|
289
|
+
# Normalise transport-level timeout into the same response
|
|
290
|
+
# shape the local path produces, so callers (and the
|
|
291
|
+
# prompt-handler bridge) see one timeout contract.
|
|
292
|
+
return DialogResponse(dialog_id=dialog.id, timed_out=True)
|
|
293
|
+
|
|
294
|
+
return DialogResponse(
|
|
295
|
+
dialog_id=UUID(dialog_id),
|
|
296
|
+
confirmed=resp_data.get("confirmed", False),
|
|
297
|
+
choice=resp_data.get("choice"),
|
|
298
|
+
choices=resp_data.get("choices"),
|
|
299
|
+
value=resp_data.get("value"),
|
|
300
|
+
timed_out=resp_data.get("timed_out", False),
|
|
301
|
+
cancelled=resp_data.get("cancelled", False),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _register_pending(self, dialog: Dialog) -> None:
|
|
305
|
+
"""Add a dialog to pending state and notify listeners.
|
|
306
|
+
|
|
307
|
+
Shared by :meth:`_show_local` (which then awaits an asyncio
|
|
308
|
+
event) and :meth:`register_dialog` (which expects API-side
|
|
309
|
+
polling). DialogOpened emission is the caller's responsibility:
|
|
310
|
+
:meth:`show` already emits before dispatching to ``_show_local``;
|
|
311
|
+
``register_dialog`` emits explicitly because it has no
|
|
312
|
+
``show()`` parent.
|
|
313
|
+
"""
|
|
314
|
+
self._pending[dialog.id] = dialog
|
|
315
|
+
self._open_times[dialog.id] = time.monotonic()
|
|
316
|
+
self._notify_listeners(dialog)
|
|
317
|
+
|
|
318
|
+
async def _show_local(self, dialog: Dialog) -> DialogResponse:
|
|
319
|
+
"""Show dialog locally (in-process mode)."""
|
|
320
|
+
event = asyncio.Event()
|
|
321
|
+
self._register_pending(dialog)
|
|
322
|
+
self._events[dialog.id] = event
|
|
323
|
+
|
|
324
|
+
# Wait for response or timeout
|
|
325
|
+
if dialog.timeout_seconds:
|
|
326
|
+
try:
|
|
327
|
+
await asyncio.wait_for(event.wait(), timeout=dialog.timeout_seconds)
|
|
328
|
+
except TimeoutError:
|
|
329
|
+
response = DialogResponse(dialog_id=dialog.id, timed_out=True)
|
|
330
|
+
self._responses[dialog.id] = response
|
|
331
|
+
else:
|
|
332
|
+
await event.wait()
|
|
333
|
+
|
|
334
|
+
# Clean up and return response
|
|
335
|
+
self._pending.pop(dialog.id, None)
|
|
336
|
+
self._events.pop(dialog.id, None)
|
|
337
|
+
self._open_times.pop(dialog.id, None)
|
|
338
|
+
return self._responses.pop(dialog.id, DialogResponse(dialog_id=dialog.id, cancelled=True))
|
|
339
|
+
|
|
340
|
+
def register_dialog(self, dialog: Dialog) -> None:
|
|
341
|
+
"""Register a dialog created externally (e.g., from API).
|
|
342
|
+
|
|
343
|
+
This adds the dialog to pending without creating an asyncio event,
|
|
344
|
+
since the caller will poll for response via API.
|
|
345
|
+
"""
|
|
346
|
+
self._register_pending(dialog)
|
|
347
|
+
self._emit_dialog_opened(dialog)
|
|
348
|
+
|
|
349
|
+
def get_response(self, dialog_id: UUID) -> DialogResponse | None:
|
|
350
|
+
"""Get a response for a dialog if available.
|
|
351
|
+
|
|
352
|
+
Used by API to check if dialog has been responded to.
|
|
353
|
+
"""
|
|
354
|
+
return self._responses.get(dialog_id)
|
|
355
|
+
|
|
356
|
+
def respond(self, dialog_id: UUID, response: DialogResponse) -> bool:
|
|
357
|
+
"""Submit a response to a pending dialog.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
dialog_id: The dialog ID to respond to.
|
|
361
|
+
response: The operator's response.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
True if dialog was found and response recorded.
|
|
365
|
+
"""
|
|
366
|
+
if dialog_id not in self._pending:
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
dialog = self._pending[dialog_id]
|
|
370
|
+
|
|
371
|
+
# Emit responded event only for externally-registered dialogs
|
|
372
|
+
# (in-process dialogs have events emitted by show())
|
|
373
|
+
if dialog_id not in self._events:
|
|
374
|
+
t0 = self._open_times.pop(dialog_id, None)
|
|
375
|
+
duration = time.monotonic() - t0 if t0 is not None else 0.0
|
|
376
|
+
self._emit_dialog_responded(dialog, response, duration)
|
|
377
|
+
|
|
378
|
+
self._responses[dialog_id] = response
|
|
379
|
+
|
|
380
|
+
# Signal event if exists (in-process mode)
|
|
381
|
+
event = self._events.get(dialog_id)
|
|
382
|
+
if event:
|
|
383
|
+
event.set()
|
|
384
|
+
|
|
385
|
+
# Clean up pending for external dialogs (no event)
|
|
386
|
+
# Keep in _responses for polling
|
|
387
|
+
if dialog_id not in self._events:
|
|
388
|
+
self._pending.pop(dialog_id, None)
|
|
389
|
+
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
def get_pending_dialogs(self, run_id: str | None = None) -> list[Dialog]:
|
|
393
|
+
"""Get all pending dialogs, optionally filtered by run ID."""
|
|
394
|
+
if run_id is None:
|
|
395
|
+
return list(self._pending.values())
|
|
396
|
+
return [d for d in self._pending.values() if d.run_id == run_id]
|
|
397
|
+
|
|
398
|
+
def get_pending_dialog(self, run_id: str | None = None) -> Dialog | None:
|
|
399
|
+
"""Get the first pending dialog for a run."""
|
|
400
|
+
dialogs = self.get_pending_dialogs(run_id)
|
|
401
|
+
return dialogs[0] if dialogs else None
|
|
402
|
+
|
|
403
|
+
# Convenience methods for common dialog types
|
|
404
|
+
|
|
405
|
+
async def confirm(
|
|
406
|
+
self,
|
|
407
|
+
message: str,
|
|
408
|
+
title: str = "Confirm",
|
|
409
|
+
run_id: str | None = None,
|
|
410
|
+
step_name: str | None = None,
|
|
411
|
+
timeout: float | None = None,
|
|
412
|
+
) -> DialogResponse:
|
|
413
|
+
"""Show a confirmation dialog."""
|
|
414
|
+
dialog = ConfirmDialog(
|
|
415
|
+
title=title,
|
|
416
|
+
message=message,
|
|
417
|
+
run_id=run_id,
|
|
418
|
+
step_name=step_name,
|
|
419
|
+
timeout_seconds=timeout,
|
|
420
|
+
)
|
|
421
|
+
return await self.show(dialog)
|
|
422
|
+
|
|
423
|
+
async def choose(
|
|
424
|
+
self,
|
|
425
|
+
message: str,
|
|
426
|
+
choices: list[str],
|
|
427
|
+
title: str = "Select",
|
|
428
|
+
run_id: str | None = None,
|
|
429
|
+
step_name: str | None = None,
|
|
430
|
+
allow_multiple: bool = False,
|
|
431
|
+
timeout: float | None = None,
|
|
432
|
+
) -> DialogResponse:
|
|
433
|
+
"""Show a choice selection dialog."""
|
|
434
|
+
dialog = ChoiceDialog(
|
|
435
|
+
title=title,
|
|
436
|
+
message=message,
|
|
437
|
+
choices=choices,
|
|
438
|
+
allow_multiple=allow_multiple,
|
|
439
|
+
run_id=run_id,
|
|
440
|
+
step_name=step_name,
|
|
441
|
+
timeout_seconds=timeout,
|
|
442
|
+
)
|
|
443
|
+
return await self.show(dialog)
|
|
444
|
+
|
|
445
|
+
async def input(
|
|
446
|
+
self,
|
|
447
|
+
message: str,
|
|
448
|
+
title: str = "Input",
|
|
449
|
+
placeholder: str = "",
|
|
450
|
+
default_value: str = "",
|
|
451
|
+
run_id: str | None = None,
|
|
452
|
+
step_name: str | None = None,
|
|
453
|
+
timeout: float | None = None,
|
|
454
|
+
) -> DialogResponse:
|
|
455
|
+
"""Show a text input dialog."""
|
|
456
|
+
dialog = InputDialog(
|
|
457
|
+
title=title,
|
|
458
|
+
message=message,
|
|
459
|
+
placeholder=placeholder,
|
|
460
|
+
default_value=default_value,
|
|
461
|
+
run_id=run_id,
|
|
462
|
+
step_name=step_name,
|
|
463
|
+
timeout_seconds=timeout,
|
|
464
|
+
)
|
|
465
|
+
return await self.show(dialog)
|
|
466
|
+
|
|
467
|
+
async def show_image(
|
|
468
|
+
self,
|
|
469
|
+
message: str,
|
|
470
|
+
image_url: str | None = None,
|
|
471
|
+
image_path: str | None = None,
|
|
472
|
+
title: str = "Image",
|
|
473
|
+
run_id: str | None = None,
|
|
474
|
+
step_name: str | None = None,
|
|
475
|
+
timeout: float | None = None,
|
|
476
|
+
) -> DialogResponse:
|
|
477
|
+
"""Show an image dialog."""
|
|
478
|
+
dialog = ImageDialog(
|
|
479
|
+
title=title,
|
|
480
|
+
message=message,
|
|
481
|
+
image_url=image_url,
|
|
482
|
+
image_path=image_path,
|
|
483
|
+
run_id=run_id,
|
|
484
|
+
step_name=step_name,
|
|
485
|
+
timeout_seconds=timeout,
|
|
486
|
+
)
|
|
487
|
+
return await self.show(dialog)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# Global dialog manager instance
|
|
491
|
+
_manager: DialogManager | None = None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def get_dialog_manager(
|
|
495
|
+
server_url: str | None = None,
|
|
496
|
+
*,
|
|
497
|
+
auto_respond: str | None = None,
|
|
498
|
+
) -> DialogManager:
|
|
499
|
+
"""Get or create the global dialog manager.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
server_url: Optional server URL for HTTP mode. If not provided,
|
|
503
|
+
checks TESTERKIT_SERVER_URL environment variable. If neither
|
|
504
|
+
is set, uses in-process mode.
|
|
505
|
+
auto_respond: Auto-respond mode (``"confirm"`` / ``"cancel"``).
|
|
506
|
+
Falls back to ``TESTERKIT_AUTO_CONFIRM`` env var per
|
|
507
|
+
:meth:`DialogManager._get_auto_response`. Only honored on
|
|
508
|
+
first call — subsequent calls return the cached manager.
|
|
509
|
+
|
|
510
|
+
Environment variables:
|
|
511
|
+
TESTERKIT_SERVER_URL: Server URL for HTTP mode
|
|
512
|
+
TESTERKIT_AUTO_CONFIRM: Truthy → auto-confirm dialogs ("confirm" by
|
|
513
|
+
default; "cancel" / "skip" supported for explicit control)
|
|
514
|
+
"""
|
|
515
|
+
global _manager
|
|
516
|
+
if _manager is None:
|
|
517
|
+
url = server_url or os.environ.get("TESTERKIT_SERVER_URL")
|
|
518
|
+
_manager = DialogManager(server_url=url, auto_respond=auto_respond)
|
|
519
|
+
return _manager
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------------------
|
|
523
|
+
# Prompt-handler bridge
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
#
|
|
526
|
+
# A test runner sees one entry point — :func:`testerkit.prompts.ask`. When the
|
|
527
|
+
# UI / API server is running, that entry point should route through the
|
|
528
|
+
# dialog surface instead of the default TTY/auto-confirm chain.
|
|
529
|
+
# ``register_as_prompt_handler`` installs the bridge: ``prompts.ask`` from
|
|
530
|
+
# the test process becomes a synchronous call that drives the DialogManager
|
|
531
|
+
# under a private event loop and translates ``DialogResponse`` back into the
|
|
532
|
+
# value :func:`prompts.ask` is expected to return.
|
|
533
|
+
#
|
|
534
|
+
# Cancellation / timeout surface as :class:`PromptUnavailableError` so the
|
|
535
|
+
# caller sees the same failure shape it would from any other prompt path.
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def register_as_prompt_handler(server_url: str | None = None) -> None:
|
|
539
|
+
"""Install :class:`DialogManager` as the active prompt handler.
|
|
540
|
+
|
|
541
|
+
Call this from UI / API startup once the manager is wired up. After
|
|
542
|
+
registration, :func:`testerkit.prompts.ask` from the test process
|
|
543
|
+
dispatches through dialogs instead of the TTY / auto-confirm chain.
|
|
544
|
+
|
|
545
|
+
Mode requirements:
|
|
546
|
+
|
|
547
|
+
* **In-process mode** — caller and DialogManager live in the same
|
|
548
|
+
process (e.g., NiceGUI app driving its own tests). ``server_url``
|
|
549
|
+
can be ``None``; the manager's pending-dialog dict is shared
|
|
550
|
+
directly.
|
|
551
|
+
* **HTTP mode** — caller is a test subprocess and the UI server is a
|
|
552
|
+
separate process. Pass ``server_url`` (or set
|
|
553
|
+
``TESTERKIT_SERVER_URL``) so the bridge can POST dialogs to the
|
|
554
|
+
server and poll for the response. Without ``server_url``, the
|
|
555
|
+
test subprocess will silently never resolve its prompts because
|
|
556
|
+
its in-process manager has no UI listener attached.
|
|
557
|
+
|
|
558
|
+
Caller is responsible for picking the right mode for its
|
|
559
|
+
deployment; the bridge does not auto-detect.
|
|
560
|
+
"""
|
|
561
|
+
from testerkit.prompts import set_prompt_handler
|
|
562
|
+
|
|
563
|
+
set_prompt_handler(lambda cfg: _dispatch_prompt(cfg, server_url))
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _dispatch_prompt(config: Any, server_url: str | None) -> Any:
|
|
567
|
+
"""Sync bridge that runs an async dialog show and unwraps the response."""
|
|
568
|
+
from testerkit.prompts import PromptUnavailableError
|
|
569
|
+
from testerkit.prompts.core import select_value
|
|
570
|
+
|
|
571
|
+
manager = get_dialog_manager(server_url)
|
|
572
|
+
if config.prompt_type == "confirm":
|
|
573
|
+
coro = manager.confirm(config.message, timeout=config.timeout_seconds)
|
|
574
|
+
elif config.prompt_type == "choice":
|
|
575
|
+
choices = config.choices or []
|
|
576
|
+
if not choices:
|
|
577
|
+
raise ValueError("choice prompt requires non-empty choices")
|
|
578
|
+
coro = manager.choose(config.message, choices, timeout=config.timeout_seconds)
|
|
579
|
+
elif config.prompt_type == "input":
|
|
580
|
+
coro = manager.input(config.message, timeout=config.timeout_seconds)
|
|
581
|
+
else:
|
|
582
|
+
raise ValueError(f"unknown prompt_type: {config.prompt_type!r}")
|
|
583
|
+
|
|
584
|
+
response = _run_sync(coro)
|
|
585
|
+
if response.cancelled or response.timed_out:
|
|
586
|
+
raise PromptUnavailableError(
|
|
587
|
+
f"Dialog for {config.message!r} {'timed out' if response.timed_out else 'cancelled'}."
|
|
588
|
+
)
|
|
589
|
+
return select_value(
|
|
590
|
+
config,
|
|
591
|
+
confirmed=response.confirmed,
|
|
592
|
+
choice=response.choice,
|
|
593
|
+
value=response.value,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _run_sync(coro: Any) -> Any:
|
|
598
|
+
"""Run *coro* to completion from a synchronous context.
|
|
599
|
+
|
|
600
|
+
``asyncio.run`` raises if a loop is already running — a real concern
|
|
601
|
+
if a caller invokes :func:`prompts.ask` from inside an async fixture
|
|
602
|
+
or test. Detect a running loop and surface a clearer error than
|
|
603
|
+
pytest's default ``RuntimeError`` so the caller knows to switch to
|
|
604
|
+
awaiting the manager directly.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
asyncio.get_running_loop()
|
|
608
|
+
except RuntimeError:
|
|
609
|
+
# No loop in this thread — safe to run.
|
|
610
|
+
return asyncio.run(coro)
|
|
611
|
+
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
"prompts.ask() was invoked from inside a running event loop. "
|
|
614
|
+
"Async callers should await DialogManager methods directly "
|
|
615
|
+
"(get_dialog_manager().confirm(...) etc.) rather than going "
|
|
616
|
+
"through the sync prompts.ask() bridge."
|
|
617
|
+
)
|