explorbot 0.0.1 → 0.1.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.
- package/README.md +80 -26
- package/bin/explorbot-cli.ts +680 -0
- package/boat/api-tester/src/ai/chief/styles.ts +15 -0
- package/boat/api-tester/src/ai/chief.ts +335 -0
- package/boat/api-tester/src/ai/curler-tools.ts +278 -0
- package/boat/api-tester/src/ai/curler.ts +306 -0
- package/boat/api-tester/src/api-client.ts +28 -0
- package/boat/api-tester/src/apibot.ts +203 -0
- package/boat/api-tester/src/cli.ts +301 -0
- package/boat/api-tester/src/config.ts +190 -0
- package/dist/bin/explorbot-cli.js +23 -101
- package/dist/boat/api-tester/bin/apibot-cli.js +0 -1
- package/dist/boat/api-tester/src/ai/chief/styles.js +0 -1
- package/dist/boat/api-tester/src/ai/chief.js +0 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +0 -1
- package/dist/boat/api-tester/src/ai/curler.js +0 -1
- package/dist/boat/api-tester/src/api-client.js +0 -1
- package/dist/boat/api-tester/src/apibot.js +0 -1
- package/dist/boat/api-tester/src/cli.js +0 -1
- package/dist/boat/api-tester/src/config.js +0 -1
- package/dist/src/action-result.js +0 -1
- package/dist/src/action.js +14 -12
- package/dist/src/activity.js +0 -1
- package/dist/src/ai/agent.js +0 -1
- package/dist/src/ai/bosun.js +0 -1
- package/dist/src/ai/captain/idle-mode.js +0 -1
- package/dist/src/ai/captain/mixin.js +0 -1
- package/dist/src/ai/captain/test-mode.js +0 -1
- package/dist/src/ai/captain/web-mode.js +0 -1
- package/dist/src/ai/captain.js +0 -1
- package/dist/src/ai/conversation.js +0 -1
- package/dist/src/ai/experience-compactor.js +0 -1
- package/dist/src/ai/fisherman-tools.js +0 -1
- package/dist/src/ai/fisherman.js +0 -1
- package/dist/src/ai/historian.js +0 -1
- package/dist/src/ai/navigator.js +0 -1
- package/dist/src/ai/pilot.js +0 -1
- package/dist/src/ai/planner/session-dedup.js +0 -1
- package/dist/src/ai/planner/styles.js +0 -1
- package/dist/src/ai/planner/subpages.js +42 -7
- package/dist/src/ai/planner.js +15 -4
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.js +0 -1
- package/dist/src/ai/researcher/cache.js +13 -9
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.js +16 -20
- package/dist/src/ai/researcher/fingerprint-worker.js +0 -1
- package/dist/src/ai/researcher/focus.js +0 -1
- package/dist/src/ai/researcher/locators.js +1 -2
- package/dist/src/ai/researcher/mixin.js +0 -1
- package/dist/src/ai/researcher/parser.js +4 -4
- package/dist/src/ai/researcher/research-result.js +2 -1
- package/dist/src/ai/researcher.js +6 -6
- package/dist/src/ai/rules.js +0 -1
- package/dist/src/ai/task-agent.js +0 -1
- package/dist/src/ai/tester.js +0 -1
- package/dist/src/ai/tools.js +4 -1
- package/dist/src/api/api-client.js +0 -1
- package/dist/src/api/request-result.js +0 -1
- package/dist/src/api/request-store.js +0 -1
- package/dist/src/api/spec-reader.js +0 -1
- package/dist/src/api/xhr-capture.js +0 -1
- package/dist/src/browser-server.js +0 -1
- package/dist/src/command-handler.js +0 -1
- package/dist/src/commands/add-rule-command.js +0 -1
- package/dist/src/commands/base-command.js +0 -1
- package/dist/src/commands/clean-command.js +0 -1
- package/dist/src/commands/context-aria-command.js +0 -1
- package/dist/src/commands/context-command.js +2 -3
- package/dist/src/commands/context-data-command.js +0 -1
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-html-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/debug-command.js +0 -1
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/exit-command.js +0 -1
- package/dist/src/commands/explore-command.js +3 -3
- package/dist/src/commands/freesail-command.js +0 -1
- package/dist/src/commands/help-command.js +0 -1
- package/dist/src/commands/index.js +0 -1
- package/dist/src/commands/init-command.js +117 -0
- package/dist/src/commands/knows-command.js +0 -1
- package/dist/src/commands/learn-command.js +0 -1
- package/dist/src/commands/navigate-command.js +0 -1
- package/dist/src/commands/path-command.js +0 -1
- package/dist/src/commands/plan-clear-command.js +0 -1
- package/dist/src/commands/plan-command.js +6 -2
- package/dist/src/commands/plan-edit-command.js +0 -1
- package/dist/src/commands/plan-load-command.js +0 -1
- package/dist/src/commands/plan-reload-command.js +0 -1
- package/dist/src/commands/plan-save-command.js +0 -1
- package/dist/src/commands/research-command.js +0 -1
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/status-command.js +0 -1
- package/dist/src/commands/test-command.js +0 -1
- package/dist/src/components/ActivityPane.js +0 -1
- package/dist/src/components/AddKnowledge.js +0 -1
- package/dist/src/components/AddRule.js +0 -1
- package/dist/src/components/App.js +0 -1
- package/dist/src/components/Autocomplete.js +0 -1
- package/dist/src/components/InputPane.js +0 -1
- package/dist/src/components/InputReadline.js +0 -1
- package/dist/src/components/LogPane.js +0 -1
- package/dist/src/components/PlanEditor.js +0 -1
- package/dist/src/components/PlanPane.js +0 -1
- package/dist/src/components/SessionTimer.js +0 -1
- package/dist/src/components/StateTransitionPane.js +0 -1
- package/dist/src/components/StatusPane.js +0 -1
- package/dist/src/components/TaskPane.js +0 -1
- package/dist/src/components/Welcome.js +0 -1
- package/dist/src/components/WelcomeChecklist.js +0 -1
- package/dist/src/components/WelcomeCommands.js +0 -1
- package/dist/src/components/autocomplete-store.js +0 -1
- package/dist/src/components/parse-keypress.js +0 -1
- package/dist/src/config.js +0 -1
- package/dist/src/execution-controller.js +0 -1
- package/dist/src/experience-tracker.js +0 -1
- package/dist/src/explorbot.js +1 -2
- package/dist/src/explorer.js +58 -17
- package/dist/src/index.js +0 -1
- package/dist/src/knowledge-tracker.js +2 -2
- package/dist/src/observability.js +0 -1
- package/dist/src/reporter.js +0 -1
- package/dist/src/state-manager.js +0 -1
- package/dist/src/stats.js +0 -1
- package/dist/src/test-plan.js +0 -1
- package/dist/src/utils/aria.js +0 -1
- package/dist/src/utils/cli-name.js +16 -0
- package/dist/src/utils/code-extractor.js +0 -1
- package/dist/src/utils/context-formatter.js +0 -1
- package/dist/src/utils/error-page.js +0 -1
- package/dist/src/utils/expandable.js +0 -1
- package/dist/src/utils/hooks-runner.js +0 -1
- package/dist/src/utils/html-diff.js +0 -1
- package/dist/src/utils/html.js +0 -1
- package/dist/src/utils/logger.js +0 -1
- package/dist/src/utils/loop.js +0 -1
- package/dist/src/utils/markdown-parser.js +0 -1
- package/dist/src/utils/markdown-query.js +0 -1
- package/dist/src/utils/markdown-terminal.js +0 -1
- package/dist/src/utils/research-parser.js +0 -1
- package/dist/src/utils/retry.js +0 -1
- package/dist/src/utils/rules-loader.js +0 -1
- package/dist/src/utils/strings.js +0 -1
- package/dist/src/utils/test-plan-markdown.js +0 -1
- package/dist/src/utils/throttle.js +0 -1
- package/dist/src/utils/unique-names.js +0 -1
- package/dist/src/utils/url-matcher.js +0 -1
- package/dist/src/utils/web-element.js +6 -5
- package/dist/src/utils/xpath.js +0 -1
- package/package.json +28 -4
- package/src/action-result.ts +694 -0
- package/src/action.ts +449 -0
- package/src/activity.ts +111 -0
- package/src/ai/agent.ts +3 -0
- package/src/ai/bosun.ts +557 -0
- package/src/ai/captain/idle-mode.ts +116 -0
- package/src/ai/captain/mixin.ts +22 -0
- package/src/ai/captain/test-mode.ts +262 -0
- package/src/ai/captain/web-mode.ts +136 -0
- package/src/ai/captain.ts +504 -0
- package/src/ai/conversation.ts +205 -0
- package/src/ai/experience-compactor.ts +284 -0
- package/src/ai/fisherman-tools.ts +181 -0
- package/src/ai/fisherman.ts +223 -0
- package/src/ai/historian.ts +457 -0
- package/src/ai/navigator.ts +572 -0
- package/src/ai/pilot.ts +776 -0
- package/src/ai/planner/session-dedup.ts +35 -0
- package/src/ai/planner/styles.ts +17 -0
- package/src/ai/planner/subpages.ts +171 -0
- package/src/ai/planner.ts +549 -0
- package/src/ai/provider.ts +613 -0
- package/src/ai/quartermaster.ts +286 -0
- package/src/ai/researcher/cache.ts +109 -0
- package/src/ai/researcher/coordinates.ts +239 -0
- package/src/ai/researcher/deep-analysis.ts +412 -0
- package/src/ai/researcher/fingerprint-worker.ts +59 -0
- package/src/ai/researcher/focus.ts +42 -0
- package/src/ai/researcher/locators.ts +282 -0
- package/src/ai/researcher/mixin.ts +4 -0
- package/src/ai/researcher/parser.ts +186 -0
- package/src/ai/researcher/research-result.ts +116 -0
- package/src/ai/researcher.ts +858 -0
- package/src/ai/rules.ts +376 -0
- package/src/ai/task-agent.ts +141 -0
- package/src/ai/tester.ts +939 -0
- package/src/ai/tools.ts +1122 -0
- package/src/api/api-client.ts +109 -0
- package/src/api/request-result.ts +212 -0
- package/src/api/request-store.ts +130 -0
- package/src/api/spec-reader.ts +174 -0
- package/src/api/xhr-capture.ts +100 -0
- package/src/browser-server.ts +74 -0
- package/src/command-handler.ts +454 -0
- package/src/commands/add-rule-command.ts +63 -0
- package/src/commands/base-command.ts +27 -0
- package/src/commands/clean-command.ts +73 -0
- package/src/commands/context-aria-command.ts +22 -0
- package/src/commands/context-command.ts +67 -0
- package/src/commands/context-data-command.ts +30 -0
- package/src/commands/context-experience-command.ts +48 -0
- package/src/commands/context-html-command.ts +33 -0
- package/src/commands/context-knowledge-command.ts +43 -0
- package/src/commands/debug-command.ts +13 -0
- package/src/commands/drill-command.ts +34 -0
- package/src/commands/exit-command.ts +32 -0
- package/src/commands/explore-command.ts +129 -0
- package/src/commands/freesail-command.ts +95 -0
- package/src/commands/help-command.ts +8 -0
- package/src/commands/index.ts +69 -0
- package/src/commands/init-command.ts +131 -0
- package/src/commands/knows-command.ts +68 -0
- package/src/commands/learn-command.ts +44 -0
- package/src/commands/navigate-command.ts +18 -0
- package/src/commands/path-command.ts +83 -0
- package/src/commands/plan-clear-command.ts +14 -0
- package/src/commands/plan-command.ts +46 -0
- package/src/commands/plan-edit-command.ts +9 -0
- package/src/commands/plan-load-command.ts +18 -0
- package/src/commands/plan-reload-command.ts +28 -0
- package/src/commands/plan-save-command.ts +25 -0
- package/src/commands/research-command.ts +45 -0
- package/src/commands/start-command.ts +13 -0
- package/src/commands/status-command.tsx +23 -0
- package/src/commands/test-command.ts +84 -0
- package/src/components/ActivityPane.tsx +80 -0
- package/src/components/AddKnowledge.tsx +169 -0
- package/src/components/AddRule.tsx +174 -0
- package/src/components/App.tsx +377 -0
- package/src/components/Autocomplete.tsx +63 -0
- package/src/components/InputPane.tsx +259 -0
- package/src/components/InputReadline.tsx +704 -0
- package/src/components/LogPane.tsx +187 -0
- package/src/components/PlanEditor.tsx +150 -0
- package/src/components/PlanPane.tsx +71 -0
- package/src/components/SessionTimer.tsx +35 -0
- package/src/components/StateTransitionPane.tsx +149 -0
- package/src/components/StatusPane.tsx +62 -0
- package/src/components/TaskPane.tsx +119 -0
- package/src/components/Welcome.tsx +83 -0
- package/src/components/WelcomeChecklist.tsx +118 -0
- package/src/components/WelcomeCommands.tsx +102 -0
- package/src/components/autocomplete-store.ts +35 -0
- package/src/components/parse-keypress.ts +170 -0
- package/src/config.ts +491 -0
- package/src/execution-controller.ts +109 -0
- package/src/experience-tracker.ts +350 -0
- package/src/explorbot.ts +405 -0
- package/src/explorer.ts +760 -0
- package/src/index.tsx +62 -0
- package/src/knowledge-tracker.ts +230 -0
- package/src/observability.ts +150 -0
- package/src/reporter.ts +224 -0
- package/src/state-manager.ts +556 -0
- package/src/stats.ts +53 -0
- package/src/test-plan.ts +432 -0
- package/src/utils/aria.ts +629 -0
- package/src/utils/cli-name.ts +13 -0
- package/src/utils/code-extractor.ts +22 -0
- package/src/utils/context-formatter.ts +239 -0
- package/src/utils/error-page.ts +23 -0
- package/src/utils/expandable.ts +38 -0
- package/src/utils/hooks-runner.ts +79 -0
- package/src/utils/html-diff.ts +918 -0
- package/src/utils/html.ts +1316 -0
- package/src/utils/logger.ts +534 -0
- package/src/utils/loop.ts +176 -0
- package/src/utils/markdown-parser.ts +127 -0
- package/src/utils/markdown-query.ts +466 -0
- package/src/utils/markdown-terminal.ts +43 -0
- package/src/utils/research-parser.ts +11 -0
- package/src/utils/retry.ts +73 -0
- package/src/utils/rules-loader.ts +118 -0
- package/src/utils/strings.ts +13 -0
- package/src/utils/test-plan-markdown.ts +332 -0
- package/src/utils/throttle.ts +18 -0
- package/src/utils/unique-names.ts +14 -0
- package/src/utils/url-matcher.ts +45 -0
- package/src/utils/web-element.ts +147 -0
- package/src/utils/xpath.ts +129 -0
- package/dist/bin/explorbot-cli.js.map +0 -1
- package/dist/boat/api-tester/bin/apibot-cli.js.map +0 -1
- package/dist/boat/api-tester/example/apibot.config.js +0 -31
- package/dist/boat/api-tester/example/apibot.config.js.map +0 -1
- package/dist/boat/api-tester/src/ai/chief/styles.js.map +0 -1
- package/dist/boat/api-tester/src/ai/chief.js.map +0 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js.map +0 -1
- package/dist/boat/api-tester/src/ai/curler.js.map +0 -1
- package/dist/boat/api-tester/src/api-client.js.map +0 -1
- package/dist/boat/api-tester/src/apibot.js.map +0 -1
- package/dist/boat/api-tester/src/cli.js.map +0 -1
- package/dist/boat/api-tester/src/config.js.map +0 -1
- package/dist/prompts/audit-rules.md +0 -124
- package/dist/src/action-result.js.map +0 -1
- package/dist/src/action.js.map +0 -1
- package/dist/src/activity.js.map +0 -1
- package/dist/src/ai/agent.js.map +0 -1
- package/dist/src/ai/bosun.js.map +0 -1
- package/dist/src/ai/captain/idle-mode.js.map +0 -1
- package/dist/src/ai/captain/mixin.js.map +0 -1
- package/dist/src/ai/captain/test-mode.js.map +0 -1
- package/dist/src/ai/captain/web-mode.js.map +0 -1
- package/dist/src/ai/captain.js.map +0 -1
- package/dist/src/ai/conversation.js.map +0 -1
- package/dist/src/ai/experience-compactor.js.map +0 -1
- package/dist/src/ai/fisherman-tools.js.map +0 -1
- package/dist/src/ai/fisherman.js.map +0 -1
- package/dist/src/ai/historian.js.map +0 -1
- package/dist/src/ai/navigator.js.map +0 -1
- package/dist/src/ai/pilot.js.map +0 -1
- package/dist/src/ai/planner/session-dedup.js.map +0 -1
- package/dist/src/ai/planner/styles.js.map +0 -1
- package/dist/src/ai/planner/subpages.js.map +0 -1
- package/dist/src/ai/planner.js.map +0 -1
- package/dist/src/ai/provider.js.map +0 -1
- package/dist/src/ai/quartermaster.js.map +0 -1
- package/dist/src/ai/researcher/cache.js.map +0 -1
- package/dist/src/ai/researcher/coordinates.js.map +0 -1
- package/dist/src/ai/researcher/deep-analysis.js.map +0 -1
- package/dist/src/ai/researcher/fingerprint-worker.js.map +0 -1
- package/dist/src/ai/researcher/focus.js.map +0 -1
- package/dist/src/ai/researcher/locators.js.map +0 -1
- package/dist/src/ai/researcher/mixin.js.map +0 -1
- package/dist/src/ai/researcher/parser.js.map +0 -1
- package/dist/src/ai/researcher/research-result.js.map +0 -1
- package/dist/src/ai/researcher.js.map +0 -1
- package/dist/src/ai/rules.js.map +0 -1
- package/dist/src/ai/task-agent.js.map +0 -1
- package/dist/src/ai/tester.js.map +0 -1
- package/dist/src/ai/tools.js.map +0 -1
- package/dist/src/api/api-client.js.map +0 -1
- package/dist/src/api/request-result.js.map +0 -1
- package/dist/src/api/request-store.js.map +0 -1
- package/dist/src/api/spec-reader.js.map +0 -1
- package/dist/src/api/xhr-capture.js.map +0 -1
- package/dist/src/browser-server.js.map +0 -1
- package/dist/src/command-handler.js.map +0 -1
- package/dist/src/commands/add-rule-command.js.map +0 -1
- package/dist/src/commands/base-command.js.map +0 -1
- package/dist/src/commands/clean-command.js.map +0 -1
- package/dist/src/commands/context-aria-command.js.map +0 -1
- package/dist/src/commands/context-command.js.map +0 -1
- package/dist/src/commands/context-data-command.js.map +0 -1
- package/dist/src/commands/context-experience-command.js.map +0 -1
- package/dist/src/commands/context-html-command.js.map +0 -1
- package/dist/src/commands/context-knowledge-command.js.map +0 -1
- package/dist/src/commands/debug-command.js.map +0 -1
- package/dist/src/commands/drill-command.js.map +0 -1
- package/dist/src/commands/exit-command.js.map +0 -1
- package/dist/src/commands/explore-command.js.map +0 -1
- package/dist/src/commands/freesail-command.js.map +0 -1
- package/dist/src/commands/help-command.js.map +0 -1
- package/dist/src/commands/index.js.map +0 -1
- package/dist/src/commands/knows-command.js.map +0 -1
- package/dist/src/commands/learn-command.js.map +0 -1
- package/dist/src/commands/navigate-command.js.map +0 -1
- package/dist/src/commands/path-command.js.map +0 -1
- package/dist/src/commands/plan-clear-command.js.map +0 -1
- package/dist/src/commands/plan-command.js.map +0 -1
- package/dist/src/commands/plan-edit-command.js.map +0 -1
- package/dist/src/commands/plan-load-command.js.map +0 -1
- package/dist/src/commands/plan-reload-command.js.map +0 -1
- package/dist/src/commands/plan-save-command.js.map +0 -1
- package/dist/src/commands/research-command.js.map +0 -1
- package/dist/src/commands/start-command.js.map +0 -1
- package/dist/src/commands/status-command.js.map +0 -1
- package/dist/src/commands/test-command.js.map +0 -1
- package/dist/src/components/ActivityPane.js.map +0 -1
- package/dist/src/components/AddKnowledge.js.map +0 -1
- package/dist/src/components/AddRule.js.map +0 -1
- package/dist/src/components/App.js.map +0 -1
- package/dist/src/components/Autocomplete.js.map +0 -1
- package/dist/src/components/InputPane.js.map +0 -1
- package/dist/src/components/InputReadline.js.map +0 -1
- package/dist/src/components/LogPane.js.map +0 -1
- package/dist/src/components/PlanEditor.js.map +0 -1
- package/dist/src/components/PlanPane.js.map +0 -1
- package/dist/src/components/SessionTimer.js.map +0 -1
- package/dist/src/components/StateTransitionPane.js.map +0 -1
- package/dist/src/components/StatusPane.js.map +0 -1
- package/dist/src/components/TaskPane.js.map +0 -1
- package/dist/src/components/Welcome.js.map +0 -1
- package/dist/src/components/WelcomeChecklist.js.map +0 -1
- package/dist/src/components/WelcomeCommands.js.map +0 -1
- package/dist/src/components/autocomplete-store.js.map +0 -1
- package/dist/src/components/parse-keypress.js.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/execution-controller.js.map +0 -1
- package/dist/src/experience-tracker.js.map +0 -1
- package/dist/src/explorbot.js.map +0 -1
- package/dist/src/explorer.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/knowledge-tracker.js.map +0 -1
- package/dist/src/observability.js.map +0 -1
- package/dist/src/reporter.js.map +0 -1
- package/dist/src/state-manager.js.map +0 -1
- package/dist/src/stats.js.map +0 -1
- package/dist/src/test-plan.js.map +0 -1
- package/dist/src/utils/aria.js.map +0 -1
- package/dist/src/utils/code-extractor.js.map +0 -1
- package/dist/src/utils/context-formatter.js.map +0 -1
- package/dist/src/utils/error-page.js.map +0 -1
- package/dist/src/utils/expandable.js.map +0 -1
- package/dist/src/utils/hooks-runner.js.map +0 -1
- package/dist/src/utils/html-diff.js.map +0 -1
- package/dist/src/utils/html.js.map +0 -1
- package/dist/src/utils/logger.js.map +0 -1
- package/dist/src/utils/loop.js.map +0 -1
- package/dist/src/utils/markdown-parser.js.map +0 -1
- package/dist/src/utils/markdown-query.js.map +0 -1
- package/dist/src/utils/markdown-terminal.js.map +0 -1
- package/dist/src/utils/research-parser.js.map +0 -1
- package/dist/src/utils/retry.js.map +0 -1
- package/dist/src/utils/rules-loader.js.map +0 -1
- package/dist/src/utils/strings.js.map +0 -1
- package/dist/src/utils/test-plan-markdown.js.map +0 -1
- package/dist/src/utils/throttle.js.map +0 -1
- package/dist/src/utils/unique-names.js.map +0 -1
- package/dist/src/utils/url-matcher.js.map +0 -1
- package/dist/src/utils/web-element.js.map +0 -1
- package/dist/src/utils/xpath.js.map +0 -1
- package/prompts/audit-rules.md +0 -124
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import dedent from 'dedent';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Conversation } from '../../../../src/ai/conversation.ts';
|
|
4
|
+
import { WithSessionDedup } from '../../../../src/ai/planner/session-dedup.ts';
|
|
5
|
+
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
6
|
+
import { Observability } from '../../../../src/observability.ts';
|
|
7
|
+
import { Plan, Test } from '../../../../src/test-plan.ts';
|
|
8
|
+
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
9
|
+
import { RulesLoader } from '../../../../src/utils/rules-loader.ts';
|
|
10
|
+
import type { ApiClient } from '../api-client.ts';
|
|
11
|
+
import type { ApibotConfig } from '../config.ts';
|
|
12
|
+
import { getActiveStyle, getStyles } from './chief/styles.ts';
|
|
13
|
+
|
|
14
|
+
const debugLog = createDebug('explorbot:chief');
|
|
15
|
+
|
|
16
|
+
const ApiTasksSchema = z.object({
|
|
17
|
+
planName: z.string().describe('Short descriptive name for the API test plan'),
|
|
18
|
+
scenarios: z
|
|
19
|
+
.array(
|
|
20
|
+
z.object({
|
|
21
|
+
scenario: z.string().describe('A single sentence describing what to test'),
|
|
22
|
+
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority based on API importance'),
|
|
23
|
+
steps: z.array(z.string()).describe('List of HTTP requests/actions to perform (e.g., "POST /users with valid payload", "GET /users/{id} to verify creation")'),
|
|
24
|
+
expectedOutcomes: z.array(z.string()).describe('List of verifiable outcomes (e.g., "Response status is 201", "Response body contains id field", "GET returns the created resource")'),
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
.describe('List of testing scenarios'),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ChiefBase = WithSessionDedup(Object as unknown as new (...args: any[]) => object);
|
|
31
|
+
|
|
32
|
+
export class Chief extends ChiefBase {
|
|
33
|
+
private provider: AIProvider;
|
|
34
|
+
private config: ApibotConfig;
|
|
35
|
+
private apiClient: ApiClient | null;
|
|
36
|
+
currentPlan: Plan | null = null;
|
|
37
|
+
private lastStyleName = '';
|
|
38
|
+
|
|
39
|
+
MIN_TASKS = 3;
|
|
40
|
+
MAX_TASKS = 10;
|
|
41
|
+
|
|
42
|
+
constructor(provider: AIProvider, config: ApibotConfig, apiClient?: ApiClient | null) {
|
|
43
|
+
super();
|
|
44
|
+
this.provider = provider;
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.apiClient = apiClient || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async plan(endpoint: string, opts?: { style?: string; specDefinition?: string }): Promise<Plan> {
|
|
50
|
+
tag('info').log(`Planning API tests for ${endpoint}`);
|
|
51
|
+
if (opts?.style) tag('info').log(`Planning style: ${opts.style}`);
|
|
52
|
+
|
|
53
|
+
debugLog('Sending planning prompt to AI provider');
|
|
54
|
+
|
|
55
|
+
await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
|
|
56
|
+
const sampleData = await this.collectSampleData(endpoint);
|
|
57
|
+
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
58
|
+
|
|
59
|
+
if (opts?.specDefinition) {
|
|
60
|
+
conversation.addUserText(dedent`
|
|
61
|
+
<api_spec>
|
|
62
|
+
${opts.specDefinition}
|
|
63
|
+
</api_spec>
|
|
64
|
+
|
|
65
|
+
NOTE: The spec may show absolute paths (e.g. /api/) but the base URL is ${this.config.api.baseEndpoint}.
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const aiResult = await this.provider.generateObject(conversation.messages, ApiTasksSchema, conversation.model, {
|
|
70
|
+
agentName: 'chief',
|
|
71
|
+
timeout: 120_000,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!aiResult?.object?.scenarios?.length) {
|
|
75
|
+
throw new Error('No test scenarios generated');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tests = aiResult.object.scenarios.map((s: any) => new Test(s.scenario, s.priority, s.expectedOutcomes, endpoint, s.steps || []));
|
|
79
|
+
|
|
80
|
+
if (!this.currentPlan) {
|
|
81
|
+
const planName = aiResult.object.planName || `API Tests: ${endpoint}`;
|
|
82
|
+
this.currentPlan = new Plan(planName);
|
|
83
|
+
this.currentPlan.url = endpoint;
|
|
84
|
+
const allPreviousScenarios = this.getPreviousSessionScenarios();
|
|
85
|
+
for (const t of tests) {
|
|
86
|
+
if (allPreviousScenarios.has(t.scenario.toLowerCase())) continue;
|
|
87
|
+
t.style = this.lastStyleName;
|
|
88
|
+
this.currentPlan.addTest(t);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
this.currentPlan.nextIteration();
|
|
92
|
+
this.addNewTests(tests, endpoint);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const summary = `Scenarios:\n${this.currentPlan.tests.map((t) => `- [${t.priority}] ${t.scenario}`).join('\n')}`;
|
|
97
|
+
tag('multiline').log(summary);
|
|
98
|
+
|
|
99
|
+
const availableStyles = Object.keys(getStyles()).join(', ');
|
|
100
|
+
tag('success').log(`Planning complete! ${this.currentPlan.tests.length} tests in plan: ${this.currentPlan.title}`);
|
|
101
|
+
tag('info').log(`Planning style: ${this.lastStyleName} (available: ${availableStyles})`);
|
|
102
|
+
|
|
103
|
+
this.registerPlanInSession(this.currentPlan);
|
|
104
|
+
|
|
105
|
+
return this.currentPlan;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private addNewTests(tests: Test[], defaultEndpoint: string): void {
|
|
109
|
+
if (!this.currentPlan) return;
|
|
110
|
+
|
|
111
|
+
const existing = new Set(this.currentPlan.tests.map((t) => t.scenario.toLowerCase()));
|
|
112
|
+
const allPreviousScenarios = this.getPreviousSessionScenariosExcluding(this.currentPlan);
|
|
113
|
+
|
|
114
|
+
for (const test of tests) {
|
|
115
|
+
if (existing.has(test.scenario.toLowerCase())) continue;
|
|
116
|
+
if (allPreviousScenarios.has(test.scenario.toLowerCase())) continue;
|
|
117
|
+
test.style = this.lastStyleName;
|
|
118
|
+
test.startUrl = test.startUrl || defaultEndpoint;
|
|
119
|
+
this.currentPlan.addTest(test);
|
|
120
|
+
existing.add(test.scenario.toLowerCase());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async collectSampleData(endpoint: string): Promise<string> {
|
|
125
|
+
if (!this.apiClient) return '';
|
|
126
|
+
|
|
127
|
+
tag('info').log(`Collecting sample data for ${endpoint}`);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const listResult = await this.apiClient.request({ method: 'GET', path: endpoint });
|
|
131
|
+
if (listResult.error || listResult.status >= 400) {
|
|
132
|
+
tag('warning').log(`Sample data fetch failed: ${listResult.error || listResult.status}`);
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const body = listResult.responseBody;
|
|
137
|
+
if (!body) return '';
|
|
138
|
+
|
|
139
|
+
const items = Array.isArray(body) ? body : Array.isArray(body.data) ? body.data : Array.isArray(body.items) ? body.items : [];
|
|
140
|
+
if (!items.length) return '';
|
|
141
|
+
|
|
142
|
+
const sampleItem = items[0];
|
|
143
|
+
const fields = Object.keys(sampleItem);
|
|
144
|
+
const idField = fields.find((f) => f === '_id' || f === 'id');
|
|
145
|
+
|
|
146
|
+
let detailItem = sampleItem;
|
|
147
|
+
if (idField && items.length > 0) {
|
|
148
|
+
const detailResult = await this.apiClient.request({ method: 'GET', path: `${endpoint}/${sampleItem[idField]}` });
|
|
149
|
+
if (!detailResult.error && detailResult.status < 400) {
|
|
150
|
+
const detailBody = detailResult.responseBody;
|
|
151
|
+
if (detailBody?.data) detailItem = detailBody.data;
|
|
152
|
+
else if (detailBody && !Array.isArray(detailBody)) detailItem = detailBody;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const lines: string[] = [];
|
|
157
|
+
lines.push(`Records found: ${body.meta?.total || items.length}`);
|
|
158
|
+
lines.push(`Fields: ${fields.join(', ')}`);
|
|
159
|
+
|
|
160
|
+
const ids: string[] = [];
|
|
161
|
+
for (const item of items.slice(0, 10)) {
|
|
162
|
+
const id = item._id || item.id;
|
|
163
|
+
if (id) ids.push(String(id));
|
|
164
|
+
}
|
|
165
|
+
if (ids.length) lines.push(`IDs: ${ids.join(', ')}`);
|
|
166
|
+
|
|
167
|
+
const foreignKeys: Record<string, Set<string>> = {};
|
|
168
|
+
const stringValues: Record<string, Set<string>> = {};
|
|
169
|
+
|
|
170
|
+
for (const item of items.slice(0, 30)) {
|
|
171
|
+
for (const [key, val] of Object.entries(item)) {
|
|
172
|
+
if (key.endsWith('_id') && val != null) {
|
|
173
|
+
foreignKeys[key] ||= new Set();
|
|
174
|
+
if (foreignKeys[key].size < 5) foreignKeys[key].add(String(val));
|
|
175
|
+
}
|
|
176
|
+
if (typeof val === 'string' && val.length > 0 && val.length < 100 && key !== 'id' && key !== '_id') {
|
|
177
|
+
stringValues[key] ||= new Set();
|
|
178
|
+
if (stringValues[key].size < 10) stringValues[key].add(val);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const [key, vals] of Object.entries(foreignKeys)) {
|
|
184
|
+
lines.push(`${key}: ${[...vals].join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const [key, vals] of Object.entries(stringValues)) {
|
|
188
|
+
if (vals.size < items.length * 0.8) {
|
|
189
|
+
lines.push(`${key} values: ${[...vals].join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const truncatedSample: Record<string, unknown> = {};
|
|
194
|
+
for (const [key, val] of Object.entries(detailItem)) {
|
|
195
|
+
if (typeof val === 'string' && val.length > 100) {
|
|
196
|
+
truncatedSample[key] = `${val.substring(0, 100)}...`;
|
|
197
|
+
} else {
|
|
198
|
+
truncatedSample[key] = val;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
lines.push(`Sample record: ${JSON.stringify(truncatedSample, null, 2)}`);
|
|
202
|
+
|
|
203
|
+
const sampleData = lines.join('\n');
|
|
204
|
+
tag('success').log('Sample data collected');
|
|
205
|
+
debugLog('Sample data:', sampleData.substring(0, 500));
|
|
206
|
+
return sampleData;
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
tag('warning').log(`Sample data collection failed: ${error.message}`);
|
|
209
|
+
debugLog('collectSampleData error:', error);
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private buildConversation(endpoint: string, style?: string, sampleData?: string): Conversation {
|
|
215
|
+
const model = this.provider.getAgenticModel('chief');
|
|
216
|
+
const conversation = new Conversation([], model);
|
|
217
|
+
|
|
218
|
+
conversation.addUserText(this.getSystemMessage());
|
|
219
|
+
|
|
220
|
+
const { name, approach } = getActiveStyle(this.currentPlan?.iteration || 0, style);
|
|
221
|
+
this.lastStyleName = name;
|
|
222
|
+
|
|
223
|
+
const planningPrompt = dedent`
|
|
224
|
+
<task>
|
|
225
|
+
Create ${this.MIN_TASKS}-${this.MAX_TASKS} API testing scenarios for endpoint: ${endpoint}
|
|
226
|
+
Base URL: ${this.config.api.baseEndpoint}
|
|
227
|
+
</task>
|
|
228
|
+
|
|
229
|
+
<approach>
|
|
230
|
+
${approach}
|
|
231
|
+
</approach>
|
|
232
|
+
|
|
233
|
+
<rules>
|
|
234
|
+
${RulesLoader.loadRules('chief', ['general'], endpoint)}
|
|
235
|
+
</rules>
|
|
236
|
+
|
|
237
|
+
<context>
|
|
238
|
+
Endpoint: ${endpoint}
|
|
239
|
+
Base URL: ${this.config.api.baseEndpoint}
|
|
240
|
+
Headers and authentication are handled automatically — do NOT include them in test steps.
|
|
241
|
+
Steps should only contain relative paths (e.g. /tests, /tests/{id}), not full URLs.
|
|
242
|
+
</context>
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
conversation.addUserText(planningPrompt);
|
|
246
|
+
|
|
247
|
+
if (sampleData) {
|
|
248
|
+
conversation.addUserText(dedent`
|
|
249
|
+
<sample_data>
|
|
250
|
+
${sampleData}
|
|
251
|
+
</sample_data>
|
|
252
|
+
|
|
253
|
+
Use this real data from the API when planning test scenarios:
|
|
254
|
+
- Reference real existing IDs for parent record references (e.g. use actual milestone_id, project_id values)
|
|
255
|
+
- Use real enum values discovered in the data
|
|
256
|
+
- Each test MUST use DIFFERENT data — never reuse the same field values across tests
|
|
257
|
+
- For "create" tests: base payload on a real record but change field values to create new unique data
|
|
258
|
+
- For "update" tests: pick a real existing ID and modify specific fields
|
|
259
|
+
- For tests needing parent references: use real _id field values from sample_data
|
|
260
|
+
`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.currentPlan) {
|
|
264
|
+
const existingTests = this.currentPlan.tests
|
|
265
|
+
.map((t) => {
|
|
266
|
+
let entry = `- "${t.scenario}" [${t.priority}] [${t.result || 'pending'}]`;
|
|
267
|
+
if (t.plannedSteps?.length) entry += `\n Steps: ${t.plannedSteps.join('; ')}`;
|
|
268
|
+
if (t.expected?.length) entry += `\n Expects: ${t.expected.join('; ')}`;
|
|
269
|
+
return entry;
|
|
270
|
+
})
|
|
271
|
+
.join('\n');
|
|
272
|
+
|
|
273
|
+
conversation.addUserText(dedent`
|
|
274
|
+
CRITICAL: This plan already has tests. Do NOT re-propose existing scenarios or SEMANTICALLY SIMILAR ones.
|
|
275
|
+
|
|
276
|
+
<existing_tests>
|
|
277
|
+
${existingTests}
|
|
278
|
+
</existing_tests>
|
|
279
|
+
|
|
280
|
+
A scenario is a DUPLICATE if it tests the same behavior, even with different wording.
|
|
281
|
+
For example, "Create a basic suite" and "Successful creation of a simple suite" test the same thing — both are basic POST creation.
|
|
282
|
+
Propose ONLY scenarios that verify behaviors NOT already covered above.
|
|
283
|
+
Return empty array if no new tests needed.
|
|
284
|
+
`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const sessionTests = this.getSessionTestsSummary();
|
|
288
|
+
if (sessionTests) {
|
|
289
|
+
conversation.addUserText(dedent`
|
|
290
|
+
Tests already planned in this session across all endpoints. DO NOT duplicate any of these:
|
|
291
|
+
|
|
292
|
+
<session_tests>
|
|
293
|
+
${sessionTests}
|
|
294
|
+
</session_tests>
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const tasksMessage = dedent`
|
|
299
|
+
<task>
|
|
300
|
+
Provide testing scenarios as structured data:
|
|
301
|
+
1. Create a short plan name (e.g., "Users API CRUD Testing", "Auth Endpoint Validation")
|
|
302
|
+
2. Assign priorities:
|
|
303
|
+
- CRITICAL: Core CRUD operations
|
|
304
|
+
- HIGH: listing and filtering and searching element
|
|
305
|
+
- NORMAL: Input validation, error handling
|
|
306
|
+
- NORMAL: Edge cases, boundary conditions
|
|
307
|
+
3. For each scenario provide BOTH steps and expected outcomes
|
|
308
|
+
4. Steps should be specific HTTP requests (e.g., "POST /users with {name: 'Test', email: 'test@test.com'}")
|
|
309
|
+
5. Expected outcomes should be verifiable (e.g., "Status 201", "Response contains id field")
|
|
310
|
+
</task>
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
conversation.addUserText(tasksMessage);
|
|
314
|
+
|
|
315
|
+
return conversation;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private getSystemMessage(): string {
|
|
319
|
+
return dedent`
|
|
320
|
+
<role>
|
|
321
|
+
You are a senior API test engineer specializing in REST API testing.
|
|
322
|
+
Your task is to plan comprehensive test scenarios for API endpoints.
|
|
323
|
+
</role>
|
|
324
|
+
|
|
325
|
+
<expertise>
|
|
326
|
+
- HTTP methods and status codes
|
|
327
|
+
- REST API conventions and best practices
|
|
328
|
+
- Input validation and error handling patterns
|
|
329
|
+
- Authentication and authorization testing
|
|
330
|
+
- Data integrity and CRUD operations
|
|
331
|
+
- Edge cases and boundary value analysis
|
|
332
|
+
</expertise>
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { expect } from 'expect';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import dedent from 'dedent';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import type { Test, TestResultType } from '../../../../src/test-plan.ts';
|
|
7
|
+
import { TestResult } from '../../../../src/test-plan.ts';
|
|
8
|
+
import { tag } from '../../../../src/utils/logger.ts';
|
|
9
|
+
import type { ApiClient } from '../api-client.ts';
|
|
10
|
+
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
11
|
+
|
|
12
|
+
const readResponseData = (responseFile: string) => {
|
|
13
|
+
return JSON.parse(readFileSync(responseFile, 'utf8'));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function summarizeStructure(value: unknown, depth = 0): string {
|
|
17
|
+
if (depth > 2) return '...';
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
if (value.length === 0) return '[]';
|
|
20
|
+
return `[${summarizeStructure(value[0], depth + 1)}]`;
|
|
21
|
+
}
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
const entries = Object.keys(value).slice(0, 10);
|
|
24
|
+
const parts = entries.map((k) => `${k}: ${summarizeStructure((value as any)[k], depth + 1)}`);
|
|
25
|
+
if (Object.keys(value).length > 10) parts.push('...');
|
|
26
|
+
return `{ ${parts.join(', ')} }`;
|
|
27
|
+
}
|
|
28
|
+
return typeof value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createCurlerTools(apiClient: ApiClient, requestState: RequestStore, test: Test, searchSpec?: (query: string) => string) {
|
|
32
|
+
const commitVerification = (label: string, passed: boolean, failDetail: string) => {
|
|
33
|
+
const activeNote = test.startNote(label);
|
|
34
|
+
if (passed) {
|
|
35
|
+
tag('success').log(`${label} passed`);
|
|
36
|
+
activeNote.commit(TestResult.PASSED);
|
|
37
|
+
} else {
|
|
38
|
+
tag('warning').log(`${label} failed — ${failDetail}`);
|
|
39
|
+
activeNote.commit(TestResult.FAILED);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
request: tool({
|
|
45
|
+
description: dedent`
|
|
46
|
+
Make an HTTP request to the API endpoint.
|
|
47
|
+
Returns status, timing, and a preview of the response body (first 500 chars).
|
|
48
|
+
Full response is saved to responseFile — use verifyStructure and verifyData to check it.
|
|
49
|
+
`,
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).describe('HTTP method'),
|
|
52
|
+
path: z.string().describe('API path (e.g., /users, /users/1)'),
|
|
53
|
+
headers: z.record(z.string(), z.string()).optional().describe('Additional headers'),
|
|
54
|
+
body: z.any().optional().describe('Request body (JSON object or string)'),
|
|
55
|
+
queryParams: z.record(z.string(), z.string()).optional().describe('Query parameters'),
|
|
56
|
+
}),
|
|
57
|
+
execute: async (input) => {
|
|
58
|
+
tag('step').log(`${input.method} ${input.path}`);
|
|
59
|
+
const activeNote = test.startNote(`${input.method} ${input.path}`);
|
|
60
|
+
|
|
61
|
+
const result = await apiClient.request({
|
|
62
|
+
method: input.method,
|
|
63
|
+
path: input.path,
|
|
64
|
+
headers: input.headers,
|
|
65
|
+
body: input.body,
|
|
66
|
+
queryParams: input.queryParams,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
requestState.addRequest(result);
|
|
70
|
+
|
|
71
|
+
if (result.error) {
|
|
72
|
+
tag('error').log(`${input.method} ${input.path} > Network error: ${result.error}`);
|
|
73
|
+
activeNote.commit(TestResult.FAILED);
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: result.error,
|
|
77
|
+
curlCommand: result.toCurlCommand(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const statusLine = `${result.status} ${result.statusText}`;
|
|
82
|
+
const bodyString = input.body ? (typeof input.body === 'string' ? input.body : JSON.stringify(input.body, null, 2)) : undefined;
|
|
83
|
+
test.addStep(`${input.method} ${input.path} > ${statusLine} (${result.timing}ms)`, result.timing, undefined, undefined, bodyString, [result.responseFile]);
|
|
84
|
+
|
|
85
|
+
if (result.status >= 400) {
|
|
86
|
+
tag('error').log(`${input.method} ${input.path} > ${statusLine} (${result.timing}ms)`);
|
|
87
|
+
activeNote.commit(TestResult.FAILED);
|
|
88
|
+
} else {
|
|
89
|
+
tag('success').log(`${input.method} ${input.path} > ${statusLine} (${result.timing}ms)`);
|
|
90
|
+
activeNote.commit(TestResult.PASSED);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (bodyString) {
|
|
94
|
+
tag('multiline').log(`Request body:\n${bodyString}`);
|
|
95
|
+
}
|
|
96
|
+
tag('multiline').log(`Response body:\n${result.rawResponseBody}`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
status: result.status,
|
|
101
|
+
statusText: result.statusText,
|
|
102
|
+
timing: result.timing,
|
|
103
|
+
responsePreview: result.rawResponseBody.substring(0, 500),
|
|
104
|
+
responseFile: result.responseFile,
|
|
105
|
+
curlCommand: result.toCurlCommand(),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
...(searchSpec
|
|
111
|
+
? {
|
|
112
|
+
schemaFor: tool({
|
|
113
|
+
description: dedent`
|
|
114
|
+
Search the API spec for endpoints matching a keyword.
|
|
115
|
+
Returns endpoint paths, HTTP methods, and schema definitions.
|
|
116
|
+
Use to discover related endpoints when you need to set up test data or find dependencies.
|
|
117
|
+
Pass "*" to list all available endpoints (paths and methods only).
|
|
118
|
+
`,
|
|
119
|
+
inputSchema: z.object({
|
|
120
|
+
query: z.string().describe('Keyword to search for in endpoint paths (e.g., "comments", "users"). Use "*" to list all endpoints.'),
|
|
121
|
+
}),
|
|
122
|
+
execute: async ({ query }) => {
|
|
123
|
+
tag('step').log(`Schema lookup: ${query}`);
|
|
124
|
+
return { result: searchSpec(query) };
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
|
|
130
|
+
verifyStructure: tool({
|
|
131
|
+
description: dedent`
|
|
132
|
+
Verify response JSON structure using a Zod schema.
|
|
133
|
+
Write a JS expression that returns a Zod schema (z is available).
|
|
134
|
+
Example schema: "z.object({ id: z.number(), name: z.string(), items: z.array(z.object({ title: z.string() })) })"
|
|
135
|
+
Returns validation errors if the response doesn't match.
|
|
136
|
+
On success, returns a "structure" field showing the actual response shape — use it to write correct verifyData assertions.
|
|
137
|
+
`,
|
|
138
|
+
inputSchema: z.object({
|
|
139
|
+
responseFile: z.string().describe('Path to the response JSON file'),
|
|
140
|
+
schema: z.string().describe('JS expression returning a Zod schema, e.g. "z.object({ id: z.number(), name: z.string() })"'),
|
|
141
|
+
}),
|
|
142
|
+
execute: async ({ responseFile, schema: schemaCode }) => {
|
|
143
|
+
let data: unknown;
|
|
144
|
+
try {
|
|
145
|
+
data = readResponseData(responseFile);
|
|
146
|
+
} catch (e: any) {
|
|
147
|
+
commitVerification('Structure check: failed to read response', false, e.message);
|
|
148
|
+
return { passed: false, errors: [e.message] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const schemaObj = new Function('z', `return ${schemaCode}`)(z);
|
|
153
|
+
const result = schemaObj.safeParse(data);
|
|
154
|
+
|
|
155
|
+
if (result.success) {
|
|
156
|
+
commitVerification('Structure check: ✓ schema valid', true, '');
|
|
157
|
+
const structure = summarizeStructure(data);
|
|
158
|
+
return { passed: true, errors: [], structure };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const errors = result.error.issues.map((i: any) => `${i.path.join('.')}: ${i.message}`);
|
|
162
|
+
commitVerification(`Structure check: ✗ ${errors.join(', ')}`, false, errors.join('; '));
|
|
163
|
+
return { passed: false, errors };
|
|
164
|
+
} catch (e: any) {
|
|
165
|
+
commitVerification(`Structure check: schema error — ${e.message}`, false, e.message);
|
|
166
|
+
return { passed: false, errors: [e.message] };
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
verifyData: tool({
|
|
172
|
+
description: dedent`
|
|
173
|
+
Verify specific values in the response JSON using expect() assertions.
|
|
174
|
+
Each assertion is a JS expression with "response" (full parsed JSON body) and "expect" (Jest expect) available.
|
|
175
|
+
"response" is the entire parsed JSON — access nested fields accordingly.
|
|
176
|
+
Example assertions:
|
|
177
|
+
- "expect(response.name).toBe('Test Suite')"
|
|
178
|
+
- "expect(response.items).toHaveLength(3)"
|
|
179
|
+
- "expect(response).toHaveProperty('id')"
|
|
180
|
+
- "expect(response.count).toBeGreaterThan(0)"
|
|
181
|
+
- "expect(response.data.tags).toContain('urgent')"
|
|
182
|
+
- "expect(response).toMatchObject({ status: 'active' })"
|
|
183
|
+
`,
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
responseFile: z.string().describe('Path to the response JSON file'),
|
|
186
|
+
assertions: z.array(z.string()).describe('JS expressions using expect(response) — e.g. "expect(response.id).toBe(1)"'),
|
|
187
|
+
}),
|
|
188
|
+
execute: async ({ responseFile, assertions }) => {
|
|
189
|
+
let response: unknown;
|
|
190
|
+
try {
|
|
191
|
+
response = readResponseData(responseFile);
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
commitVerification('Data check: failed to read response', false, e.message);
|
|
194
|
+
return { passed: false, results: [{ code: '<read file>', passed: false, error: e.message }] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const results: Array<{ code: string; passed: boolean; error?: string }> = [];
|
|
198
|
+
for (const code of assertions) {
|
|
199
|
+
try {
|
|
200
|
+
new Function('expect', 'response', code)(expect, response);
|
|
201
|
+
results.push({ code, passed: true });
|
|
202
|
+
} catch (e: any) {
|
|
203
|
+
results.push({ code, passed: false, error: e.message });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const passed = results.every((r) => r.passed);
|
|
208
|
+
const okResults = results.filter((r) => r.passed);
|
|
209
|
+
const failedResults = results.filter((r) => !r.passed);
|
|
210
|
+
let label = 'Data check:';
|
|
211
|
+
if (okResults.length > 0) label += ` ✓ ${okResults.map((r) => r.code).join(', ')}`;
|
|
212
|
+
if (failedResults.length > 0) label += ` ✗ ${failedResults.map((f) => `${f.code}: ${f.error}`).join(', ')}`;
|
|
213
|
+
|
|
214
|
+
commitVerification(label, passed, failedResults.map((f) => `${f.code}: ${f.error}`).join('; '));
|
|
215
|
+
|
|
216
|
+
return { passed, results };
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
record: tool({
|
|
221
|
+
description: dedent`
|
|
222
|
+
Record a note or observation during testing.
|
|
223
|
+
Use status "success" for achieved outcomes, "fail" for failures.
|
|
224
|
+
`,
|
|
225
|
+
inputSchema: z.object({
|
|
226
|
+
note: z.string().describe('Observation or finding'),
|
|
227
|
+
status: z.enum(['success', 'fail']).optional().describe('Outcome status'),
|
|
228
|
+
}),
|
|
229
|
+
execute: async ({ note, status }) => {
|
|
230
|
+
let mappedStatus: TestResultType = null;
|
|
231
|
+
if (status === 'success') mappedStatus = TestResult.PASSED;
|
|
232
|
+
else if (status === 'fail') mappedStatus = TestResult.FAILED;
|
|
233
|
+
|
|
234
|
+
test.addNote(note, mappedStatus);
|
|
235
|
+
|
|
236
|
+
if (status === 'success') {
|
|
237
|
+
tag('success').log(`${note}`);
|
|
238
|
+
} else if (status === 'fail') {
|
|
239
|
+
tag('warning').log(`${note}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { recorded: true };
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
|
|
246
|
+
finish: tool({
|
|
247
|
+
description: dedent`
|
|
248
|
+
Mark the test as complete. Use when all test goals are achieved.
|
|
249
|
+
`,
|
|
250
|
+
inputSchema: z.object({
|
|
251
|
+
summary: z.string().describe('Summary of what was tested and results'),
|
|
252
|
+
}),
|
|
253
|
+
execute: async ({ summary }) => {
|
|
254
|
+
test.summary = summary;
|
|
255
|
+
test.addNote(`Test complete: ${summary}`, TestResult.PASSED);
|
|
256
|
+
test.finish(TestResult.PASSED);
|
|
257
|
+
tag('success').log(`Test finished: ${summary}`);
|
|
258
|
+
return { finished: true };
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
|
|
262
|
+
stop: tool({
|
|
263
|
+
description: dedent`
|
|
264
|
+
Abort the test. Use when the scenario cannot be completed.
|
|
265
|
+
`,
|
|
266
|
+
inputSchema: z.object({
|
|
267
|
+
reason: z.string().describe('Why the test cannot continue'),
|
|
268
|
+
}),
|
|
269
|
+
execute: async ({ reason }) => {
|
|
270
|
+
test.summary = `Test stopped: ${reason}`;
|
|
271
|
+
test.addNote(`Test stopped: ${reason}`, TestResult.FAILED);
|
|
272
|
+
test.finish(TestResult.FAILED);
|
|
273
|
+
tag('warning').log(`Test stopped: ${reason}`);
|
|
274
|
+
return { stopped: true };
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
}
|