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