tabwright 1.0.0 → 1.0.1

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 (562) hide show
  1. package/dist/aria-snapshot.d.ts +0 -1
  2. package/dist/aria-snapshot.js +9 -1
  3. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +0 -1
  4. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +0 -1
  5. package/dist/bippy.js +1 -1
  6. package/dist/browser-config.d.ts +0 -1
  7. package/dist/browser-config.js +0 -1
  8. package/dist/browser-install.d.ts +0 -1
  9. package/dist/browser-install.js +0 -1
  10. package/dist/browser-launch.d.ts +0 -1
  11. package/dist/browser-launch.js +0 -1
  12. package/dist/builtin-capabilities.d.ts +0 -1
  13. package/dist/builtin-capabilities.js +0 -1
  14. package/dist/capability-agent-skill.d.ts +0 -1
  15. package/dist/capability-agent-skill.js +0 -1
  16. package/dist/capability-auth-state.d.ts +0 -1
  17. package/dist/capability-auth-state.js +0 -1
  18. package/dist/capability-auth.d.ts +0 -1
  19. package/dist/capability-auth.js +0 -1
  20. package/dist/capability-options.d.ts +0 -1
  21. package/dist/capability-options.js +0 -1
  22. package/dist/capability-package.d.ts +0 -1
  23. package/dist/capability-package.js +0 -1
  24. package/dist/capability-registry.d.ts +0 -1
  25. package/dist/capability-registry.js +0 -1
  26. package/dist/capability-runner.d.ts +0 -1
  27. package/dist/capability-runner.js +0 -1
  28. package/dist/capability-studio.d.ts +0 -1
  29. package/dist/capability-studio.js +0 -1
  30. package/dist/cdp-log.d.ts +0 -1
  31. package/dist/cdp-log.js +0 -1
  32. package/dist/cdp-relay.d.ts +0 -1
  33. package/dist/cdp-relay.js +0 -1
  34. package/dist/cdp-session.d.ts +0 -1
  35. package/dist/cdp-session.js +0 -1
  36. package/dist/cdp-types.d.ts +0 -1
  37. package/dist/cdp-types.js +0 -1
  38. package/dist/chrome-discovery.d.ts +0 -1
  39. package/dist/chrome-discovery.js +0 -1
  40. package/dist/clean-html.d.ts +0 -1
  41. package/dist/clean-html.js +0 -1
  42. package/dist/cli.d.ts +0 -1
  43. package/dist/cli.js +9 -2
  44. package/dist/cloud-client.d.ts +0 -1
  45. package/dist/cloud-client.js +0 -1
  46. package/dist/create-logger.d.ts +0 -1
  47. package/dist/create-logger.js +0 -1
  48. package/dist/debugger-examples-types.d.ts +0 -1
  49. package/dist/debugger-examples-types.js +0 -1
  50. package/dist/debugger-examples.d.ts +0 -1
  51. package/dist/debugger-examples.js +0 -1
  52. package/dist/debugger.d.ts +0 -1
  53. package/dist/debugger.js +0 -1
  54. package/dist/diff-utils.d.ts +0 -1
  55. package/dist/diff-utils.js +0 -1
  56. package/dist/doctor.d.ts +0 -1
  57. package/dist/doctor.js +5 -7
  58. package/dist/editor-examples.d.ts +0 -1
  59. package/dist/editor-examples.js +0 -1
  60. package/dist/editor.d.ts +0 -1
  61. package/dist/editor.js +0 -1
  62. package/dist/env-compat.d.ts +0 -1
  63. package/dist/env-compat.js +0 -1
  64. package/dist/executor.d.ts +0 -1
  65. package/dist/executor.js +1 -2
  66. package/dist/extension/background.js +7 -3
  67. package/dist/extension/manifest.json +1 -1
  68. package/dist/ghost-browser.d.ts +0 -1
  69. package/dist/ghost-browser.js +0 -1
  70. package/dist/ghost-cursor-controller.d.ts +0 -1
  71. package/dist/ghost-cursor-controller.js +0 -1
  72. package/dist/ghost-cursor.d.ts +0 -1
  73. package/dist/ghost-cursor.js +0 -1
  74. package/dist/htmlrewrite.d.ts +0 -1
  75. package/dist/htmlrewrite.js +0 -1
  76. package/dist/index.d.ts +0 -1
  77. package/dist/index.js +0 -1
  78. package/dist/kill-port.d.ts +0 -1
  79. package/dist/kill-port.js +0 -1
  80. package/dist/kitty-graphics.d.ts +0 -1
  81. package/dist/kitty-graphics.js +0 -1
  82. package/dist/mcp.d.ts +0 -1
  83. package/dist/mcp.js +0 -1
  84. package/dist/package-paths.d.ts +0 -1
  85. package/dist/package-paths.js +0 -1
  86. package/dist/page-markdown.d.ts +0 -1
  87. package/dist/page-markdown.js +0 -1
  88. package/dist/performance-examples.d.ts +0 -1
  89. package/dist/performance-examples.js +0 -1
  90. package/dist/playwright-import.d.ts +0 -1
  91. package/dist/playwright-import.js +0 -1
  92. package/dist/product-paths.d.ts +0 -1
  93. package/dist/product-paths.js +0 -1
  94. package/dist/protocol.d.ts +0 -1
  95. package/dist/protocol.js +0 -1
  96. package/dist/react-source.d.ts +0 -1
  97. package/dist/react-source.js +0 -1
  98. package/dist/readability.js +1 -1
  99. package/dist/relay-client.d.ts +0 -1
  100. package/dist/relay-client.js +0 -1
  101. package/dist/relay-state.d.ts +0 -1
  102. package/dist/relay-state.js +0 -1
  103. package/dist/replay-ai-index.d.ts +0 -1
  104. package/dist/replay-ai-index.js +0 -1
  105. package/dist/replay-eval.d.ts +0 -1
  106. package/dist/replay-eval.js +0 -1
  107. package/dist/replay-handoff.d.ts +0 -1
  108. package/dist/replay-handoff.js +0 -1
  109. package/dist/replay-workflow-compiler.d.ts +0 -1
  110. package/dist/replay-workflow-compiler.js +0 -1
  111. package/dist/rrweb-recording-relay.d.ts +0 -1
  112. package/dist/rrweb-recording-relay.js +0 -1
  113. package/dist/rrweb-recording.d.ts +0 -1
  114. package/dist/rrweb-recording.js +0 -1
  115. package/dist/scoped-fs.d.ts +0 -1
  116. package/dist/scoped-fs.js +0 -1
  117. package/dist/selector-generator.js +1 -1
  118. package/dist/start-relay-server.d.ts +0 -1
  119. package/dist/start-relay-server.js +0 -1
  120. package/dist/styles-examples.d.ts +0 -1
  121. package/dist/styles-examples.js +0 -1
  122. package/dist/styles.d.ts +0 -1
  123. package/dist/styles.js +0 -1
  124. package/dist/tabwright-agent-skill.d.ts +0 -1
  125. package/dist/tabwright-agent-skill.js +0 -1
  126. package/dist/test-declarations.d.ts +0 -1
  127. package/dist/test-declarations.js +0 -1
  128. package/dist/utils.d.ts +0 -1
  129. package/dist/utils.js +2 -2
  130. package/dist/wait-for-page-load.d.ts +0 -1
  131. package/dist/wait-for-page-load.js +0 -1
  132. package/dist/workflow-capability.d.ts +0 -1
  133. package/dist/workflow-capability.js +0 -1
  134. package/package.json +9 -4
  135. package/readme.md +35 -0
  136. package/dist/aria-snapshot.d.ts.map +0 -1
  137. package/dist/aria-snapshot.js.map +0 -1
  138. package/dist/aria-snapshot.test.d.ts +0 -2
  139. package/dist/aria-snapshot.test.d.ts.map +0 -1
  140. package/dist/aria-snapshot.test.js +0 -270
  141. package/dist/aria-snapshot.test.js.map +0 -1
  142. package/dist/aria-snapshot.unit.test.d.ts +0 -2
  143. package/dist/aria-snapshot.unit.test.d.ts.map +0 -1
  144. package/dist/aria-snapshot.unit.test.js +0 -542
  145. package/dist/aria-snapshot.unit.test.js.map +0 -1
  146. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +0 -1
  147. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +0 -1
  148. package/dist/browser-config.d.ts.map +0 -1
  149. package/dist/browser-config.js.map +0 -1
  150. package/dist/browser-install.d.ts.map +0 -1
  151. package/dist/browser-install.js.map +0 -1
  152. package/dist/browser-launch.d.ts.map +0 -1
  153. package/dist/browser-launch.js.map +0 -1
  154. package/dist/builtin-capabilities.d.ts.map +0 -1
  155. package/dist/builtin-capabilities.js.map +0 -1
  156. package/dist/capability-agent-skill.d.ts.map +0 -1
  157. package/dist/capability-agent-skill.js.map +0 -1
  158. package/dist/capability-auth-state.d.ts.map +0 -1
  159. package/dist/capability-auth-state.js.map +0 -1
  160. package/dist/capability-auth.d.ts.map +0 -1
  161. package/dist/capability-auth.js.map +0 -1
  162. package/dist/capability-cli-confirmation.test.d.ts +0 -2
  163. package/dist/capability-cli-confirmation.test.d.ts.map +0 -1
  164. package/dist/capability-cli-confirmation.test.js +0 -79
  165. package/dist/capability-cli-confirmation.test.js.map +0 -1
  166. package/dist/capability-options.d.ts.map +0 -1
  167. package/dist/capability-options.js.map +0 -1
  168. package/dist/capability-options.test.d.ts +0 -2
  169. package/dist/capability-options.test.d.ts.map +0 -1
  170. package/dist/capability-options.test.js +0 -81
  171. package/dist/capability-options.test.js.map +0 -1
  172. package/dist/capability-package.d.ts.map +0 -1
  173. package/dist/capability-package.js.map +0 -1
  174. package/dist/capability-package.test.d.ts +0 -2
  175. package/dist/capability-package.test.d.ts.map +0 -1
  176. package/dist/capability-package.test.js +0 -179
  177. package/dist/capability-package.test.js.map +0 -1
  178. package/dist/capability-registry.d.ts.map +0 -1
  179. package/dist/capability-registry.js.map +0 -1
  180. package/dist/capability-registry.test.d.ts +0 -2
  181. package/dist/capability-registry.test.d.ts.map +0 -1
  182. package/dist/capability-registry.test.js +0 -1683
  183. package/dist/capability-registry.test.js.map +0 -1
  184. package/dist/capability-runner.d.ts.map +0 -1
  185. package/dist/capability-runner.js.map +0 -1
  186. package/dist/capability-search-relevance.test.d.ts +0 -2
  187. package/dist/capability-search-relevance.test.d.ts.map +0 -1
  188. package/dist/capability-search-relevance.test.js +0 -67
  189. package/dist/capability-search-relevance.test.js.map +0 -1
  190. package/dist/capability-studio.d.ts.map +0 -1
  191. package/dist/capability-studio.js.map +0 -1
  192. package/dist/capability-studio.test.d.ts +0 -2
  193. package/dist/capability-studio.test.d.ts.map +0 -1
  194. package/dist/capability-studio.test.js +0 -41
  195. package/dist/capability-studio.test.js.map +0 -1
  196. package/dist/cdp-log.d.ts.map +0 -1
  197. package/dist/cdp-log.js.map +0 -1
  198. package/dist/cdp-log.test.d.ts +0 -2
  199. package/dist/cdp-log.test.d.ts.map +0 -1
  200. package/dist/cdp-log.test.js +0 -109
  201. package/dist/cdp-log.test.js.map +0 -1
  202. package/dist/cdp-relay.d.ts.map +0 -1
  203. package/dist/cdp-relay.js.map +0 -1
  204. package/dist/cdp-session.d.ts.map +0 -1
  205. package/dist/cdp-session.js.map +0 -1
  206. package/dist/cdp-types.d.ts.map +0 -1
  207. package/dist/cdp-types.js.map +0 -1
  208. package/dist/channel-owner-inspect.test.d.ts +0 -2
  209. package/dist/channel-owner-inspect.test.d.ts.map +0 -1
  210. package/dist/channel-owner-inspect.test.js +0 -75
  211. package/dist/channel-owner-inspect.test.js.map +0 -1
  212. package/dist/chrome-discovery.d.ts.map +0 -1
  213. package/dist/chrome-discovery.js.map +0 -1
  214. package/dist/chrome-discovery.test.d.ts +0 -2
  215. package/dist/chrome-discovery.test.d.ts.map +0 -1
  216. package/dist/chrome-discovery.test.js +0 -41
  217. package/dist/chrome-discovery.test.js.map +0 -1
  218. package/dist/clean-html.d.ts.map +0 -1
  219. package/dist/clean-html.js.map +0 -1
  220. package/dist/cli-help.test.d.ts +0 -2
  221. package/dist/cli-help.test.d.ts.map +0 -1
  222. package/dist/cli-help.test.js +0 -98
  223. package/dist/cli-help.test.js.map +0 -1
  224. package/dist/cli.d.ts.map +0 -1
  225. package/dist/cli.js.map +0 -1
  226. package/dist/cloud-client.d.ts.map +0 -1
  227. package/dist/cloud-client.js.map +0 -1
  228. package/dist/create-logger.d.ts.map +0 -1
  229. package/dist/create-logger.js.map +0 -1
  230. package/dist/debugger-examples-types.d.ts.map +0 -1
  231. package/dist/debugger-examples-types.js.map +0 -1
  232. package/dist/debugger-examples.d.ts.map +0 -1
  233. package/dist/debugger-examples.js.map +0 -1
  234. package/dist/debugger.d.ts.map +0 -1
  235. package/dist/debugger.js.map +0 -1
  236. package/dist/diff-utils.d.ts.map +0 -1
  237. package/dist/diff-utils.js.map +0 -1
  238. package/dist/diff-utils.test.d.ts +0 -2
  239. package/dist/diff-utils.test.d.ts.map +0 -1
  240. package/dist/diff-utils.test.js +0 -137
  241. package/dist/diff-utils.test.js.map +0 -1
  242. package/dist/doctor.d.ts.map +0 -1
  243. package/dist/doctor.js.map +0 -1
  244. package/dist/doctor.test.d.ts +0 -2
  245. package/dist/doctor.test.d.ts.map +0 -1
  246. package/dist/doctor.test.js +0 -208
  247. package/dist/doctor.test.js.map +0 -1
  248. package/dist/editor-examples.d.ts.map +0 -1
  249. package/dist/editor-examples.js.map +0 -1
  250. package/dist/editor.d.ts.map +0 -1
  251. package/dist/editor.js.map +0 -1
  252. package/dist/env-compat.d.ts.map +0 -1
  253. package/dist/env-compat.js.map +0 -1
  254. package/dist/executor.d.ts.map +0 -1
  255. package/dist/executor.js.map +0 -1
  256. package/dist/executor.unit.test.d.ts +0 -2
  257. package/dist/executor.unit.test.d.ts.map +0 -1
  258. package/dist/executor.unit.test.js +0 -143
  259. package/dist/executor.unit.test.js.map +0 -1
  260. package/dist/extension-connection.test.d.ts +0 -2
  261. package/dist/extension-connection.test.d.ts.map +0 -1
  262. package/dist/extension-connection.test.js +0 -708
  263. package/dist/extension-connection.test.js.map +0 -1
  264. package/dist/extension-icon-warning.test.d.ts +0 -2
  265. package/dist/extension-icon-warning.test.d.ts.map +0 -1
  266. package/dist/extension-icon-warning.test.js +0 -33
  267. package/dist/extension-icon-warning.test.js.map +0 -1
  268. package/dist/ghost-browser.d.ts.map +0 -1
  269. package/dist/ghost-browser.js.map +0 -1
  270. package/dist/ghost-cursor-controller.d.ts.map +0 -1
  271. package/dist/ghost-cursor-controller.js.map +0 -1
  272. package/dist/ghost-cursor.d.ts.map +0 -1
  273. package/dist/ghost-cursor.js.map +0 -1
  274. package/dist/htmlrewrite.d.ts.map +0 -1
  275. package/dist/htmlrewrite.js.map +0 -1
  276. package/dist/htmlrewrite.test.d.ts +0 -2
  277. package/dist/htmlrewrite.test.d.ts.map +0 -1
  278. package/dist/htmlrewrite.test.js +0 -7975
  279. package/dist/htmlrewrite.test.js.map +0 -1
  280. package/dist/index.d.ts.map +0 -1
  281. package/dist/index.js.map +0 -1
  282. package/dist/kill-port.d.ts.map +0 -1
  283. package/dist/kill-port.js.map +0 -1
  284. package/dist/kitty-graphics.d.ts.map +0 -1
  285. package/dist/kitty-graphics.js.map +0 -1
  286. package/dist/kitty-graphics.test.d.ts +0 -2
  287. package/dist/kitty-graphics.test.d.ts.map +0 -1
  288. package/dist/kitty-graphics.test.js +0 -125
  289. package/dist/kitty-graphics.test.js.map +0 -1
  290. package/dist/locator-selector.test.d.ts +0 -2
  291. package/dist/locator-selector.test.d.ts.map +0 -1
  292. package/dist/locator-selector.test.js +0 -98
  293. package/dist/locator-selector.test.js.map +0 -1
  294. package/dist/mcp-client.d.ts +0 -20
  295. package/dist/mcp-client.d.ts.map +0 -1
  296. package/dist/mcp-client.js +0 -56
  297. package/dist/mcp-client.js.map +0 -1
  298. package/dist/mcp.d.ts.map +0 -1
  299. package/dist/mcp.js.map +0 -1
  300. package/dist/on-mouse-action.test.d.ts +0 -2
  301. package/dist/on-mouse-action.test.d.ts.map +0 -1
  302. package/dist/on-mouse-action.test.js +0 -180
  303. package/dist/on-mouse-action.test.js.map +0 -1
  304. package/dist/options-page.test.d.ts +0 -2
  305. package/dist/options-page.test.d.ts.map +0 -1
  306. package/dist/options-page.test.js +0 -773
  307. package/dist/options-page.test.js.map +0 -1
  308. package/dist/package-paths.d.ts.map +0 -1
  309. package/dist/package-paths.js.map +0 -1
  310. package/dist/page-markdown.d.ts.map +0 -1
  311. package/dist/page-markdown.js.map +0 -1
  312. package/dist/performance-examples.d.ts.map +0 -1
  313. package/dist/performance-examples.js.map +0 -1
  314. package/dist/playwright-import.d.ts.map +0 -1
  315. package/dist/playwright-import.js.map +0 -1
  316. package/dist/popup-relocation.test.d.ts +0 -7
  317. package/dist/popup-relocation.test.d.ts.map +0 -1
  318. package/dist/popup-relocation.test.js +0 -139
  319. package/dist/popup-relocation.test.js.map +0 -1
  320. package/dist/product-paths.d.ts.map +0 -1
  321. package/dist/product-paths.js.map +0 -1
  322. package/dist/product-paths.test.d.ts +0 -2
  323. package/dist/product-paths.test.d.ts.map +0 -1
  324. package/dist/product-paths.test.js +0 -39
  325. package/dist/product-paths.test.js.map +0 -1
  326. package/dist/protocol.d.ts.map +0 -1
  327. package/dist/protocol.js.map +0 -1
  328. package/dist/protocol.test.d.ts +0 -2
  329. package/dist/protocol.test.d.ts.map +0 -1
  330. package/dist/protocol.test.js +0 -31
  331. package/dist/protocol.test.js.map +0 -1
  332. package/dist/react-source.d.ts.map +0 -1
  333. package/dist/react-source.js.map +0 -1
  334. package/dist/relay-client.d.ts.map +0 -1
  335. package/dist/relay-client.js.map +0 -1
  336. package/dist/relay-client.test.d.ts +0 -2
  337. package/dist/relay-client.test.d.ts.map +0 -1
  338. package/dist/relay-client.test.js +0 -305
  339. package/dist/relay-client.test.js.map +0 -1
  340. package/dist/relay-core.test.d.ts +0 -2
  341. package/dist/relay-core.test.d.ts.map +0 -1
  342. package/dist/relay-core.test.js +0 -1427
  343. package/dist/relay-core.test.js.map +0 -1
  344. package/dist/relay-navigation.test.d.ts +0 -2
  345. package/dist/relay-navigation.test.d.ts.map +0 -1
  346. package/dist/relay-navigation.test.js +0 -661
  347. package/dist/relay-navigation.test.js.map +0 -1
  348. package/dist/relay-session.test.d.ts +0 -2
  349. package/dist/relay-session.test.d.ts.map +0 -1
  350. package/dist/relay-session.test.js +0 -1121
  351. package/dist/relay-session.test.js.map +0 -1
  352. package/dist/relay-state.d.ts.map +0 -1
  353. package/dist/relay-state.js.map +0 -1
  354. package/dist/relay-state.test.d.ts +0 -2
  355. package/dist/relay-state.test.d.ts.map +0 -1
  356. package/dist/relay-state.test.js +0 -594
  357. package/dist/relay-state.test.js.map +0 -1
  358. package/dist/replay-ai-index.d.ts.map +0 -1
  359. package/dist/replay-ai-index.js.map +0 -1
  360. package/dist/replay-ai-index.test.d.ts +0 -2
  361. package/dist/replay-ai-index.test.d.ts.map +0 -1
  362. package/dist/replay-ai-index.test.js +0 -255
  363. package/dist/replay-ai-index.test.js.map +0 -1
  364. package/dist/replay-cli.test.d.ts +0 -2
  365. package/dist/replay-cli.test.d.ts.map +0 -1
  366. package/dist/replay-cli.test.js +0 -275
  367. package/dist/replay-cli.test.js.map +0 -1
  368. package/dist/replay-eval.d.ts.map +0 -1
  369. package/dist/replay-eval.js.map +0 -1
  370. package/dist/replay-handoff.d.ts.map +0 -1
  371. package/dist/replay-handoff.js.map +0 -1
  372. package/dist/replay-handoff.test.d.ts +0 -2
  373. package/dist/replay-handoff.test.d.ts.map +0 -1
  374. package/dist/replay-handoff.test.js +0 -123
  375. package/dist/replay-handoff.test.js.map +0 -1
  376. package/dist/replay-workflow-compiler.d.ts.map +0 -1
  377. package/dist/replay-workflow-compiler.js.map +0 -1
  378. package/dist/replay-workflow-compiler.test.d.ts +0 -2
  379. package/dist/replay-workflow-compiler.test.d.ts.map +0 -1
  380. package/dist/replay-workflow-compiler.test.js +0 -223
  381. package/dist/replay-workflow-compiler.test.js.map +0 -1
  382. package/dist/rrweb-recording-relay.d.ts.map +0 -1
  383. package/dist/rrweb-recording-relay.js.map +0 -1
  384. package/dist/rrweb-recording-relay.test.d.ts +0 -2
  385. package/dist/rrweb-recording-relay.test.d.ts.map +0 -1
  386. package/dist/rrweb-recording-relay.test.js +0 -85
  387. package/dist/rrweb-recording-relay.test.js.map +0 -1
  388. package/dist/rrweb-recording.d.ts.map +0 -1
  389. package/dist/rrweb-recording.js.map +0 -1
  390. package/dist/scoped-fs.d.ts.map +0 -1
  391. package/dist/scoped-fs.js.map +0 -1
  392. package/dist/scoped-fs.test.d.ts +0 -5
  393. package/dist/scoped-fs.test.d.ts.map +0 -1
  394. package/dist/scoped-fs.test.js +0 -73
  395. package/dist/scoped-fs.test.js.map +0 -1
  396. package/dist/skill-stub.test.d.ts +0 -2
  397. package/dist/skill-stub.test.d.ts.map +0 -1
  398. package/dist/skill-stub.test.js +0 -22
  399. package/dist/skill-stub.test.js.map +0 -1
  400. package/dist/snapshot-tools.test.d.ts +0 -2
  401. package/dist/snapshot-tools.test.d.ts.map +0 -1
  402. package/dist/snapshot-tools.test.js +0 -856
  403. package/dist/snapshot-tools.test.js.map +0 -1
  404. package/dist/start-relay-server.d.ts.map +0 -1
  405. package/dist/start-relay-server.js.map +0 -1
  406. package/dist/styles-examples.d.ts.map +0 -1
  407. package/dist/styles-examples.js.map +0 -1
  408. package/dist/styles.d.ts.map +0 -1
  409. package/dist/styles.js.map +0 -1
  410. package/dist/tabwright-agent-skill.d.ts.map +0 -1
  411. package/dist/tabwright-agent-skill.js.map +0 -1
  412. package/dist/tabwright-agent-skill.test.d.ts +0 -2
  413. package/dist/tabwright-agent-skill.test.d.ts.map +0 -1
  414. package/dist/tabwright-agent-skill.test.js +0 -51
  415. package/dist/tabwright-agent-skill.test.js.map +0 -1
  416. package/dist/test-declarations.d.ts.map +0 -1
  417. package/dist/test-declarations.js.map +0 -1
  418. package/dist/test-utils.d.ts +0 -69
  419. package/dist/test-utils.d.ts.map +0 -1
  420. package/dist/test-utils.js +0 -448
  421. package/dist/test-utils.js.map +0 -1
  422. package/dist/test-utils.test.d.ts +0 -2
  423. package/dist/test-utils.test.d.ts.map +0 -1
  424. package/dist/test-utils.test.js +0 -55
  425. package/dist/test-utils.test.js.map +0 -1
  426. package/dist/utils.d.ts.map +0 -1
  427. package/dist/utils.js.map +0 -1
  428. package/dist/wait-for-page-load.d.ts.map +0 -1
  429. package/dist/wait-for-page-load.js.map +0 -1
  430. package/dist/workflow-capability.d.ts.map +0 -1
  431. package/dist/workflow-capability.js.map +0 -1
  432. package/dist/workflow-script-flow.test.d.ts +0 -2
  433. package/dist/workflow-script-flow.test.d.ts.map +0 -1
  434. package/dist/workflow-script-flow.test.js +0 -285
  435. package/dist/workflow-script-flow.test.js.map +0 -1
  436. package/src/__snapshots__/x.com.processed.html +0 -1003
  437. package/src/__snapshots__/x.com.processed.withStyles.html +0 -2720
  438. package/src/a11y-client.ts +0 -241
  439. package/src/aria-snapshot.test.ts +0 -311
  440. package/src/aria-snapshot.ts +0 -1681
  441. package/src/aria-snapshot.unit.test.ts +0 -615
  442. package/src/aria-snapshots/github-interactive.txt +0 -193
  443. package/src/aria-snapshots/github-raw.txt +0 -309
  444. package/src/aria-snapshots/hackernews-interactive.txt +0 -477
  445. package/src/aria-snapshots/hackernews-raw.txt +0 -428
  446. package/src/aria-snapshots/prosemirror-interactive.txt +0 -66
  447. package/src/aria-snapshots/prosemirror-raw.txt +0 -145
  448. package/src/assets/aria-labels-hacker-news.png +0 -0
  449. package/src/assets/aria-labels-old-reddit.png +0 -0
  450. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +0 -5
  451. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +0 -18
  452. package/src/assets/x.com.html +0 -32946
  453. package/src/browser-config.ts +0 -179
  454. package/src/browser-install.ts +0 -284
  455. package/src/browser-launch.ts +0 -75
  456. package/src/builtin-capabilities.ts +0 -2765
  457. package/src/capability-agent-skill.ts +0 -305
  458. package/src/capability-auth-state.ts +0 -197
  459. package/src/capability-auth.ts +0 -249
  460. package/src/capability-cli-confirmation.test.ts +0 -90
  461. package/src/capability-options.test.ts +0 -89
  462. package/src/capability-options.ts +0 -57
  463. package/src/capability-package.test.ts +0 -204
  464. package/src/capability-package.ts +0 -555
  465. package/src/capability-registry.test.ts +0 -1905
  466. package/src/capability-registry.ts +0 -1234
  467. package/src/capability-runner.ts +0 -801
  468. package/src/capability-search-relevance.test.ts +0 -73
  469. package/src/capability-studio.test.ts +0 -45
  470. package/src/capability-studio.ts +0 -641
  471. package/src/cdp-log.test.ts +0 -131
  472. package/src/cdp-log.ts +0 -112
  473. package/src/cdp-relay.ts +0 -2944
  474. package/src/cdp-session.ts +0 -77
  475. package/src/cdp-types.ts +0 -158
  476. package/src/channel-owner-inspect.test.ts +0 -94
  477. package/src/chrome-discovery.test.ts +0 -50
  478. package/src/chrome-discovery.ts +0 -220
  479. package/src/clean-html.ts +0 -141
  480. package/src/cli-help.test.ts +0 -123
  481. package/src/cli.ts +0 -3181
  482. package/src/cloud-client.ts +0 -179
  483. package/src/create-logger.ts +0 -38
  484. package/src/debugger-examples-types.ts +0 -16
  485. package/src/debugger-examples.ts +0 -66
  486. package/src/debugger.ts +0 -708
  487. package/src/diff-utils.test.ts +0 -152
  488. package/src/diff-utils.ts +0 -70
  489. package/src/doctor.test.ts +0 -244
  490. package/src/doctor.ts +0 -321
  491. package/src/editor-examples.ts +0 -158
  492. package/src/editor.ts +0 -426
  493. package/src/env-compat.ts +0 -28
  494. package/src/executor.ts +0 -1824
  495. package/src/executor.unit.test.ts +0 -168
  496. package/src/extension-connection.test.ts +0 -855
  497. package/src/extension-icon-warning.test.ts +0 -45
  498. package/src/ghost-browser.ts +0 -149
  499. package/src/ghost-cursor-client.ts +0 -494
  500. package/src/ghost-cursor-controller.ts +0 -129
  501. package/src/ghost-cursor.ts +0 -123
  502. package/src/htmlrewrite.test.ts +0 -8008
  503. package/src/htmlrewrite.ts +0 -390
  504. package/src/index.ts +0 -53
  505. package/src/kill-port.ts +0 -219
  506. package/src/kitty-graphics.test.ts +0 -139
  507. package/src/kitty-graphics.ts +0 -79
  508. package/src/locator-selector.test.ts +0 -119
  509. package/src/mcp-client.ts +0 -78
  510. package/src/mcp.ts +0 -629
  511. package/src/on-mouse-action.test.ts +0 -226
  512. package/src/options-page.test.ts +0 -839
  513. package/src/package-paths.ts +0 -38
  514. package/src/page-markdown.ts +0 -240
  515. package/src/performance-examples.ts +0 -186
  516. package/src/playwright-import.ts +0 -59
  517. package/src/popup-relocation.test.ts +0 -163
  518. package/src/product-paths.test.ts +0 -47
  519. package/src/product-paths.ts +0 -28
  520. package/src/protocol.test.ts +0 -52
  521. package/src/protocol.ts +0 -351
  522. package/src/react-source.ts +0 -379
  523. package/src/relay-client.test.ts +0 -375
  524. package/src/relay-client.ts +0 -572
  525. package/src/relay-core.test.ts +0 -1603
  526. package/src/relay-navigation.test.ts +0 -790
  527. package/src/relay-session.test.ts +0 -1375
  528. package/src/relay-state.test.ts +0 -729
  529. package/src/relay-state.ts +0 -561
  530. package/src/replay-ai-index.test.ts +0 -278
  531. package/src/replay-ai-index.ts +0 -872
  532. package/src/replay-cli.test.ts +0 -325
  533. package/src/replay-eval.ts +0 -1039
  534. package/src/replay-handoff.test.ts +0 -163
  535. package/src/replay-handoff.ts +0 -109
  536. package/src/replay-workflow-compiler.test.ts +0 -250
  537. package/src/replay-workflow-compiler.ts +0 -700
  538. package/src/resource.md +0 -409
  539. package/src/rrweb-recording-relay.test.ts +0 -99
  540. package/src/rrweb-recording-relay.ts +0 -400
  541. package/src/rrweb-recording.ts +0 -322
  542. package/src/scoped-fs.test.ts +0 -80
  543. package/src/scoped-fs.ts +0 -420
  544. package/src/skill-stub.test.ts +0 -24
  545. package/src/snapshot-tools.test.ts +0 -1017
  546. package/src/snapshots/hacker-news-accessibility-full.md +0 -81
  547. package/src/snapshots/hacker-news-accessibility-interactive.md +0 -43
  548. package/src/snapshots/page-markdown-output.txt +0 -16
  549. package/src/snapshots/shadcn-ui-accessibility-full.md +0 -201
  550. package/src/snapshots/shadcn-ui-accessibility-interactive.md +0 -109
  551. package/src/start-relay-server.ts +0 -69
  552. package/src/styles-examples.ts +0 -84
  553. package/src/styles.ts +0 -343
  554. package/src/tabwright-agent-skill.test.ts +0 -61
  555. package/src/tabwright-agent-skill.ts +0 -105
  556. package/src/test-declarations.ts +0 -13
  557. package/src/test-utils.test.ts +0 -70
  558. package/src/test-utils.ts +0 -566
  559. package/src/utils.ts +0 -78
  560. package/src/wait-for-page-load.ts +0 -198
  561. package/src/workflow-capability.ts +0 -417
  562. package/src/workflow-script-flow.test.ts +0 -388
