tabwright 1.0.0

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