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