@@ -1,1905 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { describe, expect, test, vi } from 'vitest'
4
- import {
5
- createCapability,
6
- appendCapabilityRun,
7
- getProjectCapabilitiesDir,
8
- listCapabilities,
9
- readCapabilityRuns,
10
- readCapabilityScript,
11
- routeCapabilities,
12
- searchCapabilities,
13
- toCapabilityContract,
14
- updateCapabilityManifest as updateCapabilityManifestRecord,
15
- updateCapabilityScript,
16
- validateJsonAgainstSchema,
17
- writeCapabilitySecrets,
18
- } from './capability-registry.js'
19
- import { refreshCapabilityAuthFromCookies, refreshCapabilityAuthWithExecutor } from './capability-auth.js'
20
- import { getCapabilityAuthState } from './capability-auth-state.js'
21
- import { installBuiltinCapabilitySuite } from './builtin-capabilities.js'
22
- import {
23
- initCapabilityAgentSkill,
24
- installCapabilityAgentSkill,
25
- showCapabilityAgentSkill,
26
- } from './capability-agent-skill.js'
27
- import { prepareCapabilityRun, runCapabilityWithExecutor, runNodeCapability } from './capability-runner.js'
28
- import { saveWorkflowCapability, saveWorkflowFromRecording } from './workflow-capability.js'
29
-
30
- function createTempDir(prefix: string): string {
31
- const tempRoot = path.join(process.cwd(), 'tmp')
32
- fs.mkdirSync(tempRoot, { recursive: true })
33
- return fs.mkdtempSync(path.join(tempRoot, prefix))
34
- }
35
-
36
- function updateCapabilityManifest(
37
- options: Parameters<typeof updateCapabilityManifestRecord>[0],
38
- ): ReturnType<typeof updateCapabilityManifestRecord> {
39
- return updateCapabilityManifestRecord({
40
- ...options,
41
- allowUnvalidatedTrust: options.patch.status === 'trusted' || options.allowUnvalidatedTrust,
42
- })
43
- }
44
-
45
- describe('capability registry', () => {
46
- test('creates and lists project capabilities', () => {
47
- const cwd = createTempDir('capability-registry-')
48
- try {
49
- const capability = createCapability({
50
- id: 'query-user',
51
- title: 'Query user',
52
- description: 'Find user details',
53
- location: 'project',
54
- cwd,
55
- createdBy: 'ai',
56
- })
57
-
58
- expect(capability.dir).toBe(path.join(getProjectCapabilitiesDir({ cwd }), 'query-user'))
59
- expect(capability.manifest.status).toBe('draft')
60
- expect(readCapabilityScript({ id: 'query-user', cwd })).toContain('return {')
61
- expect(listCapabilities({ cwd }).map((item) => item.manifest.id)).toContain('query-user')
62
- } finally {
63
- fs.rmSync(cwd, { recursive: true, force: true })
64
- }
65
- })
66
-
67
- test('changing script downgrades trusted capability to draft', () => {
68
- const cwd = createTempDir('capability-downgrade-')
69
- try {
70
- createCapability({ id: 'saved-script', location: 'project', cwd })
71
- updateCapabilityManifest({ id: 'saved-script', cwd, patch: { status: 'trusted' } })
72
- const capability = updateCapabilityScript({
73
- id: 'saved-script',
74
- cwd,
75
- source: 'return { ok: true }',
76
- })
77
-
78
- expect(capability.manifest.status).toBe('draft')
79
- expect(readCapabilityScript({ id: 'saved-script', cwd })).toBe('return { ok: true }')
80
- } finally {
81
- fs.rmSync(cwd, { recursive: true, force: true })
82
- }
83
- })
84
-
85
- test('changing a trusted contract requires validation before trusting it again', () => {
86
- const cwd = createTempDir('capability-contract-downgrade-')
87
- try {
88
- createCapability({ id: 'saved-contract', location: 'project', cwd })
89
- updateCapabilityManifest({ id: 'saved-contract', cwd, patch: { status: 'trusted' } })
90
-
91
- const edited = updateCapabilityManifest({
92
- id: 'saved-contract',
93
- cwd,
94
- patch: { description: 'Updated behavior contract' },
95
- })
96
-
97
- expect(edited.manifest.status).toBe('draft')
98
- expect(() => {
99
- updateCapabilityManifestRecord({
100
- id: 'saved-contract',
101
- cwd,
102
- patch: { description: 'Reviewed behavior contract', status: 'trusted' },
103
- })
104
- }).toThrow('cannot change its contract and become trusted in the same update')
105
- } finally {
106
- fs.rmSync(cwd, { recursive: true, force: true })
107
- }
108
- })
109
-
110
- test('creates node capabilities', () => {
111
- const cwd = createTempDir('capability-node-')
112
- try {
113
- const capability = createCapability({
114
- id: 'api-tool',
115
- location: 'project',
116
- cwd,
117
- runtime: 'node',
118
- })
119
-
120
- expect(capability.manifest.runtime).toBe('node')
121
- expect(capability.manifest.permissions).toEqual(['network'])
122
- expect(readCapabilityScript({ id: 'api-tool', cwd })).toContain('secrets')
123
- } finally {
124
- fs.rmSync(cwd, { recursive: true, force: true })
125
- }
126
- })
127
-
128
- test('installs built-in conan config capabilities', () => {
129
- const cwd = createTempDir('capability-builtins-')
130
- try {
131
- const codexHome = path.join(cwd, 'codex-home')
132
- const installed = installBuiltinCapabilitySuite({
133
- suite: 'conan-config',
134
- location: 'project',
135
- cwd,
136
- codexHome,
137
- })
138
-
139
- expect(installed.capabilities.map((capability) => capability.manifest.id)).toEqual(['conan-config'])
140
- expect(installed.agentSkills.map((agentSkill) => agentSkill.name)).toEqual(['conan-config'])
141
- expect(installed.agentSkills[0]?.target).toBe('codex')
142
- expect(fs.readFileSync(path.join(codexHome, 'skills', 'conan-config', 'SKILL.md'), 'utf-8')).toContain(
143
- '--confirm conan-config:apply-change',
144
- )
145
- expect(
146
- fs.readFileSync(path.join(codexHome, 'skills', 'conan-config', 'agents', 'openai.yaml'), 'utf-8'),
147
- ).toContain('Conan 文案配置')
148
- const projectCapabilityIds = listCapabilities({ cwd })
149
- .filter((capability) => {
150
- return capability.location === 'project'
151
- })
152
- .map((item) => item.manifest.id)
153
- expect(projectCapabilityIds).toEqual(['conan-config'])
154
- const query = listCapabilities({ cwd }).find((capability) => {
155
- return capability.manifest.id === 'conan-config'
156
- })
157
- if (!query) {
158
- throw new Error('Expected conan-config to be installed')
159
- }
160
- expect(query.manifest).toMatchObject({
161
- runtime: 'node',
162
- status: 'trusted',
163
- sideEffect: 'write',
164
- requiresConfirmation: true,
165
- operations: {
166
- search: { sideEffect: 'read', requiresConfirmation: false },
167
- get: { routingHint: 'exact-match-direct-run', sideEffect: 'read', requiresConfirmation: false },
168
- history: { sideEffect: 'read', requiresConfirmation: false },
169
- 'get-schema': { sideEffect: 'read', requiresConfirmation: false },
170
- 'prepare-change': { sideEffect: 'read', requiresConfirmation: false },
171
- 'apply-change': { sideEffect: 'write', requiresConfirmation: true },
172
- 'save-draft': { sideEffect: 'write', requiresConfirmation: true },
173
- 'publish-draft': { sideEffect: 'write', requiresConfirmation: true },
174
- 'discard-draft': { sideEffect: 'write', requiresConfirmation: true },
175
- create: { sideEffect: 'write', requiresConfirmation: true },
176
- copy: { sideEffect: 'write', requiresConfirmation: true },
177
- rollback: { sideEffect: 'write', requiresConfirmation: true },
178
- },
179
- auth: {
180
- type: 'cookie',
181
- refresh: 'from-browser',
182
- secretKey: 'cookieHeader',
183
- },
184
- })
185
- expect(toCapabilityContract(query)).toMatchObject({
186
- id: 'conan-config',
187
- autonomousInvocation: { allowed: false },
188
- operations: {
189
- get: { autonomousInvocation: { allowed: true } },
190
- 'apply-change': {
191
- confirmationToken: 'conan-config:apply-change',
192
- autonomousInvocation: { allowed: false },
193
- },
194
- 'save-draft': {
195
- confirmationToken: 'conan-config:save-draft',
196
- autonomousInvocation: { allowed: false },
197
- },
198
- },
199
- })
200
- expect(query.manifest.whenToUse.join('\n')).toContain('Space_Enhanced_Config')
201
- expect(readCapabilityScript({ id: 'conan-config', cwd })).toContain('/conan-config/api/newConfigs/')
202
- expect(readCapabilityScript({ id: 'conan-config', cwd })).toContain('saveConfigArtifacts')
203
- expect(readCapabilityScript({ id: 'conan-config', cwd })).toContain('source_changed_after_validation')
204
- expect(query.manifest.operations.get?.inputSchema.properties).toMatchObject({
205
- environment: { enum: ['cn-prod', 'cn-test'] },
206
- saveArtifacts: { type: 'boolean' },
207
- })
208
- expect(query.manifest.operations.get?.outputSchema.properties).toMatchObject({
209
- artifacts: { type: 'object' },
210
- })
211
- expect(
212
- searchCapabilities({
213
- cwd,
214
- query:
215
- 'https://buff.zhenguanyu.com/buff-army/#/buff-oversea-designer/Space_Enhanced_Config?key=wareLandingPageSendCouponConfig&rootGroupingKey=Space_Pedia',
216
- })[0]?.capability.manifest.id,
217
- ).toBe('conan-config')
218
- const routes = routeCapabilities({
219
- cwd,
220
- task: '查这个配置 https://buff.zhenguanyu.com/buff-army/#/buff-oversea-designer/Space_Enhanced_Config?key=wareLandingPageSendCouponConfig&rootGroupingKey=Space_Pedia&config=%E5%8F%91%E5%88%B8',
221
- })
222
- expect(routes[0]?.capability.manifest.id).toBe('conan-config')
223
- expect(routes[0]?.operation).toBe('get')
224
- expect(routes[0]?.input).toEqual({
225
- action: 'get',
226
- url: 'https://buff.zhenguanyu.com/buff-army/#/buff-oversea-designer/Space_Enhanced_Config?key=wareLandingPageSendCouponConfig&rootGroupingKey=Space_Pedia&config=%E5%8F%91%E5%88%B8',
227
- })
228
- expect(routes[0]?.command).toContain('tabwright capability run conan-config')
229
- expect(routes[0]?.shellCommand).toBe(routes[0]?.command)
230
- expect(routes[0]?.commandWarning).toContain('not a shell command')
231
- expect(routes[0]?.commandWarning).toContain('run shellCommand exactly')
232
- expect(routes[0]?.executionHint).toMatchObject({
233
- routeCanRunSandboxed: true,
234
- runRequiresEscalatedSandbox: true,
235
- commandMustStartWith: 'tabwright capability run ',
236
- })
237
- const testRoute = routeCapabilities({
238
- cwd,
239
- task: 'https://buff-test.zhenguanyu.com/micro-buff/#/buff-oversea-designer/Space_Enhanced_Config?key=demoConfig&rootGroupingKey=Space_Pedia',
240
- })
241
- expect(testRoute[0]?.operation).toBe('get')
242
- expect(routeCapabilities({ cwd, task: 'https://example.com/not-a-known-config' })).toEqual([])
243
- expect(
244
- searchCapabilities({ cwd, query: '文案配置 查询 配置' }).some((result) => {
245
- return result.capability.manifest.id === 'conan-config'
246
- }),
247
- ).toBe(true)
248
- } finally {
249
- fs.rmSync(cwd, { recursive: true, force: true })
250
- }
251
- })
252
-
253
- test('runs unified Conan config read and confirmed publish operations', async () => {
254
- const cwd = createTempDir('capability-conan-config-run-')
255
- const currentValue = { title: 'before', items: [{ enabled: false }] }
256
- const targetValue = { title: 'after', items: [{ enabled: true }] }
257
- let publishedValue = currentValue
258
- let pendingValue = currentValue
259
- vi.stubGlobal('fetch', async (input: string | URL | Request, init?: RequestInit) => {
260
- const url = new URL(input instanceof Request ? input.url : input.toString())
261
- const body = typeof init?.body === 'string' ? init.body : ''
262
- const responseBody: unknown = (() => {
263
- if (url.pathname.endsWith('/enhancedConfigs/searchTree')) {
264
- return { code: 0, data: { configBases: [] } }
265
- }
266
- if (url.pathname.includes('/enhancedConfigs/detail/')) {
267
- return {
268
- code: 0,
269
- data: {
270
- config: {
271
- id: 42,
272
- namespace: 'Space_Pedia',
273
- key: 'demo',
274
- desc: 'Demo config',
275
- name: 'Demo',
276
- groupingId: 7,
277
- schemaId: 9,
278
- updatedTime: 123,
279
- value: JSON.stringify(currentValue),
280
- },
281
- configDraft: null,
282
- },
283
- }
284
- }
285
- if (url.pathname.endsWith('/schema')) {
286
- return [
287
- {
288
- schemaJson: JSON.stringify({
289
- schema: {
290
- type: 'object',
291
- properties: {
292
- title: { name: 'title', title: '标题', type: 'string', 'x-index': 0 },
293
- items: { name: 'items', title: '列表', type: 'array', 'x-index': 1 },
294
- },
295
- },
296
- form: {},
297
- }),
298
- },
299
- ]
300
- }
301
- if (url.pathname.endsWith('/enhancedConfigs/update')) {
302
- const payload = JSON.parse(body) as { value: string }
303
- pendingValue = JSON.parse(payload.value) as typeof currentValue
304
- return { code: 0, data: 88 }
305
- }
306
- if (url.pathname.endsWith('/enhancedConfigs/publish')) {
307
- publishedValue = pendingValue
308
- return { code: 0, data: true }
309
- }
310
- if (url.pathname.includes('/newConfigs/')) {
311
- return {
312
- id: 42,
313
- namespace: 'Space_Pedia',
314
- key: 'demo',
315
- desc: 'Demo config',
316
- updatedTime: 123,
317
- value: JSON.stringify(publishedValue),
318
- }
319
- }
320
- throw new Error(`Unexpected Conan test request: ${url.toString()}`)
321
- })()
322
- return new Response(JSON.stringify(responseBody), {
323
- status: 200,
324
- headers: { 'content-type': 'application/json' },
325
- })
326
- })
327
- try {
328
- const installed = installBuiltinCapabilitySuite({
329
- suite: 'conan-config',
330
- location: 'project',
331
- cwd,
332
- installAgentSkills: false,
333
- })
334
- const capability = installed.capabilities[0]
335
- if (!capability) {
336
- throw new Error('Expected unified Conan config capability')
337
- }
338
- writeCapabilitySecrets({ capability, secrets: { cookieHeader: 'session=test' } })
339
-
340
- await expect(
341
- runNodeCapability({ id: 'conan-config', cwd, input: { action: 'search', query: 'demo' } }),
342
- ).resolves.toMatchObject({ output: { action: 'search', resultCount: 0 } })
343
-
344
- const prepared = await runNodeCapability({
345
- id: 'conan-config',
346
- cwd,
347
- input: {
348
- action: 'prepare-change',
349
- namespace: 'Space_Pedia',
350
- key: 'demo',
351
- value: targetValue,
352
- changeSummary: '更新标题并启用第一项',
353
- },
354
- })
355
- const preparedOutput = prepared.output as {
356
- schemaId: number
357
- updatedTime: number
358
- sourceSha256: string
359
- targetSha256: string
360
- diff: unknown[]
361
- }
362
- expect(preparedOutput.diff.length).toBeGreaterThan(0)
363
-
364
- const applyInput = {
365
- action: 'apply-change',
366
- namespace: 'Space_Pedia',
367
- key: 'demo',
368
- value: targetValue,
369
- changeSummary: '更新标题并启用第一项',
370
- validationReport: {
371
- passed: true,
372
- errors: [],
373
- warnings: [],
374
- warningsAccepted: true,
375
- environment: 'cn-prod',
376
- schemaId: preparedOutput.schemaId,
377
- updatedTime: preparedOutput.updatedTime,
378
- sourceSha256: preparedOutput.sourceSha256,
379
- targetSha256: preparedOutput.targetSha256,
380
- summary: '校验通过',
381
- },
382
- }
383
- await expect(runNodeCapability({ id: 'conan-config', cwd, input: applyInput })).rejects.toThrow(
384
- 'rerun with --confirm conan-config:apply-change',
385
- )
386
- await expect(
387
- runNodeCapability({
388
- id: 'conan-config',
389
- cwd,
390
- input: applyInput,
391
- confirmation: 'conan-config:apply-change',
392
- }),
393
- ).resolves.toMatchObject({ output: { status: 'published', submitted: true, verified: true } })
394
- expect(publishedValue).toEqual(targetValue)
395
- } finally {
396
- vi.unstubAllGlobals()
397
- fs.rmSync(cwd, { recursive: true, force: true })
398
- }
399
- })
400
-
401
- test('runs Conan config draft, create, copy, history, and rollback operations in cn-test', async () => {
402
- const cwd = createTempDir('capability-conan-config-lifecycle-')
403
- type TestConfig = {
404
- id: number
405
- namespace: string
406
- rootGroupingKey: string
407
- key: string
408
- name: string
409
- desc: string
410
- groupingId: number
411
- schemaId: number
412
- updatedTime: number
413
- value: string
414
- }
415
- type TestDraft = {
416
- id: number
417
- opLdap: string
418
- config: TestConfig
419
- }
420
- const initialValue = { title: 'current', enabled: false }
421
- const draftValue = { title: 'draft', enabled: true }
422
- const discardedValue = { title: 'discard me', enabled: true }
423
- const historicalValue = { title: 'historical', enabled: false }
424
- const configs = new Map<string, TestConfig>()
425
- configs.set('Space_Pedia/demo_config', {
426
- id: 42,
427
- namespace: 'Space_Pedia',
428
- rootGroupingKey: 'Space_Pedia',
429
- key: 'demo_config',
430
- name: 'Demo',
431
- desc: 'Demo config',
432
- groupingId: 7,
433
- schemaId: 9,
434
- updatedTime: 123,
435
- value: JSON.stringify(initialValue),
436
- })
437
- let configDraft: TestDraft | null = null
438
- let nextConfigId = 43
439
- let nextDraftId = 88
440
- const requestedHosts = new Set<string>()
441
- const schemaRow = {
442
- id: 9,
443
- name: 'Demo schema',
444
- schemaJson: JSON.stringify({
445
- schema: {
446
- type: 'object',
447
- properties: {
448
- title: { name: 'title', title: '标题', type: 'string', required: true, 'x-index': 0 },
449
- enabled: { name: 'enabled', title: '启用', type: 'boolean', 'x-index': 1 },
450
- },
451
- },
452
- form: {},
453
- }),
454
- }
455
- const buildTree = () => {
456
- return [
457
- {
458
- id: 1,
459
- type: 2,
460
- name: 'Space Pedia',
461
- groupingKey: 'Space_Pedia',
462
- updatedTime: 200,
463
- children: [
464
- {
465
- id: 7,
466
- type: 2,
467
- name: 'Target group',
468
- groupingKey: 'target_group',
469
- updatedTime: 201,
470
- children: Array.from(configs.values()).map((config) => {
471
- return {
472
- id: config.id,
473
- type: 1,
474
- name: config.name,
475
- key: config.key,
476
- rootGroupingKey: config.rootGroupingKey,
477
- groupingId: config.groupingId,
478
- schemaId: config.schemaId,
479
- }
480
- }),
481
- },
482
- ],
483
- },
484
- ]
485
- }
486
- const findConfigById = (id: number) => {
487
- return Array.from(configs.values()).find((config) => {
488
- return config.id === id
489
- })
490
- }
491
- vi.stubGlobal('fetch', async (input: string | URL | Request, init?: RequestInit) => {
492
- const url = new URL(input instanceof Request ? input.url : input.toString())
493
- requestedHosts.add(url.origin)
494
- const body = typeof init?.body === 'string' ? init.body : ''
495
- const responseBody: unknown = await (async () => {
496
- if (url.pathname.endsWith('/enhancedConfigGroupings/rootGrouping')) {
497
- return { code: 0, data: buildTree() }
498
- }
499
- if (url.pathname.endsWith('/enhancedConfigs/searchTree')) {
500
- return { code: 0, data: { configBases: buildTree() } }
501
- }
502
- if (url.pathname.endsWith('/enhancedConfigs/detailHistory')) {
503
- return {
504
- code: 0,
505
- data: [
506
- {
507
- id: 501,
508
- configId: 42,
509
- config: {
510
- ...configs.get('Space_Pedia/demo_config'),
511
- schemaId: 9,
512
- value: JSON.stringify(historicalValue),
513
- },
514
- },
515
- ],
516
- }
517
- }
518
- if (url.pathname.endsWith('/enhancedConfigs/detail') && url.searchParams.has('id')) {
519
- const config = findConfigById(Number(url.searchParams.get('id')))
520
- return { code: 0, data: { config, configDraft: config?.id === 42 ? configDraft : null } }
521
- }
522
- if (url.pathname.includes('/enhancedConfigs/detail/')) {
523
- const [, namespace, key] = url.pathname.match(/\/enhancedConfigs\/detail\/([^/]+)\/([^/]+)$/) || []
524
- const config = configs.get(`${decodeURIComponent(namespace || '')}/${decodeURIComponent(key || '')}`)
525
- return {
526
- code: 0,
527
- data: {
528
- config,
529
- configDraft: config?.id === 42 ? configDraft : null,
530
- configHistories: config?.id === 42 ? [{ id: 501, configId: 42, opType: 3 }] : [],
531
- },
532
- }
533
- }
534
- if (url.pathname.endsWith('/schema')) {
535
- return [schemaRow]
536
- }
537
- if (url.pathname.endsWith('/enhancedConfigs/update')) {
538
- const payload = JSON.parse(body) as {
539
- id: number
540
- name: string
541
- desc: string
542
- groupingId: number
543
- schemaId: number
544
- value: string
545
- }
546
- const config = findConfigById(payload.id)
547
- if (!config) {
548
- throw new Error(`Missing config for update: ${payload.id}`)
549
- }
550
- configDraft = {
551
- id: nextDraftId,
552
- opLdap: 'tester',
553
- config: { ...config, ...payload },
554
- }
555
- nextDraftId += 1
556
- return { code: 0, data: configDraft.id }
557
- }
558
- if (url.pathname.endsWith('/enhancedConfigs/publish')) {
559
- if (!configDraft) {
560
- throw new Error('Missing draft for publish')
561
- }
562
- const config = findConfigById(configDraft.config.id)
563
- if (!config) {
564
- throw new Error(`Missing config for publish: ${configDraft.config.id}`)
565
- }
566
- Object.assign(config, configDraft.config, { updatedTime: config.updatedTime + 1 })
567
- configDraft = null
568
- return { code: 0, data: true }
569
- }
570
- if (url.pathname.endsWith('/enhancedConfigs/unPublish')) {
571
- configDraft = null
572
- return { code: 0, data: true }
573
- }
574
- if (url.pathname.endsWith('/enhancedConfigs/add')) {
575
- const payload = JSON.parse(body) as {
576
- groupingId: number
577
- key: string
578
- name: string
579
- desc: string
580
- schemaId: number
581
- }
582
- const config: TestConfig = {
583
- id: nextConfigId,
584
- namespace: 'Space_Pedia',
585
- rootGroupingKey: 'Space_Pedia',
586
- key: payload.key,
587
- name: payload.name,
588
- desc: payload.desc,
589
- groupingId: payload.groupingId,
590
- schemaId: payload.schemaId,
591
- updatedTime: 300,
592
- value: '{}',
593
- }
594
- nextConfigId += 1
595
- configs.set(`Space_Pedia/${config.key}`, config)
596
- return {
597
- code: 0,
598
- data: { id: config.id, rootGroupingKey: config.rootGroupingKey, key: config.key },
599
- }
600
- }
601
- if (url.pathname.includes('/newConfigs/')) {
602
- const [, namespace, key] = url.pathname.match(/\/newConfigs\/([^/]+)\/([^/]+)$/) || []
603
- return configs.get(`${decodeURIComponent(namespace || '')}/${decodeURIComponent(key || '')}`)
604
- }
605
- throw new Error(`Unexpected Conan lifecycle test request: ${url.toString()}`)
606
- })()
607
- return new Response(JSON.stringify(responseBody), {
608
- status: 200,
609
- headers: { 'content-type': 'application/json' },
610
- })
611
- })
612
-
613
- try {
614
- const installed = installBuiltinCapabilitySuite({
615
- suite: 'conan-config',
616
- location: 'project',
617
- cwd,
618
- installAgentSkills: false,
619
- })
620
- const capability = installed.capabilities[0]
621
- if (!capability) {
622
- throw new Error('Expected unified Conan config capability')
623
- }
624
- writeCapabilitySecrets({ capability, secrets: { cookieHeader: 'session=test' } })
625
-
626
- await expect(
627
- runNodeCapability({ id: 'conan-config', cwd, input: { action: 'list-spaces', environment: 'cn-test' } }),
628
- ).resolves.toMatchObject({ output: { environment: 'cn-test', count: 1 } })
629
- await expect(
630
- runNodeCapability({
631
- id: 'conan-config',
632
- cwd,
633
- input: { action: 'get-schema', environment: 'cn-test', schemaId: 9 },
634
- }),
635
- ).resolves.toMatchObject({ output: { environment: 'cn-test', schemaId: 9 } })
636
- await expect(
637
- runNodeCapability({
638
- id: 'conan-config',
639
- cwd,
640
- input: {
641
- action: 'history',
642
- environment: 'cn-test',
643
- namespace: 'Space_Pedia',
644
- key: 'demo_config',
645
- historyIds: [501],
646
- },
647
- }),
648
- ).resolves.toMatchObject({ output: { historyCount: 1, details: [{ id: 501 }] } })
649
-
650
- const prepared = await runNodeCapability({
651
- id: 'conan-config',
652
- cwd,
653
- input: {
654
- action: 'prepare-change',
655
- environment: 'cn-test',
656
- namespace: 'Space_Pedia',
657
- key: 'demo_config',
658
- value: draftValue,
659
- changeSummary: 'save a test draft',
660
- },
661
- })
662
- const preparedOutput = prepared.output as {
663
- environment: string
664
- schemaId: number
665
- updatedTime: number
666
- sourceSha256: string
667
- targetSha256: string
668
- }
669
- const changeReport = {
670
- passed: true,
671
- errors: [],
672
- warnings: [],
673
- warningsAccepted: true,
674
- environment: preparedOutput.environment,
675
- schemaId: preparedOutput.schemaId,
676
- updatedTime: preparedOutput.updatedTime,
677
- sourceSha256: preparedOutput.sourceSha256,
678
- targetSha256: preparedOutput.targetSha256,
679
- summary: 'validated',
680
- }
681
- await expect(
682
- runNodeCapability({
683
- id: 'conan-config',
684
- cwd,
685
- input: {
686
- action: 'save-draft',
687
- environment: 'cn-test',
688
- namespace: 'Space_Pedia',
689
- key: 'demo_config',
690
- value: draftValue,
691
- changeSummary: 'save a test draft',
692
- validationReport: changeReport,
693
- },
694
- confirmation: 'conan-config:save-draft',
695
- }),
696
- ).resolves.toMatchObject({ output: { status: 'draft_saved', published: false, verified: true } })
697
-
698
- const publishPrepared = await runNodeCapability({
699
- id: 'conan-config',
700
- cwd,
701
- input: {
702
- action: 'prepare-publish-draft',
703
- environment: 'cn-test',
704
- namespace: 'Space_Pedia',
705
- key: 'demo_config',
706
- },
707
- })
708
- const publishOutput = publishPrepared.output as {
709
- environment: string
710
- configDraftId: number
711
- schemaId: number
712
- updatedTime: number
713
- sourceSha256: string
714
- draftSha256: string
715
- }
716
- await expect(
717
- runNodeCapability({
718
- id: 'conan-config',
719
- cwd,
720
- input: {
721
- action: 'publish-draft',
722
- environment: 'cn-test',
723
- namespace: 'Space_Pedia',
724
- key: 'demo_config',
725
- validationReport: {
726
- passed: true,
727
- errors: [],
728
- warnings: [],
729
- warningsAccepted: true,
730
- ...publishOutput,
731
- summary: 'publish validated draft',
732
- },
733
- },
734
- confirmation: 'conan-config:publish-draft',
735
- }),
736
- ).resolves.toMatchObject({ output: { status: 'published', published: true, verified: true } })
737
-
738
- const discardPrepared = await runNodeCapability({
739
- id: 'conan-config',
740
- cwd,
741
- input: {
742
- action: 'prepare-change',
743
- environment: 'cn-test',
744
- namespace: 'Space_Pedia',
745
- key: 'demo_config',
746
- value: discardedValue,
747
- changeSummary: 'discard this draft',
748
- },
749
- })
750
- const discardChangeOutput = discardPrepared.output as {
751
- environment: string
752
- schemaId: number
753
- updatedTime: number
754
- sourceSha256: string
755
- targetSha256: string
756
- }
757
- await runNodeCapability({
758
- id: 'conan-config',
759
- cwd,
760
- input: {
761
- action: 'save-draft',
762
- environment: 'cn-test',
763
- namespace: 'Space_Pedia',
764
- key: 'demo_config',
765
- value: discardedValue,
766
- changeSummary: 'discard this draft',
767
- validationReport: {
768
- passed: true,
769
- errors: [],
770
- warnings: [],
771
- warningsAccepted: true,
772
- ...discardChangeOutput,
773
- summary: 'validated',
774
- },
775
- },
776
- confirmation: 'conan-config:save-draft',
777
- })
778
- const discardInspection = await runNodeCapability({
779
- id: 'conan-config',
780
- cwd,
781
- input: {
782
- action: 'prepare-publish-draft',
783
- environment: 'cn-test',
784
- namespace: 'Space_Pedia',
785
- key: 'demo_config',
786
- },
787
- })
788
- const discardInspectionOutput = discardInspection.output as { configDraftId: number; draftSha256: string }
789
- await expect(
790
- runNodeCapability({
791
- id: 'conan-config',
792
- cwd,
793
- input: {
794
- action: 'discard-draft',
795
- environment: 'cn-test',
796
- namespace: 'Space_Pedia',
797
- key: 'demo_config',
798
- configDraftId: discardInspectionOutput.configDraftId,
799
- draftSha256: discardInspectionOutput.draftSha256,
800
- },
801
- confirmation: 'conan-config:discard-draft',
802
- }),
803
- ).resolves.toMatchObject({ output: { status: 'draft_discarded', verified: true } })
804
-
805
- const rollbackPrepared = await runNodeCapability({
806
- id: 'conan-config',
807
- cwd,
808
- input: {
809
- action: 'prepare-rollback',
810
- environment: 'cn-test',
811
- namespace: 'Space_Pedia',
812
- key: 'demo_config',
813
- historyId: 501,
814
- },
815
- })
816
- const rollbackOutput = rollbackPrepared.output as {
817
- environment: string
818
- historyId: number
819
- schemaId: number
820
- updatedTime: number
821
- sourceSha256: string
822
- historicalSha256: string
823
- }
824
- await expect(
825
- runNodeCapability({
826
- id: 'conan-config',
827
- cwd,
828
- input: {
829
- action: 'rollback',
830
- environment: 'cn-test',
831
- namespace: 'Space_Pedia',
832
- key: 'demo_config',
833
- historyId: 501,
834
- validationReport: {
835
- passed: true,
836
- errors: [],
837
- warnings: [],
838
- warningsAccepted: true,
839
- ...rollbackOutput,
840
- summary: 'rollback validated',
841
- },
842
- },
843
- confirmation: 'conan-config:rollback',
844
- }),
845
- ).resolves.toMatchObject({ output: { status: 'rolled_back', published: true, verified: true } })
846
-
847
- const createInput = {
848
- action: 'prepare-create',
849
- environment: 'cn-test',
850
- groupingId: 7,
851
- key: 'new_config',
852
- name: 'New config',
853
- desc: 'New config description',
854
- schemaId: 9,
855
- }
856
- const createPrepared = await runNodeCapability({ id: 'conan-config', cwd, input: createInput })
857
- const createOutput = createPrepared.output as {
858
- environment: string
859
- groupingUpdatedTime: number
860
- targetSha256: string
861
- }
862
- await expect(
863
- runNodeCapability({
864
- id: 'conan-config',
865
- cwd,
866
- input: {
867
- ...createInput,
868
- action: 'create',
869
- preparationReport: createOutput,
870
- },
871
- confirmation: 'conan-config:create',
872
- }),
873
- ).resolves.toMatchObject({ output: { status: 'created', published: true, verified: true } })
874
-
875
- const copyInput = {
876
- action: 'prepare-copy',
877
- environment: 'cn-test',
878
- sourceNamespace: 'Space_Pedia',
879
- sourceKey: 'demo_config',
880
- groupingId: 7,
881
- targetKey: 'copy_config',
882
- }
883
- const copyPrepared = await runNodeCapability({ id: 'conan-config', cwd, input: copyInput })
884
- const copyOutput = copyPrepared.output as {
885
- environment: string
886
- groupingUpdatedTime: number
887
- targetSha256: string
888
- sourceDefinitionSha256: string
889
- }
890
- await expect(
891
- runNodeCapability({
892
- id: 'conan-config',
893
- cwd,
894
- input: {
895
- ...copyInput,
896
- action: 'copy',
897
- preparationReport: copyOutput,
898
- },
899
- confirmation: 'conan-config:copy',
900
- }),
901
- ).resolves.toMatchObject({
902
- output: { status: 'copied', contentCopied: false, published: true, verified: true },
903
- })
904
-
905
- expect(requestedHosts).toEqual(new Set(['https://ytkconan.zhenguanyu.com']))
906
- } finally {
907
- vi.unstubAllGlobals()
908
- fs.rmSync(cwd, { recursive: true, force: true })
909
- }
910
- })
911
-
912
- test('scaffolds and installs agent skills for AI-authored capabilities', () => {
913
- const cwd = createTempDir('capability-agent-skill-')
914
- try {
915
- createCapability({
916
- id: 'query-user',
917
- title: 'Query User',
918
- description: 'Query a user by email in the admin API',
919
- location: 'project',
920
- cwd,
921
- runtime: 'node',
922
- })
923
- updateCapabilityManifest({
924
- id: 'query-user',
925
- cwd,
926
- patch: {
927
- status: 'trusted',
928
- match: ['admin user email query'],
929
- routingHint: 'exact-match-direct-run',
930
- },
931
- })
932
-
933
- const initialized = initCapabilityAgentSkill({ id: 'query-user', cwd })
934
- const skillPath = path.join(initialized.dir, 'SKILL.md')
935
- const openAiPath = path.join(initialized.dir, 'agents', 'openai.yaml')
936
- expect(initialized.files.map((file) => file.relativePath)).toEqual(['SKILL.md', 'agents/openai.yaml'])
937
- expect(fs.readFileSync(skillPath, 'utf-8')).toContain('TABWRIGHT_AGENT_SKILL_TEMPLATE')
938
- expect(fs.readFileSync(openAiPath, 'utf-8')).toContain('Query User')
939
- expect(showCapabilityAgentSkill({ id: 'query-user', cwd }).files[0]?.content).toContain('TODO')
940
-
941
- const codexHome = path.join(cwd, 'codex-home')
942
- expect(() => {
943
- installCapabilityAgentSkill({ id: 'query-user', cwd, codexHome })
944
- }).toThrow(/Edit the skill content before installing/)
945
-
946
- const editedSkill = fs
947
- .readFileSync(skillPath, 'utf-8')
948
- .replace('<!-- TABWRIGHT_AGENT_SKILL_TEMPLATE: edit before install -->\n\n', '')
949
- .replace(
950
- 'TODO: Explain when agents should use the query-user Tabwright capability. Mention concrete user phrasing, exact-match signals, and when not to use it.',
951
- 'Use when the user asks to look up an admin user by email.',
952
- )
953
- .replace(
954
- '- TODO: Describe the concrete user intent, URL pattern, page state, or data shape that should trigger this capability.',
955
- '- Use for admin user lookup requests that include an email address.',
956
- )
957
- .replace('- TODO: State when this capability should not be used.', '- Do not use for public profile lookup.')
958
- .replace('- TODO: Define the default answer shape.', '- Return user id, email, and status.')
959
- .replace(
960
- '- TODO: Say when to show a short summary versus when to point to artifacts.',
961
- '- Keep chat output short and point to artifacts for raw API output.',
962
- )
963
- .replace(
964
- '- TODO: Say whether large outputs should be saved, filtered, or exported only on request.',
965
- '- Export only when the user asks.',
966
- )
967
- fs.writeFileSync(skillPath, editedSkill)
968
-
969
- const installed = installCapabilityAgentSkill({ id: 'query-user', cwd, codexHome })
970
- expect(installed.dir).toBe(path.join(codexHome, 'skills', 'query-user'))
971
- expect(fs.readFileSync(path.join(codexHome, 'skills', 'query-user', 'SKILL.md'), 'utf-8')).toContain(
972
- 'Use when the user asks to look up an admin user by email.',
973
- )
974
- expect(fs.readFileSync(path.join(codexHome, 'skills', 'query-user', 'agents', 'openai.yaml'), 'utf-8')).toContain(
975
- 'Query User',
976
- )
977
-
978
- createCapability({ id: 'update-user', location: 'project', cwd, runtime: 'browser' })
979
- updateCapabilityManifest({
980
- id: 'update-user',
981
- cwd,
982
- patch: { sideEffect: 'write', requiresConfirmation: true },
983
- })
984
- const writeSkill = initCapabilityAgentSkill({ id: 'update-user', cwd })
985
- const writeSkillContent = fs.readFileSync(path.join(writeSkill.dir, 'SKILL.md'), 'utf-8')
986
- expect(writeSkillContent).toContain('--browser user')
987
- expect(writeSkillContent).toContain('--force')
988
- expect(writeSkillContent).toContain('--confirm update-user')
989
- expect(writeSkillContent).toContain('Stop and obtain explicit user approval')
990
- } finally {
991
- fs.rmSync(cwd, { recursive: true, force: true })
992
- }
993
- })
994
-
995
- test('creates AI-readable capability contracts', () => {
996
- const cwd = createTempDir('capability-contract-')
997
- try {
998
- createCapability({
999
- id: 'bilibili-current-user',
1000
- title: 'Bilibili Current User',
1001
- description: 'Fetch current Bilibili account information',
1002
- location: 'project',
1003
- cwd,
1004
- runtime: 'node',
1005
- })
1006
- const capability = updateCapabilityManifest({
1007
- id: 'bilibili-current-user',
1008
- cwd,
1009
- patch: {
1010
- status: 'trusted',
1011
- whenToUse: ['用户询问当前 Bilibili 登录账号是谁'],
1012
- whenNotToUse: ['查询其他人的公开主页'],
1013
- tags: ['bilibili', 'account'],
1014
- auth: {
1015
- type: 'cookie',
1016
- refresh: 'from-browser',
1017
- secretKey: 'cookieHeader',
1018
- browserUrls: ['https://www.bilibili.com/', 'https://api.bilibili.com/'],
1019
- requiredCookieNames: ['SESSDATA'],
1020
- failureSignals: ['isLogin=false'],
1021
- },
1022
- },
1023
- })
1024
- const contract = toCapabilityContract(capability)
1025
-
1026
- expect(contract).toMatchObject({
1027
- id: 'bilibili-current-user',
1028
- runtime: 'node',
1029
- routingHint: 'search-first',
1030
- sideEffect: 'read',
1031
- autonomousInvocation: { allowed: true },
1032
- })
1033
- const searchResults = searchCapabilities({ query: '当前 Bilibili 登录账号', cwd }).map((result) => {
1034
- return result.capability.manifest.id
1035
- })
1036
- expect(searchResults[0]).toBe('bilibili-current-user')
1037
- } finally {
1038
- fs.rmSync(cwd, { recursive: true, force: true })
1039
- }
1040
- })
1041
-
1042
- test('validates simple object schemas', () => {
1043
- const result = validateJsonAgainstSchema({
1044
- schema: {
1045
- type: 'object',
1046
- properties: { email: { type: 'string' } },
1047
- required: ['email'],
1048
- },
1049
- value: { email: 'a@example.com' },
1050
- label: 'input',
1051
- })
1052
-
1053
- expect(result.valid).toBe(true)
1054
- })
1055
-
1056
- test('saves recording-derived workflows as draft write capabilities', () => {
1057
- const cwd = createTempDir('workflow-capability-')
1058
- try {
1059
- const saved = saveWorkflowCapability({
1060
- id: 'create-material-from-demo',
1061
- title: 'Create Material From Demo',
1062
- description: 'Fill and submit material form from input',
1063
- cwd,
1064
- sourceRecordingId: 'recording-123',
1065
- script: 'return { title: input.title }',
1066
- inputSchema: {
1067
- type: 'object',
1068
- properties: { title: { type: 'string' } },
1069
- required: ['title'],
1070
- },
1071
- })
1072
-
1073
- const capability = listCapabilities({ cwd }).find((item) => {
1074
- return item.manifest.id === 'create-material-from-demo'
1075
- })
1076
-
1077
- expect(capability?.manifest).toMatchObject({
1078
- status: 'draft',
1079
- runtime: 'browser',
1080
- sideEffect: 'write',
1081
- requiresConfirmation: true,
1082
- tags: expect.arrayContaining(['workflow', 'recording-derived', 'recording:recording-123']),
1083
- })
1084
- expect(readCapabilityScript({ id: 'create-material-from-demo', cwd })).toBe('return { title: input.title }')
1085
- expect(saved.capability).toMatchObject({ id: 'create-material-from-demo', sideEffect: 'write' })
1086
- } finally {
1087
- fs.rmSync(cwd, { recursive: true, force: true })
1088
- }
1089
- })
1090
-
1091
- test('generates batch workflow capabilities from recording-derived steps', () => {
1092
- const cwd = createTempDir('workflow-from-recording-')
1093
- try {
1094
- const saved = saveWorkflowFromRecording({
1095
- id: 'batch-create-material-from-demo',
1096
- title: 'Batch Create Material From Demo',
1097
- description: 'Replay the demonstrated material creation flow for each input item',
1098
- cwd,
1099
- recordingId: 'recording-456',
1100
- inputSchema: {
1101
- type: 'object',
1102
- properties: {
1103
- items: {
1104
- type: 'array',
1105
- items: {
1106
- type: 'object',
1107
- properties: {
1108
- title: { type: 'string' },
1109
- image: { type: 'string' },
1110
- },
1111
- required: ['title', 'image'],
1112
- },
1113
- },
1114
- },
1115
- required: ['items'],
1116
- },
1117
- steps: [
1118
- { action: 'goto', url: { value: 'https://admin.example.com/materials/new' } },
1119
- { action: 'fill', locator: '[name="title"]', value: { inputPath: 'title' } },
1120
- { action: 'setInputFiles', locator: '[name="image"]', path: { inputPath: 'image' } },
1121
- ],
1122
- finalRequest: {
1123
- url: '**/api/materials/**',
1124
- method: 'POST',
1125
- title: 'Create material',
1126
- trigger: { action: 'click', locator: 'button[type="submit"]' },
1127
- },
1128
- })
1129
-
1130
- const script = readCapabilityScript({ id: 'batch-create-material-from-demo', cwd })
1131
- const capability = listCapabilities({ cwd }).find((item) => {
1132
- return item.manifest.id === 'batch-create-material-from-demo'
1133
- })
1134
-
1135
- expect(script).toContain('needs_ai')
1136
- expect(script).toContain('runOneWorkflowItem')
1137
- expect(script).not.toContain('taskQueue.run')
1138
- expect(script).not.toContain('approval.captureAndSubmit')
1139
- expect(script).toContain('const items')
1140
- expect(capability?.manifest.tags).toEqual(
1141
- expect.arrayContaining(['workflow', 'recording-derived', 'recording:recording-456']),
1142
- )
1143
- expect(saved.capability).toMatchObject({ id: 'batch-create-material-from-demo', requiresConfirmation: true })
1144
- } finally {
1145
- fs.rmSync(cwd, { recursive: true, force: true })
1146
- }
1147
- })
1148
-
1149
- test('generates recording-derived workflows without forcing a final request', () => {
1150
- const cwd = createTempDir('workflow-from-recording-without-request-')
1151
- try {
1152
- saveWorkflowFromRecording({
1153
- id: 'draft-page-cleanup-from-demo',
1154
- title: 'Draft Page Cleanup From Demo',
1155
- cwd,
1156
- recordingId: 'recording-789',
1157
- steps: [
1158
- { action: 'goto', url: { value: 'https://admin.example.com/editor' } },
1159
- { action: 'fill', locator: '[name="title"]', value: { inputPath: 'title' } },
1160
- ],
1161
- })
1162
-
1163
- const script = readCapabilityScript({ id: 'draft-page-cleanup-from-demo', cwd })
1164
-
1165
- expect(script).toContain('const finalRequest = undefined')
1166
- expect(script).toContain('if (!finalRequest || !finalRequest.trigger)')
1167
- expect(script).not.toContain('approval.captureAndSubmit')
1168
- } finally {
1169
- fs.rmSync(cwd, { recursive: true, force: true })
1170
- }
1171
- })
1172
- })
1173
-
1174
- describe('capability runner', () => {
1175
- test('wraps a trusted capability with input and URL match checks', () => {
1176
- const cwd = createTempDir('capability-runner-')
1177
- try {
1178
- createCapability({ id: 'lookup-user', location: 'project', cwd })
1179
- updateCapabilityScript({
1180
- id: 'lookup-user',
1181
- cwd,
1182
- source: 'return { email: input.email, url: page.url(), capabilityId: capability.id }',
1183
- })
1184
- updateCapabilityManifest({
1185
- id: 'lookup-user',
1186
- cwd,
1187
- patch: {
1188
- status: 'trusted',
1189
- match: ['https://admin.example.com/*'],
1190
- inputSchema: {
1191
- type: 'object',
1192
- properties: { email: { type: 'string' } },
1193
- required: ['email'],
1194
- },
1195
- },
1196
- })
1197
-
1198
- const prepared = prepareCapabilityRun({
1199
- id: 'lookup-user',
1200
- cwd,
1201
- input: { email: 'a@example.com' },
1202
- })
1203
-
1204
- expect(prepared.code).toContain('const input = {"email":"a@example.com"};')
1205
- expect(prepared.code).toContain('const __tabwrightCapabilityMatch = ["https://admin.example.com/*"];')
1206
- expect(prepared.code).toContain('tabwright-capability://lookup-user')
1207
- } finally {
1208
- fs.rmSync(cwd, { recursive: true, force: true })
1209
- }
1210
- })
1211
-
1212
- test('requires force for draft capabilities', () => {
1213
- const cwd = createTempDir('capability-draft-')
1214
- try {
1215
- createCapability({ id: 'draft-tool', location: 'project', cwd })
1216
-
1217
- expect(() => {
1218
- prepareCapabilityRun({ id: 'draft-tool', cwd, input: {} })
1219
- }).toThrow('Capability is draft')
1220
- expect(() => {
1221
- prepareCapabilityRun({ id: 'draft-tool', cwd, input: {}, force: true })
1222
- }).not.toThrow()
1223
- } finally {
1224
- fs.rmSync(cwd, { recursive: true, force: true })
1225
- }
1226
- })
1227
-
1228
- test('requires an exact capability confirmation even when force is set', () => {
1229
- const cwd = createTempDir('capability-confirmation-')
1230
- try {
1231
- createCapability({ id: 'write-tool', location: 'project', cwd })
1232
- updateCapabilityManifest({
1233
- id: 'write-tool',
1234
- cwd,
1235
- patch: {
1236
- status: 'trusted',
1237
- sideEffect: 'write',
1238
- requiresConfirmation: true,
1239
- inputSchema: {
1240
- type: 'object',
1241
- properties: { value: { type: 'string' } },
1242
- required: ['value'],
1243
- },
1244
- },
1245
- })
1246
-
1247
- expect(() => {
1248
- prepareCapabilityRun({ id: 'write-tool', cwd, input: { value: 'x' }, force: true })
1249
- }).toThrow('requires explicit user confirmation')
1250
- expect(() => {
1251
- prepareCapabilityRun({
1252
- id: 'write-tool',
1253
- cwd,
1254
- input: { value: 'x' },
1255
- force: true,
1256
- confirmation: 'another-tool',
1257
- })
1258
- }).toThrow('rerun with --confirm write-tool')
1259
- expect(() => {
1260
- prepareCapabilityRun({
1261
- id: 'write-tool',
1262
- cwd,
1263
- input: { value: 'x' },
1264
- force: true,
1265
- confirmation: 'write-tool',
1266
- })
1267
- }).not.toThrow()
1268
- expect(() => {
1269
- prepareCapabilityRun({ id: 'write-tool', cwd, input: {}, force: true })
1270
- }).toThrow('Invalid capability input')
1271
- } finally {
1272
- fs.rmSync(cwd, { recursive: true, force: true })
1273
- }
1274
- })
1275
-
1276
- test('applies schemas and confirmation at operation scope', async () => {
1277
- const cwd = createTempDir('capability-operation-policy-')
1278
- try {
1279
- createCapability({ id: 'mixed-tool', location: 'project', cwd, runtime: 'node' })
1280
- updateCapabilityScript({
1281
- id: 'mixed-tool',
1282
- cwd,
1283
- source: 'return input.action === "read" ? { value: input.value } : { updated: true, value: input.value };',
1284
- })
1285
- updateCapabilityManifest({
1286
- id: 'mixed-tool',
1287
- cwd,
1288
- patch: {
1289
- status: 'trusted',
1290
- sideEffect: 'write',
1291
- requiresConfirmation: true,
1292
- operations: {
1293
- read: {
1294
- title: 'Read',
1295
- description: 'Read a value',
1296
- match: ['https://example.com/read*'],
1297
- routingHint: 'exact-match-direct-run',
1298
- inputSchema: {
1299
- type: 'object',
1300
- properties: { action: { type: 'string' }, value: { type: 'string' } },
1301
- required: ['action', 'value'],
1302
- },
1303
- outputSchema: {
1304
- type: 'object',
1305
- properties: { value: { type: 'string' } },
1306
- required: ['value'],
1307
- },
1308
- sideEffect: 'read',
1309
- requiresConfirmation: false,
1310
- },
1311
- write: {
1312
- title: 'Write',
1313
- description: 'Write a value',
1314
- match: [],
1315
- routingHint: 'search-first',
1316
- inputSchema: {
1317
- type: 'object',
1318
- properties: { action: { type: 'string' }, value: { type: 'string' } },
1319
- required: ['action', 'value'],
1320
- },
1321
- outputSchema: {
1322
- type: 'object',
1323
- properties: { updated: { type: 'boolean' }, value: { type: 'string' } },
1324
- required: ['updated', 'value'],
1325
- },
1326
- sideEffect: 'write',
1327
- requiresConfirmation: true,
1328
- },
1329
- },
1330
- },
1331
- })
1332
-
1333
- await expect(
1334
- runNodeCapability({ id: 'mixed-tool', cwd, input: { action: 'read', value: 'a' } }),
1335
- ).resolves.toMatchObject({
1336
- output: { value: 'a' },
1337
- runRecord: { operation: 'read' },
1338
- })
1339
- await expect(
1340
- runNodeCapability({ id: 'mixed-tool', cwd, input: { action: 'write', value: 'b' }, force: true }),
1341
- ).rejects.toThrow('rerun with --confirm mixed-tool:write')
1342
- await expect(
1343
- runNodeCapability({
1344
- id: 'mixed-tool',
1345
- cwd,
1346
- input: { action: 'write', value: 'b' },
1347
- force: true,
1348
- confirmation: 'mixed-tool',
1349
- }),
1350
- ).rejects.toThrow('rerun with --confirm mixed-tool:write')
1351
- await expect(
1352
- runNodeCapability({
1353
- id: 'mixed-tool',
1354
- cwd,
1355
- input: { action: 'write', value: 'b' },
1356
- force: true,
1357
- confirmation: 'mixed-tool:write',
1358
- }),
1359
- ).resolves.toMatchObject({ output: { updated: true, value: 'b' }, runRecord: { operation: 'write' } })
1360
- } finally {
1361
- fs.rmSync(cwd, { recursive: true, force: true })
1362
- }
1363
- })
1364
-
1365
- test('does not execute node or browser scripts before confirmation', async () => {
1366
- const cwd = createTempDir('capability-confirmation-execution-')
1367
- try {
1368
- const nodeCapability = createCapability({
1369
- id: 'confirmed-node-write',
1370
- location: 'project',
1371
- cwd,
1372
- runtime: 'node',
1373
- })
1374
- updateCapabilityScript({
1375
- id: 'confirmed-node-write',
1376
- cwd,
1377
- source: 'artifacts.writeText({ filename: "executed.txt", text: "yes" }); return { ok: true };',
1378
- })
1379
- updateCapabilityManifest({
1380
- id: 'confirmed-node-write',
1381
- cwd,
1382
- patch: { status: 'trusted', sideEffect: 'write', requiresConfirmation: true },
1383
- })
1384
-
1385
- await expect(runNodeCapability({ id: 'confirmed-node-write', cwd, input: {}, force: true })).rejects.toThrow(
1386
- 'requires explicit user confirmation',
1387
- )
1388
- expect(fs.existsSync(path.join(nodeCapability.dir, 'artifacts', 'executed.txt'))).toBe(false)
1389
-
1390
- createCapability({ id: 'confirmed-browser-write', location: 'project', cwd, runtime: 'browser' })
1391
- updateCapabilityManifest({
1392
- id: 'confirmed-browser-write',
1393
- cwd,
1394
- patch: { status: 'trusted', sideEffect: 'write', requiresConfirmation: true },
1395
- })
1396
- let browserScriptExecuted = false
1397
- await expect(
1398
- runCapabilityWithExecutor({
1399
- executor: {
1400
- execute: async () => {
1401
- browserScriptExecuted = true
1402
- return { text: '', images: [], screenshots: [], isError: false, structuredResult: {} }
1403
- },
1404
- },
1405
- id: 'confirmed-browser-write',
1406
- cwd,
1407
- input: {},
1408
- force: true,
1409
- }),
1410
- ).rejects.toThrow('requires explicit user confirmation')
1411
- expect(browserScriptExecuted).toBe(false)
1412
- } finally {
1413
- fs.rmSync(cwd, { recursive: true, force: true })
1414
- }
1415
- })
1416
-
1417
- test('runs node capabilities without a browser executor', async () => {
1418
- const cwd = createTempDir('capability-node-run-')
1419
- try {
1420
- const capability = createCapability({
1421
- id: 'node-api-tool',
1422
- location: 'project',
1423
- cwd,
1424
- runtime: 'node',
1425
- })
1426
- writeCapabilitySecrets({ capability, secrets: { token: 'secret-token' } })
1427
- updateCapabilityScript({
1428
- id: 'node-api-tool',
1429
- cwd,
1430
- source: 'return { token: secrets.token, input }',
1431
- })
1432
- updateCapabilityManifest({
1433
- id: 'node-api-tool',
1434
- cwd,
1435
- patch: {
1436
- status: 'trusted',
1437
- outputSchema: {
1438
- type: 'object',
1439
- properties: { token: { type: 'string' } },
1440
- required: ['token'],
1441
- },
1442
- },
1443
- })
1444
-
1445
- const result = await runNodeCapability({
1446
- id: 'node-api-tool',
1447
- cwd,
1448
- input: { ok: true },
1449
- })
1450
-
1451
- expect(result.output).toEqual({ token: 'secret-token', input: { ok: true } })
1452
- expect(result.runRecord.contract).toMatchObject({
1453
- status: 'passed',
1454
- output: { status: 'passed' },
1455
- network: { status: 'not-applicable' },
1456
- trust: { before: 'trusted', after: 'trusted', downgraded: false },
1457
- })
1458
- expect(toCapabilityContract(result.capability)).toMatchObject({
1459
- lifecycle: {
1460
- stage: 'trusted',
1461
- contractHealth: { state: 'healthy' },
1462
- },
1463
- })
1464
- const edited = updateCapabilityScript({
1465
- id: 'node-api-tool',
1466
- cwd,
1467
- source: 'return { token: secrets.token, input, edited: true }',
1468
- })
1469
- expect(toCapabilityContract(edited)).toMatchObject({
1470
- lifecycle: {
1471
- stage: 'drafted',
1472
- contractHealth: { state: 'unknown' },
1473
- },
1474
- })
1475
- } finally {
1476
- fs.rmSync(cwd, { recursive: true, force: true })
1477
- }
1478
- })
1479
-
1480
- test('records output drift once and downgrades trusted capabilities', async () => {
1481
- const cwd = createTempDir('capability-output-drift-')
1482
- try {
1483
- createCapability({ id: 'drifted-node-tool', location: 'project', cwd, runtime: 'node' })
1484
- updateCapabilityScript({
1485
- id: 'drifted-node-tool',
1486
- cwd,
1487
- source: 'return { userId: 42 }',
1488
- })
1489
- const capability = updateCapabilityManifest({
1490
- id: 'drifted-node-tool',
1491
- cwd,
1492
- patch: {
1493
- status: 'trusted',
1494
- outputSchema: {
1495
- type: 'object',
1496
- properties: { userId: { type: 'string' } },
1497
- required: ['userId'],
1498
- },
1499
- },
1500
- })
1501
-
1502
- await expect(runNodeCapability({ id: 'drifted-node-tool', cwd, input: {} })).rejects.toThrow(
1503
- 'execution completed but contract conformance failed',
1504
- )
1505
-
1506
- const runs = readCapabilityRuns({ capability })
1507
- expect(runs).toHaveLength(1)
1508
- expect(runs[0]).toMatchObject({
1509
- status: 'error',
1510
- contract: {
1511
- status: 'failed',
1512
- failures: [{ kind: 'output-schema', message: 'output.userId must be string' }],
1513
- trust: { before: 'trusted', after: 'draft', downgraded: true },
1514
- },
1515
- })
1516
- const current = listCapabilities({ cwd }).find((item) => {
1517
- return item.manifest.id === 'drifted-node-tool'
1518
- })
1519
- expect(current?.manifest.status).toBe('draft')
1520
- expect(current ? toCapabilityContract(current) : {}).toMatchObject({
1521
- autonomousInvocation: {
1522
- allowed: false,
1523
- reasons: expect.arrayContaining(['current contract failed conformance']),
1524
- },
1525
- lifecycle: {
1526
- stage: 'drifted',
1527
- nextAction: 'repair',
1528
- contractHealth: { state: 'drifted' },
1529
- },
1530
- })
1531
- expect(() => {
1532
- updateCapabilityManifestRecord({ id: 'drifted-node-tool', cwd, patch: { status: 'trusted' } })
1533
- }).toThrow('has no passing conformance evidence')
1534
-
1535
- updateCapabilityScript({
1536
- id: 'drifted-node-tool',
1537
- cwd,
1538
- source: 'return { userId: "repaired" }',
1539
- })
1540
- await runNodeCapability({ id: 'drifted-node-tool', cwd, input: {}, force: true })
1541
- const repaired = updateCapabilityManifestRecord({
1542
- id: 'drifted-node-tool',
1543
- cwd,
1544
- patch: { status: 'trusted' },
1545
- })
1546
- expect(toCapabilityContract(repaired)).toMatchObject({
1547
- lifecycle: { stage: 'trusted', contractHealth: { state: 'healthy' } },
1548
- })
1549
- } finally {
1550
- fs.rmSync(cwd, { recursive: true, force: true })
1551
- }
1552
- })
1553
-
1554
- test('records browser output drift before throwing', async () => {
1555
- const cwd = createTempDir('capability-browser-drift-')
1556
- try {
1557
- const capability = createCapability({
1558
- id: 'drifted-browser-tool',
1559
- location: 'project',
1560
- cwd,
1561
- runtime: 'browser',
1562
- })
1563
- updateCapabilityManifest({
1564
- id: 'drifted-browser-tool',
1565
- cwd,
1566
- patch: {
1567
- status: 'trusted',
1568
- outputSchema: {
1569
- type: 'object',
1570
- properties: { ok: { type: 'boolean' } },
1571
- required: ['ok'],
1572
- },
1573
- },
1574
- })
1575
-
1576
- await expect(
1577
- runCapabilityWithExecutor({
1578
- executor: {
1579
- execute: async () => {
1580
- return {
1581
- text: '',
1582
- images: [],
1583
- screenshots: [],
1584
- isError: false,
1585
- structuredResult: { ok: 'yes' },
1586
- }
1587
- },
1588
- },
1589
- id: 'drifted-browser-tool',
1590
- cwd,
1591
- input: {},
1592
- }),
1593
- ).rejects.toThrow('output.ok must be boolean')
1594
-
1595
- expect(readCapabilityRuns({ capability })).toHaveLength(1)
1596
- } finally {
1597
- fs.rmSync(cwd, { recursive: true, force: true })
1598
- }
1599
- })
1600
-
1601
- test('unwraps browser observations without exposing the internal envelope', async () => {
1602
- const cwd = createTempDir('capability-browser-observation-')
1603
- try {
1604
- createCapability({ id: 'observed-browser-tool', location: 'project', cwd, runtime: 'browser' })
1605
- updateCapabilityManifest({
1606
- id: 'observed-browser-tool',
1607
- cwd,
1608
- patch: {
1609
- status: 'trusted',
1610
- permissions: ['browser.read', 'network:https://allowed.example/*'],
1611
- outputSchema: {
1612
- type: 'object',
1613
- properties: { ok: { type: 'boolean' } },
1614
- required: ['ok'],
1615
- },
1616
- },
1617
- })
1618
-
1619
- const result = await runCapabilityWithExecutor({
1620
- executor: {
1621
- execute: async () => {
1622
- return {
1623
- text: '[return value] { __tabwrightCapabilityEnvelope: 1, output: { ok: true } }',
1624
- images: [],
1625
- screenshots: [],
1626
- isError: false,
1627
- structuredResult: {
1628
- __tabwrightCapabilityEnvelope: 1,
1629
- output: { ok: true },
1630
- observedNetworkUrls: ['https://allowed.example/api/value'],
1631
- url: 'https://allowed.example/app',
1632
- },
1633
- }
1634
- },
1635
- },
1636
- id: 'observed-browser-tool',
1637
- cwd,
1638
- input: {},
1639
- })
1640
-
1641
- expect(result.output).toEqual({ ok: true })
1642
- expect(result.executeResult.structuredResult).toEqual({ ok: true })
1643
- expect(result.executeResult.text).toBe('[return value] { ok: true }')
1644
- expect(result.executeResult.text).not.toContain('__tabwrightCapabilityEnvelope')
1645
- expect(result.runRecord).toMatchObject({
1646
- url: 'https://allowed.example/app',
1647
- contract: {
1648
- status: 'passed',
1649
- network: { status: 'passed', observedHosts: ['https://allowed.example'] },
1650
- },
1651
- })
1652
-
1653
- await expect(
1654
- runCapabilityWithExecutor({
1655
- executor: {
1656
- execute: async () => {
1657
- return {
1658
- text: '[return value] { __tabwrightCapabilityEnvelope: 1 }',
1659
- images: [],
1660
- screenshots: [],
1661
- isError: false,
1662
- structuredResult: {
1663
- __tabwrightCapabilityEnvelope: 1,
1664
- output: undefined,
1665
- observedNetworkUrls: ['https://undeclared.example/api/value'],
1666
- url: 'https://allowed.example/app',
1667
- error: 'runtime failed after request',
1668
- },
1669
- }
1670
- },
1671
- },
1672
- id: 'observed-browser-tool',
1673
- cwd,
1674
- input: {},
1675
- }),
1676
- ).rejects.toThrow('Network host is not declared')
1677
- const current = listCapabilities({ cwd }).find((item) => {
1678
- return item.manifest.id === 'observed-browser-tool'
1679
- })
1680
- expect(current?.manifest.status).toBe('draft')
1681
- } finally {
1682
- fs.rmSync(cwd, { recursive: true, force: true })
1683
- }
1684
- })
1685
-
1686
- test('observes node fetch hosts and rejects undeclared network access', async () => {
1687
- const cwd = createTempDir('capability-network-drift-')
1688
- const requestUrl = 'https://undeclared.example/data'
1689
- vi.stubGlobal('fetch', async () => {
1690
- return new Response('{"ok":true}', {
1691
- status: 200,
1692
- headers: { 'content-type': 'application/json' },
1693
- })
1694
- })
1695
- try {
1696
- const capability = createCapability({
1697
- id: 'scoped-network-tool',
1698
- location: 'project',
1699
- cwd,
1700
- runtime: 'node',
1701
- })
1702
- updateCapabilityScript({
1703
- id: 'scoped-network-tool',
1704
- cwd,
1705
- source: `await fetch(${JSON.stringify(requestUrl)}); throw new Error('runtime failed after request');`,
1706
- })
1707
- updateCapabilityManifest({
1708
- id: 'scoped-network-tool',
1709
- cwd,
1710
- patch: {
1711
- status: 'trusted',
1712
- permissions: ['network:https://allowed.example/*'],
1713
- },
1714
- })
1715
-
1716
- await expect(runNodeCapability({ id: 'scoped-network-tool', cwd, input: {} })).rejects.toThrow(
1717
- 'Network host is not declared',
1718
- )
1719
-
1720
- expect(readCapabilityRuns({ capability })[0]?.contract).toMatchObject({
1721
- status: 'failed',
1722
- network: {
1723
- status: 'failed',
1724
- observedHosts: [new URL(requestUrl).origin],
1725
- undeclaredHosts: [new URL(requestUrl).origin],
1726
- },
1727
- })
1728
- } finally {
1729
- vi.unstubAllGlobals()
1730
- fs.rmSync(cwd, { recursive: true, force: true })
1731
- }
1732
- })
1733
-
1734
- test('times out a node capability that does not settle', async () => {
1735
- const cwd = createTempDir('capability-node-timeout-')
1736
- try {
1737
- createCapability({ id: 'node-timeout-tool', location: 'project', cwd, runtime: 'node' })
1738
- updateCapabilityScript({
1739
- id: 'node-timeout-tool',
1740
- cwd,
1741
- source: 'await new Promise(() => {}); return { ok: true };',
1742
- })
1743
- updateCapabilityManifest({ id: 'node-timeout-tool', cwd, patch: { status: 'trusted' } })
1744
-
1745
- await expect(runNodeCapability({ id: 'node-timeout-tool', cwd, input: {}, timeout: 50 })).rejects.toThrow(
1746
- 'Capability execution timed out after 50ms',
1747
- )
1748
- } finally {
1749
- fs.rmSync(cwd, { recursive: true, force: true })
1750
- }
1751
- })
1752
-
1753
- test('node capabilities can write scoped artifacts', async () => {
1754
- const cwd = createTempDir('capability-node-artifacts-')
1755
- try {
1756
- createCapability({
1757
- id: 'node-artifact-tool',
1758
- location: 'project',
1759
- cwd,
1760
- runtime: 'node',
1761
- })
1762
- updateCapabilityScript({
1763
- id: 'node-artifact-tool',
1764
- cwd,
1765
- source: [
1766
- 'const jsonPath = artifacts.writeJson({ filename: "results/latest.json", value: input });',
1767
- 'const textPath = artifacts.writeText({ filename: "results/latest.md", text: "# ok\\n" });',
1768
- 'return { root: artifacts.root, jsonPath, textPath };',
1769
- ].join('\n'),
1770
- })
1771
- updateCapabilityManifest({
1772
- id: 'node-artifact-tool',
1773
- cwd,
1774
- patch: { status: 'trusted' },
1775
- })
1776
-
1777
- const result = await runNodeCapability({
1778
- id: 'node-artifact-tool',
1779
- cwd,
1780
- input: { ok: true },
1781
- })
1782
- const output = result.output as { jsonPath: string; textPath: string; root: string }
1783
-
1784
- expect(output.root).toBe(path.join(getProjectCapabilitiesDir({ cwd }), 'node-artifact-tool', 'artifacts'))
1785
- expect(JSON.parse(fs.readFileSync(output.jsonPath, 'utf-8'))).toEqual({ ok: true })
1786
- expect(fs.readFileSync(output.textPath, 'utf-8')).toBe('# ok\n')
1787
- } finally {
1788
- fs.rmSync(cwd, { recursive: true, force: true })
1789
- }
1790
- })
1791
-
1792
- test('builds browser cookie auth refresh without returning cookie values', async () => {
1793
- const cwd = createTempDir('capability-auth-refresh-')
1794
- try {
1795
- createCapability({
1796
- id: 'cookie-api-tool',
1797
- location: 'project',
1798
- cwd,
1799
- runtime: 'node',
1800
- })
1801
- const capability = updateCapabilityManifest({
1802
- id: 'cookie-api-tool',
1803
- cwd,
1804
- patch: {
1805
- auth: {
1806
- type: 'cookie',
1807
- refresh: 'from-browser',
1808
- secretKey: 'cookieHeader',
1809
- browserUrls: ['https://example.com/'],
1810
- requiredCookieNames: ['SESSION'],
1811
- failureSignals: ['loggedOut'],
1812
- },
1813
- },
1814
- })
1815
- const executedCode: string[] = []
1816
- const result = await refreshCapabilityAuthWithExecutor({
1817
- id: 'cookie-api-tool',
1818
- cwd,
1819
- executor: {
1820
- execute: async (code) => {
1821
- executedCode.push(code)
1822
- writeCapabilitySecrets({ capability, secrets: { cookieHeader: 'secret-value' } })
1823
- return {
1824
- text: '[return value] { saved: true }',
1825
- images: [],
1826
- screenshots: [],
1827
- isError: false,
1828
- structuredResult: {
1829
- saved: true,
1830
- secretKey: 'cookieHeader',
1831
- cookieCount: 1,
1832
- cookieNames: ['SESSION'],
1833
- urls: ['https://example.com/'],
1834
- expiresAt: '2099-01-01T00:00:00.000Z',
1835
- path: path.join(capability.dir, 'secrets.json'),
1836
- },
1837
- }
1838
- },
1839
- },
1840
- })
1841
-
1842
- expect(result.cookieCount).toBe(1)
1843
- expect(executedCode.join('\n')).toContain('Network.getCookies')
1844
- expect(executedCode.join('\n')).toContain('cookieHeader')
1845
- expect(JSON.stringify(result)).not.toContain('secret-value')
1846
- expect(getCapabilityAuthState({ capability }).status).toBe('authenticated')
1847
- } finally {
1848
- fs.rmSync(cwd, { recursive: true, force: true })
1849
- }
1850
- })
1851
-
1852
- test('tracks direct browser cookie authentication and marks authentication failures as expired', () => {
1853
- const cwd = createTempDir('capability-auth-state-')
1854
- try {
1855
- createCapability({ id: 'cookie-state-tool', location: 'project', cwd, runtime: 'node' })
1856
- const capability = updateCapabilityManifest({
1857
- id: 'cookie-state-tool',
1858
- cwd,
1859
- patch: {
1860
- auth: {
1861
- type: 'cookie',
1862
- refresh: 'from-browser',
1863
- secretKey: 'cookieHeader',
1864
- browserUrls: ['https://example.com/'],
1865
- requiredCookieNames: ['SESSION'],
1866
- failureSignals: ['Request failed 401'],
1867
- },
1868
- },
1869
- })
1870
- const expires = Math.floor(new Date('2099-01-01T00:00:00.000Z').getTime() / 1000)
1871
- const refreshed = refreshCapabilityAuthFromCookies({
1872
- id: 'cookie-state-tool',
1873
- cwd,
1874
- browserKey: 'install:Chrome:test-profile',
1875
- cookies: [{ name: 'SESSION', value: 'secret-value', expires }],
1876
- })
1877
-
1878
- expect(refreshed.expiresAt).toBe('2099-01-01T00:00:00.000Z')
1879
- expect(getCapabilityAuthState({ capability })).toMatchObject({
1880
- status: 'authenticated',
1881
- cookieNames: ['SESSION'],
1882
- browserKey: 'install:Chrome:test-profile',
1883
- })
1884
- expect(fs.readFileSync(path.join(capability.dir, 'auth-state.json'), 'utf-8')).not.toContain('secret-value')
1885
-
1886
- appendCapabilityRun({
1887
- capability,
1888
- record: {
1889
- id: 'auth-failure',
1890
- status: 'error',
1891
- durationMs: 10,
1892
- inputHash: 'test',
1893
- error: 'Request failed 401: login expired',
1894
- createdAt: new Date(Date.now() + 1000).toISOString(),
1895
- },
1896
- })
1897
- expect(getCapabilityAuthState({ capability })).toMatchObject({
1898
- status: 'expired',
1899
- reason: expect.stringContaining('Request failed 401'),
1900
- })
1901
- } finally {
1902
- fs.rmSync(cwd, { recursive: true, force: true })
1903
- }
1904
- })
1905
- })