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.
Files changed (521) hide show
  1. testerkit/__init__.py +68 -0
  2. testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
  3. testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
  4. testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
  5. testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
  6. testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
  7. testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
  8. testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
  9. testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
  10. testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
  11. testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
  12. testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
  13. testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
  14. testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
  15. testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
  16. testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
  17. testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
  18. testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
  19. testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
  20. testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
  21. testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
  22. testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
  23. testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
  24. testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
  25. testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
  26. testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
  27. testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
  28. testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
  29. testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
  30. testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
  31. testerkit/_docs/concepts/configuration/capabilities.md +447 -0
  32. testerkit/_docs/concepts/configuration/fixtures.md +335 -0
  33. testerkit/_docs/concepts/configuration/index.md +14 -0
  34. testerkit/_docs/concepts/configuration/parts.md +276 -0
  35. testerkit/_docs/concepts/configuration/stations.md +214 -0
  36. testerkit/_docs/concepts/data/data-stores.md +199 -0
  37. testerkit/_docs/concepts/data/event-log.md +185 -0
  38. testerkit/_docs/concepts/data/event-sourcing.md +76 -0
  39. testerkit/_docs/concepts/data/flight-streaming.md +58 -0
  40. testerkit/_docs/concepts/data/index.md +17 -0
  41. testerkit/_docs/concepts/data/sessions.md +78 -0
  42. testerkit/_docs/concepts/data/three-verbs.md +260 -0
  43. testerkit/_docs/concepts/execution/index.md +13 -0
  44. testerkit/_docs/concepts/execution/outcomes.md +199 -0
  45. testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
  46. testerkit/_docs/concepts/execution/step-manifest.md +115 -0
  47. testerkit/_docs/concepts/index.md +40 -0
  48. testerkit/_docs/concepts/overview/ai-integration.md +57 -0
  49. testerkit/_docs/concepts/overview/architecture.md +357 -0
  50. testerkit/_docs/concepts/overview/index.md +14 -0
  51. testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
  52. testerkit/_docs/concepts/overview/pytest.md +53 -0
  53. testerkit/_docs/concepts/overview/tiers.md +46 -0
  54. testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
  55. testerkit/_docs/how-to/catalog/index.md +11 -0
  56. testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
  57. testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
  58. testerkit/_docs/how-to/configuration/index.md +13 -0
  59. testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
  60. testerkit/_docs/how-to/data/benchmarking.md +120 -0
  61. testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
  62. testerkit/_docs/how-to/data/capture-waveform.md +110 -0
  63. testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
  64. testerkit/_docs/how-to/data/compare-runs.md +120 -0
  65. testerkit/_docs/how-to/data/export-results.md +104 -0
  66. testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
  67. testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
  68. testerkit/_docs/how-to/data/index.md +23 -0
  69. testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
  70. testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
  71. testerkit/_docs/how-to/data/querying-channels.md +117 -0
  72. testerkit/_docs/how-to/data/querying-events.md +85 -0
  73. testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
  74. testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
  75. testerkit/_docs/how-to/execution/index.md +21 -0
  76. testerkit/_docs/how-to/execution/limits.md +193 -0
  77. testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
  78. testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
  79. testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
  80. testerkit/_docs/how-to/execution/profiles.md +304 -0
  81. testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
  82. testerkit/_docs/how-to/execution/test-context.md +212 -0
  83. testerkit/_docs/how-to/execution/traceability.md +252 -0
  84. testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
  85. testerkit/_docs/how-to/execution/writing-tests.md +363 -0
  86. testerkit/_docs/how-to/index.md +59 -0
  87. testerkit/_docs/how-to/overview/index.md +11 -0
  88. testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
  89. testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
  90. testerkit/_docs/integration/data/grafana.md +162 -0
  91. testerkit/_docs/integration/data/index.md +15 -0
  92. testerkit/_docs/integration/data/lakehouse-import.md +226 -0
  93. testerkit/_docs/integration/data/logging.md +131 -0
  94. testerkit/_docs/integration/data/results-api.md +51 -0
  95. testerkit/_docs/integration/index.md +20 -0
  96. testerkit/_docs/integration/runtime/harness.md +281 -0
  97. testerkit/_docs/integration/runtime/index.md +13 -0
  98. testerkit/_docs/integration/runtime/instruments.md +284 -0
  99. testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
  100. testerkit/_docs/reference/catalog/cookbook.md +389 -0
  101. testerkit/_docs/reference/catalog/index.md +11 -0
  102. testerkit/_docs/reference/catalog/schema.md +375 -0
  103. testerkit/_docs/reference/cli.md +660 -0
  104. testerkit/_docs/reference/configuration.md +406 -0
  105. testerkit/_docs/reference/data/channels-schema.md +145 -0
  106. testerkit/_docs/reference/data/event-types.md +554 -0
  107. testerkit/_docs/reference/data/events-schema.md +120 -0
  108. testerkit/_docs/reference/data/files-schema.md +141 -0
  109. testerkit/_docs/reference/data/index.md +18 -0
  110. testerkit/_docs/reference/data/models.md +2229 -0
  111. testerkit/_docs/reference/data/outputs.md +82 -0
  112. testerkit/_docs/reference/data/parquet-schema.md +529 -0
  113. testerkit/_docs/reference/data/performance-limits.md +224 -0
  114. testerkit/_docs/reference/data/query-api.md +247 -0
  115. testerkit/_docs/reference/index.md +69 -0
  116. testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
  117. testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
  118. testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
  119. testerkit/_docs/reference/operator-ui/designer.md +167 -0
  120. testerkit/_docs/reference/operator-ui/events.md +89 -0
  121. testerkit/_docs/reference/operator-ui/files.md +74 -0
  122. testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
  123. testerkit/_docs/reference/operator-ui/instruments.md +126 -0
  124. testerkit/_docs/reference/operator-ui/launch.md +79 -0
  125. testerkit/_docs/reference/operator-ui/live.md +102 -0
  126. testerkit/_docs/reference/operator-ui/measurements.md +159 -0
  127. testerkit/_docs/reference/operator-ui/metrics.md +217 -0
  128. testerkit/_docs/reference/operator-ui/parts.md +98 -0
  129. testerkit/_docs/reference/operator-ui/profiles.md +103 -0
  130. testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
  131. testerkit/_docs/reference/operator-ui/results/list.md +106 -0
  132. testerkit/_docs/reference/operator-ui/stations.md +98 -0
  133. testerkit/_docs/reference/operator-ui/tests.md +173 -0
  134. testerkit/_docs/reference/operator-ui/uuts.md +67 -0
  135. testerkit/_docs/reference/overview/index.md +11 -0
  136. testerkit/_docs/reference/overview/pytest-native.md +142 -0
  137. testerkit/_docs/reference/overview/skills.md +177 -0
  138. testerkit/_docs/reference/pytest/fixtures.md +342 -0
  139. testerkit/_docs/reference/pytest/index.md +12 -0
  140. testerkit/_docs/reference/pytest/markers.md +257 -0
  141. testerkit/_docs/reference/runtime/api.md +246 -0
  142. testerkit/_docs/reference/runtime/client.md +280 -0
  143. testerkit/_docs/reference/runtime/connect.md +140 -0
  144. testerkit/_docs/reference/runtime/index.md +13 -0
  145. testerkit/_docs/tutorial/01-first-test.md +113 -0
  146. testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
  147. testerkit/_docs/tutorial/03-fixtures.md +169 -0
  148. testerkit/_docs/tutorial/04-limits.md +139 -0
  149. testerkit/_docs/tutorial/05-configuration.md +194 -0
  150. testerkit/_docs/tutorial/06-specifications.md +273 -0
  151. testerkit/_docs/tutorial/07-real-instruments.md +245 -0
  152. testerkit/_docs/tutorial/08-capabilities.md +223 -0
  153. testerkit/_docs/tutorial/09-production.md +374 -0
  154. testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
  155. testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
  156. testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
  157. testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
  158. testerkit/_docs/tutorial/index.md +61 -0
  159. testerkit/_docs/tutorial/quickstart.md +282 -0
  160. testerkit/analysis/__init__.py +9 -0
  161. testerkit/analysis/_common.py +24 -0
  162. testerkit/analysis/measurement_facets.py +524 -0
  163. testerkit/analysis/measurements_query.py +1458 -0
  164. testerkit/analysis/metrics.py +389 -0
  165. testerkit/analysis/runs_query.py +434 -0
  166. testerkit/analysis/steps_query.py +413 -0
  167. testerkit/api/__init__.py +6 -0
  168. testerkit/api/_mime.py +71 -0
  169. testerkit/api/app.py +1171 -0
  170. testerkit/api/dialogs/__init__.py +36 -0
  171. testerkit/api/dialogs/manager.py +617 -0
  172. testerkit/api/dialogs/models.py +79 -0
  173. testerkit/api/models.py +80 -0
  174. testerkit/api/responses.py +205 -0
  175. testerkit/api/runner.py +222 -0
  176. testerkit/api/schemas.py +311 -0
  177. testerkit/benchmark/__init__.py +34 -0
  178. testerkit/benchmark/concurrency.py +248 -0
  179. testerkit/benchmark/core.py +232 -0
  180. testerkit/benchmark/runner.py +628 -0
  181. testerkit/benchmark/scenario.py +313 -0
  182. testerkit/benchmark/system.py +191 -0
  183. testerkit/benchmark/workloads.py +507 -0
  184. testerkit/catalog/__init__.py +6 -0
  185. testerkit/catalog/generic/generic_dmm.yaml +61 -0
  186. testerkit/catalog/generic/generic_eload.yaml +31 -0
  187. testerkit/catalog/generic/generic_fgen.yaml +56 -0
  188. testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
  189. testerkit/catalog/generic/generic_psu.yaml +77 -0
  190. testerkit/catalog/generic/generic_smu.yaml +73 -0
  191. testerkit/channels.py +538 -0
  192. testerkit/cli/__init__.py +32 -0
  193. testerkit/cli/_common.py +113 -0
  194. testerkit/cli/_time.py +179 -0
  195. testerkit/cli/benchmark_cmd.py +56 -0
  196. testerkit/cli/catalog_cmd.py +39 -0
  197. testerkit/cli/daemon.py +157 -0
  198. testerkit/cli/data_cmd.py +1040 -0
  199. testerkit/cli/discover_cmd.py +94 -0
  200. testerkit/cli/docs_cmd.py +100 -0
  201. testerkit/cli/instrument.py +172 -0
  202. testerkit/cli/mcp_cmd.py +37 -0
  203. testerkit/cli/metrics.py +366 -0
  204. testerkit/cli/project.py +312 -0
  205. testerkit/cli/root.py +14 -0
  206. testerkit/cli/runs.py +412 -0
  207. testerkit/cli/schema_cmd.py +87 -0
  208. testerkit/cli/serve_cmd.py +50 -0
  209. testerkit/cli/setup_cmd.py +604 -0
  210. testerkit/cli/station.py +294 -0
  211. testerkit/cli/validate.py +112 -0
  212. testerkit/client.py +463 -0
  213. testerkit/connect.py +635 -0
  214. testerkit/data/__init__.py +8 -0
  215. testerkit/data/_accumulator_pool.py +455 -0
  216. testerkit/data/_atomic.py +59 -0
  217. testerkit/data/_collection_indices.py +67 -0
  218. testerkit/data/_daemon_lifecycle.py +535 -0
  219. testerkit/data/_duckdb_daemon.py +695 -0
  220. testerkit/data/_duckdb_flight_server.py +813 -0
  221. testerkit/data/_event_filters.py +15 -0
  222. testerkit/data/_event_reader.py +101 -0
  223. testerkit/data/_flight_errors.py +199 -0
  224. testerkit/data/_flight_query.py +202 -0
  225. testerkit/data/_flight_retry.py +148 -0
  226. testerkit/data/_flight_subscribe.py +67 -0
  227. testerkit/data/_index_epoch.py +342 -0
  228. testerkit/data/_ipc_writer.py +185 -0
  229. testerkit/data/_json_safe.py +63 -0
  230. testerkit/data/_process.py +18 -0
  231. testerkit/data/_push_relay.py +124 -0
  232. testerkit/data/_runs_duckdb_daemon.py +2704 -0
  233. testerkit/data/_session_reaper.py +110 -0
  234. testerkit/data/_sql_helpers.py +30 -0
  235. testerkit/data/_store.py +24 -0
  236. testerkit/data/backends/__init__.py +7 -0
  237. testerkit/data/backends/_event_accumulator.py +957 -0
  238. testerkit/data/backends/_row_helpers.py +1172 -0
  239. testerkit/data/backends/parquet.py +1143 -0
  240. testerkit/data/backends/protocol.py +35 -0
  241. testerkit/data/channels/__init__.py +8 -0
  242. testerkit/data/channels/_flight_daemon.py +75 -0
  243. testerkit/data/channels/client.py +242 -0
  244. testerkit/data/channels/flight_manager.py +88 -0
  245. testerkit/data/channels/index.py +664 -0
  246. testerkit/data/channels/models.py +418 -0
  247. testerkit/data/channels/server.py +162 -0
  248. testerkit/data/channels/store.py +1481 -0
  249. testerkit/data/data_dir.py +61 -0
  250. testerkit/data/duckdb_manager.py +79 -0
  251. testerkit/data/event_log.py +384 -0
  252. testerkit/data/event_store.py +768 -0
  253. testerkit/data/events.py +1025 -0
  254. testerkit/data/exporters/__init__.py +6 -0
  255. testerkit/data/exporters/_helpers.py +40 -0
  256. testerkit/data/exporters/csv_exporter.py +168 -0
  257. testerkit/data/exporters/hdf5.py +234 -0
  258. testerkit/data/exporters/json_exporter.py +229 -0
  259. testerkit/data/exporters/mdf4.py +201 -0
  260. testerkit/data/exporters/stdf.py +255 -0
  261. testerkit/data/exporters/tdms.py +292 -0
  262. testerkit/data/files/__init__.py +71 -0
  263. testerkit/data/files/_backend.py +207 -0
  264. testerkit/data/files/_catalog_daemon.py +84 -0
  265. testerkit/data/files/catalog.py +218 -0
  266. testerkit/data/files/catalog_manager.py +325 -0
  267. testerkit/data/files/models.py +60 -0
  268. testerkit/data/files/serializers.py +414 -0
  269. testerkit/data/files/store.py +463 -0
  270. testerkit/data/files/streaming.py +783 -0
  271. testerkit/data/models.py +604 -0
  272. testerkit/data/ref.py +152 -0
  273. testerkit/data/retention.py +297 -0
  274. testerkit/data/run_store.py +367 -0
  275. testerkit/data/runs_duckdb_manager.py +103 -0
  276. testerkit/data/schema_dispatch.py +191 -0
  277. testerkit/data/schema_migrate.py +76 -0
  278. testerkit/data/schema_versions.py +85 -0
  279. testerkit/data/schemas.py +288 -0
  280. testerkit/data/subscribers/__init__.py +48 -0
  281. testerkit/data/subscribers/_base.py +20 -0
  282. testerkit/data/subscribers/_output_file.py +25 -0
  283. testerkit/data/subscribers/replay.py +55 -0
  284. testerkit/environment.py +84 -0
  285. testerkit/execution/__init__.py +11 -0
  286. testerkit/execution/_git.py +190 -0
  287. testerkit/execution/_state.py +740 -0
  288. testerkit/execution/accessors.py +54 -0
  289. testerkit/execution/audit.py +51 -0
  290. testerkit/execution/cascade.py +85 -0
  291. testerkit/execution/connections.py +491 -0
  292. testerkit/execution/harness.py +1931 -0
  293. testerkit/execution/instrument_events.py +42 -0
  294. testerkit/execution/limits.py +154 -0
  295. testerkit/execution/metadata.py +102 -0
  296. testerkit/execution/mocks.py +63 -0
  297. testerkit/execution/profiles.py +616 -0
  298. testerkit/execution/run_scope.py +1331 -0
  299. testerkit/execution/session_scope.py +196 -0
  300. testerkit/execution/sidecar.py +280 -0
  301. testerkit/execution/site_runner.py +705 -0
  302. testerkit/execution/sites.py +183 -0
  303. testerkit/execution/sync.py +294 -0
  304. testerkit/execution/uut_provider.py +231 -0
  305. testerkit/execution/vectors.py +205 -0
  306. testerkit/execution/verify.py +396 -0
  307. testerkit/expand.py +75 -0
  308. testerkit/files.py +235 -0
  309. testerkit/fixtures/__init__.py +6 -0
  310. testerkit/fixtures/manager.py +357 -0
  311. testerkit/grafana/__init__.py +1 -0
  312. testerkit/grafana/bootstrap.py +61 -0
  313. testerkit/grafana/cli.py +314 -0
  314. testerkit/grafana/dashboards/asset_utilization.json +92 -0
  315. testerkit/grafana/dashboards/channel_explorer.json +77 -0
  316. testerkit/grafana/dashboards/event_log.json +92 -0
  317. testerkit/grafana/dashboards/failure_pareto.json +104 -0
  318. testerkit/grafana/dashboards/measurement_distribution.json +97 -0
  319. testerkit/grafana/dashboards/measurement_trend.json +161 -0
  320. testerkit/grafana/dashboards/station_comparison.json +95 -0
  321. testerkit/grafana/dashboards/test_duration.json +104 -0
  322. testerkit/grafana/dashboards/unit_traceability.json +73 -0
  323. testerkit/grafana/dashboards/yield_overview.json +189 -0
  324. testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
  325. testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
  326. testerkit/grafana/server.py +282 -0
  327. testerkit/init.py +724 -0
  328. testerkit/instruments/__init__.py +10 -0
  329. testerkit/instruments/base.py +82 -0
  330. testerkit/instruments/discovery/__init__.py +62 -0
  331. testerkit/instruments/discovery/_base.py +174 -0
  332. testerkit/instruments/discovery/lxi.py +152 -0
  333. testerkit/instruments/discovery/ni.py +95 -0
  334. testerkit/instruments/discovery/serial.py +73 -0
  335. testerkit/instruments/discovery/visa.py +98 -0
  336. testerkit/instruments/lifecycle.py +213 -0
  337. testerkit/instruments/loader.py +79 -0
  338. testerkit/instruments/locks.py +178 -0
  339. testerkit/instruments/mocks.py +230 -0
  340. testerkit/instruments/observer.py +270 -0
  341. testerkit/instruments/observers/__init__.py +36 -0
  342. testerkit/instruments/observers/_base.py +68 -0
  343. testerkit/instruments/observers/daqmx.py +70 -0
  344. testerkit/instruments/observers/descriptor.py +69 -0
  345. testerkit/instruments/observers/generic.py +92 -0
  346. testerkit/instruments/observers/lantz.py +31 -0
  347. testerkit/instruments/observers/modbus.py +96 -0
  348. testerkit/instruments/observers/motion.py +68 -0
  349. testerkit/instruments/observers/ni_modular.py +73 -0
  350. testerkit/instruments/observers/ophyd.py +80 -0
  351. testerkit/instruments/observers/pymeasure.py +120 -0
  352. testerkit/instruments/observers/qcodes.py +66 -0
  353. testerkit/instruments/observers/scpi.py +88 -0
  354. testerkit/instruments/observers/tektronix.py +57 -0
  355. testerkit/instruments/observers/visa.py +103 -0
  356. testerkit/instruments/pool.py +459 -0
  357. testerkit/instruments/proxy.py +59 -0
  358. testerkit/instruments/route_manager.py +329 -0
  359. testerkit/instruments/routed_proxy.py +88 -0
  360. testerkit/instruments/server.py +508 -0
  361. testerkit/instruments/switch.py +43 -0
  362. testerkit/instruments/visa.py +294 -0
  363. testerkit/matching/__init__.py +6 -0
  364. testerkit/matching/service.py +969 -0
  365. testerkit/mcp/__init__.py +6 -0
  366. testerkit/mcp/server.py +681 -0
  367. testerkit/mcp/tools.py +1868 -0
  368. testerkit/models/__init__.py +18 -0
  369. testerkit/models/capability.py +598 -0
  370. testerkit/models/catalog.py +76 -0
  371. testerkit/models/data_options.py +153 -0
  372. testerkit/models/enums.py +334 -0
  373. testerkit/models/instrument.py +152 -0
  374. testerkit/models/instrument_asset.py +25 -0
  375. testerkit/models/part.py +271 -0
  376. testerkit/models/part_manifest.py +96 -0
  377. testerkit/models/project.py +114 -0
  378. testerkit/models/station.py +140 -0
  379. testerkit/models/test_config.py +748 -0
  380. testerkit/ontology/__init__.py +39 -0
  381. testerkit/ontology/schema.py +188 -0
  382. testerkit/ontology/testerkit.yaml +1420 -0
  383. testerkit/parts/__init__.py +8 -0
  384. testerkit/parts/context.py +181 -0
  385. testerkit/parts/folder.py +251 -0
  386. testerkit/parts/loader.py +27 -0
  387. testerkit/prompts/__init__.py +17 -0
  388. testerkit/prompts/core.py +133 -0
  389. testerkit/pytest_plugin/__init__.py +1287 -0
  390. testerkit/pytest_plugin/autouse.py +369 -0
  391. testerkit/pytest_plugin/helpers.py +354 -0
  392. testerkit/pytest_plugin/hooks.py +1982 -0
  393. testerkit/pytest_plugin/markers.py +203 -0
  394. testerkit/pytest_plugin/retry.py +37 -0
  395. testerkit/pytest_plugin/sweeps.py +117 -0
  396. testerkit/queries.py +78 -0
  397. testerkit/reports/__init__.py +6 -0
  398. testerkit/reports/core.py +492 -0
  399. testerkit/reports/datasheet.py +831 -0
  400. testerkit/reports/templates/datasheet.html +383 -0
  401. testerkit/reports/templates/default.html +153 -0
  402. testerkit/sbom.py +142 -0
  403. testerkit/schema_export.py +101 -0
  404. testerkit/signals.py +63 -0
  405. testerkit/skills/README.md +58 -0
  406. testerkit/skills/templates/project-instructions.md +125 -0
  407. testerkit/skills/testerkit-analysis/SKILL.md +87 -0
  408. testerkit/skills/testerkit-capture/SKILL.md +114 -0
  409. testerkit/skills/testerkit-data/SKILL.md +138 -0
  410. testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
  411. testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
  412. testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
  413. testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
  414. testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
  415. testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
  416. testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
  417. testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
  418. testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
  419. testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
  420. testerkit/skills/testerkit-debug/SKILL.md +127 -0
  421. testerkit/skills/testerkit-interactive/SKILL.md +188 -0
  422. testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
  423. testerkit/skills/testerkit-mocks/SKILL.md +128 -0
  424. testerkit/skills/testerkit-parts/SKILL.md +126 -0
  425. testerkit/skills/testerkit-profiles/SKILL.md +139 -0
  426. testerkit/skills/testerkit-sites/SKILL.md +140 -0
  427. testerkit/skills/testerkit-stations/SKILL.md +146 -0
  428. testerkit/skills/testerkit-tests/SKILL.md +136 -0
  429. testerkit/store.py +1639 -0
  430. testerkit/ui/__init__.py +71 -0
  431. testerkit/ui/_asgi.py +162 -0
  432. testerkit/ui/app.py +32 -0
  433. testerkit/ui/components/__init__.py +13 -0
  434. testerkit/ui/components/artifact_viewer.py +250 -0
  435. testerkit/ui/components/channel_values.py +159 -0
  436. testerkit/ui/components/event_timeline.py +222 -0
  437. testerkit/ui/components/execution_gantt.py +210 -0
  438. testerkit/ui/components/file_streams.py +147 -0
  439. testerkit/ui/components/instrument_activity.py +146 -0
  440. testerkit/ui/components/session_table.py +118 -0
  441. testerkit/ui/pages/__init__.py +25 -0
  442. testerkit/ui/pages/channels/__init__.py +4 -0
  443. testerkit/ui/pages/channels/detail.py +932 -0
  444. testerkit/ui/pages/channels/list.py +515 -0
  445. testerkit/ui/pages/dashboard.py +236 -0
  446. testerkit/ui/pages/designer/__init__.py +5 -0
  447. testerkit/ui/pages/designer/graph.py +635 -0
  448. testerkit/ui/pages/designer/matching.py +663 -0
  449. testerkit/ui/pages/designer/page.py +593 -0
  450. testerkit/ui/pages/designer/properties.py +591 -0
  451. testerkit/ui/pages/designer/state.py +468 -0
  452. testerkit/ui/pages/docs/__init__.py +6 -0
  453. testerkit/ui/pages/docs/index.py +95 -0
  454. testerkit/ui/pages/docs/page.py +698 -0
  455. testerkit/ui/pages/events/__init__.py +3 -0
  456. testerkit/ui/pages/events/list.py +263 -0
  457. testerkit/ui/pages/explore.py +1108 -0
  458. testerkit/ui/pages/files/__init__.py +4 -0
  459. testerkit/ui/pages/files/detail.py +426 -0
  460. testerkit/ui/pages/files/list.py +447 -0
  461. testerkit/ui/pages/fixtures/__init__.py +8 -0
  462. testerkit/ui/pages/fixtures/detail.py +280 -0
  463. testerkit/ui/pages/fixtures/edit.py +370 -0
  464. testerkit/ui/pages/fixtures/list.py +180 -0
  465. testerkit/ui/pages/fixtures/new.py +195 -0
  466. testerkit/ui/pages/instruments/__init__.py +4 -0
  467. testerkit/ui/pages/instruments/detail.py +320 -0
  468. testerkit/ui/pages/instruments/edit.py +482 -0
  469. testerkit/ui/pages/instruments/list.py +249 -0
  470. testerkit/ui/pages/instruments/new.py +200 -0
  471. testerkit/ui/pages/launch.py +261 -0
  472. testerkit/ui/pages/live.py +124 -0
  473. testerkit/ui/pages/metrics_page.py +1229 -0
  474. testerkit/ui/pages/parts/__init__.py +11 -0
  475. testerkit/ui/pages/parts/detail.py +202 -0
  476. testerkit/ui/pages/parts/edit.py +310 -0
  477. testerkit/ui/pages/parts/list.py +160 -0
  478. testerkit/ui/pages/parts/new.py +170 -0
  479. testerkit/ui/pages/parts/requirements.py +153 -0
  480. testerkit/ui/pages/parts/stations.py +199 -0
  481. testerkit/ui/pages/profiles/__init__.py +4 -0
  482. testerkit/ui/pages/profiles/detail.py +113 -0
  483. testerkit/ui/pages/profiles/list.py +86 -0
  484. testerkit/ui/pages/results/__init__.py +4 -0
  485. testerkit/ui/pages/results/detail.py +631 -0
  486. testerkit/ui/pages/results/list.py +230 -0
  487. testerkit/ui/pages/stations/__init__.py +4 -0
  488. testerkit/ui/pages/stations/detail.py +256 -0
  489. testerkit/ui/pages/stations/edit.py +273 -0
  490. testerkit/ui/pages/stations/list.py +180 -0
  491. testerkit/ui/pages/stations/new.py +311 -0
  492. testerkit/ui/pages/tests/__init__.py +4 -0
  493. testerkit/ui/pages/tests/detail.py +152 -0
  494. testerkit/ui/pages/tests/list.py +194 -0
  495. testerkit/ui/pages/uuts/__init__.py +4 -0
  496. testerkit/ui/pages/uuts/list.py +97 -0
  497. testerkit/ui/shared/__init__.py +8 -0
  498. testerkit/ui/shared/components.py +1826 -0
  499. testerkit/ui/shared/dialogs.py +105 -0
  500. testerkit/ui/shared/event_binding.py +194 -0
  501. testerkit/ui/shared/layout.py +314 -0
  502. testerkit/ui/shared/services.py +1719 -0
  503. testerkit/ui/shared/timestamps.py +21 -0
  504. testerkit/ui/static/branding/favicon-16.png +0 -0
  505. testerkit/ui/static/branding/favicon-32.png +0 -0
  506. testerkit/ui/static/branding/favicon-48.png +0 -0
  507. testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
  508. testerkit/ui/static/branding/testerkit-mark.svg +1 -0
  509. testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
  510. testerkit/ui/static/global.css +465 -0
  511. testerkit/utils/__init__.py +17 -0
  512. testerkit/utils/enum_meta.py +800 -0
  513. testerkit/utils/paths.py +75 -0
  514. testerkit/utils/ranges.py +257 -0
  515. testerkit/validation.py +82 -0
  516. testerkit/verbs.py +142 -0
  517. testerkit-0.4.0.dist-info/METADATA +250 -0
  518. testerkit-0.4.0.dist-info/RECORD +521 -0
  519. testerkit-0.4.0.dist-info/WHEEL +4 -0
  520. testerkit-0.4.0.dist-info/entry_points.txt +5 -0
  521. 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
+ )