explorbot 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
package/dist/src/ai/driller.js
CHANGED
|
@@ -6,10 +6,9 @@ import { setActivity } from "../activity.js";
|
|
|
6
6
|
import { Observability } from "../observability.js";
|
|
7
7
|
import { Plan, Test, TestResult } from "../test-plan.js";
|
|
8
8
|
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
|
-
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS,
|
|
9
|
+
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, getComponentScopeHtmlExtractorSource, inferHtmlRole } from "../utils/html.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { loop, pause } from "../utils/loop.js";
|
|
12
|
-
import { OVERLAY_SELECTORS } from "../utils/overlay.js";
|
|
13
12
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
14
13
|
import { eidxInContainer } from "../utils/web-eidx.js";
|
|
15
14
|
import { WebElement } from "../utils/web-element.js";
|
|
@@ -547,8 +546,12 @@ export class Driller extends TaskAgent {
|
|
|
547
546
|
async detectNestedOverlayContext(component, result) {
|
|
548
547
|
if (!result?.pageDiff?.ariaChanges || result.pageDiff.urlChanged)
|
|
549
548
|
return null;
|
|
550
|
-
const
|
|
551
|
-
|
|
549
|
+
const parts = result.pageDiff.htmlParts ?? [];
|
|
550
|
+
let appeared = parts.filter((part) => part.added?.length > 0);
|
|
551
|
+
if (result.pageDiff.areaOfInterest)
|
|
552
|
+
appeared = parts;
|
|
553
|
+
const appearedHtml = appeared.map((part) => part.subtree).join('\n');
|
|
554
|
+
if (!appearedHtml)
|
|
552
555
|
return null;
|
|
553
556
|
const state = this.stateManager.getCurrentState();
|
|
554
557
|
if (!state)
|
|
@@ -561,7 +564,7 @@ export class Driller extends TaskAgent {
|
|
|
561
564
|
Keep the recorded code reusable and include the parent-opening action when the nested element requires the overlay to be open.
|
|
562
565
|
|
|
563
566
|
<overlay_html>
|
|
564
|
-
${
|
|
567
|
+
${appearedHtml}
|
|
565
568
|
</overlay_html>
|
|
566
569
|
|
|
567
570
|
<current_page_aria>
|
|
@@ -570,21 +573,6 @@ export class Driller extends TaskAgent {
|
|
|
570
573
|
</nested_overlay>
|
|
571
574
|
`;
|
|
572
575
|
}
|
|
573
|
-
async getVisibleOverlayHtml() {
|
|
574
|
-
return this.explorer.withPage((page) => page.evaluate(({ extractorSource, config }) => {
|
|
575
|
-
const extract = new Function(`return ${extractorSource}`)();
|
|
576
|
-
return extract(config);
|
|
577
|
-
}, {
|
|
578
|
-
extractorSource: getVisibleOverlayHtmlExtractorSource(),
|
|
579
|
-
config: {
|
|
580
|
-
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
581
|
-
limits: HTML_EXTRACTION_LIMITS,
|
|
582
|
-
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
583
|
-
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
584
|
-
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
585
|
-
},
|
|
586
|
-
}));
|
|
587
|
-
}
|
|
588
576
|
async getComponentScopeHtml(component, originalState) {
|
|
589
577
|
const scopedHtml = await this.explorer.withPage((page) => page.evaluate(({ eidx, extractorSource, config }) => {
|
|
590
578
|
const extract = new Function(`return ${extractorSource}`)();
|
|
@@ -74,6 +74,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
74
74
|
statusText?: undefined;
|
|
75
75
|
category?: undefined;
|
|
76
76
|
errorPreview?: undefined;
|
|
77
|
+
extracted?: undefined;
|
|
77
78
|
} | {
|
|
78
79
|
success: boolean;
|
|
79
80
|
status: number;
|
|
@@ -81,9 +82,11 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
81
82
|
category: ResponseCategory;
|
|
82
83
|
errorPreview: string;
|
|
83
84
|
error?: undefined;
|
|
85
|
+
extracted?: undefined;
|
|
84
86
|
} | {
|
|
85
87
|
success: boolean;
|
|
86
88
|
status: number;
|
|
89
|
+
extracted: Record<string, any>;
|
|
87
90
|
error?: undefined;
|
|
88
91
|
statusText?: undefined;
|
|
89
92
|
category?: undefined;
|
|
@@ -102,6 +105,10 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
102
105
|
}[];
|
|
103
106
|
}, {
|
|
104
107
|
finished: boolean;
|
|
108
|
+
error: string;
|
|
109
|
+
} | {
|
|
110
|
+
finished: boolean;
|
|
111
|
+
error?: undefined;
|
|
105
112
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
106
113
|
stop: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
107
114
|
reason: any;
|
|
@@ -111,6 +118,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
111
118
|
};
|
|
112
119
|
getResult: () => FishermanResult;
|
|
113
120
|
isFinished: () => boolean;
|
|
121
|
+
finishFromText: (text?: string) => void;
|
|
114
122
|
};
|
|
115
123
|
export interface FishermanResult {
|
|
116
124
|
success: boolean;
|
|
@@ -119,6 +127,7 @@ export interface FishermanResult {
|
|
|
119
127
|
type: string;
|
|
120
128
|
id?: string | number;
|
|
121
129
|
title?: string;
|
|
130
|
+
request?: string;
|
|
122
131
|
}>;
|
|
123
132
|
failed: Array<{
|
|
124
133
|
type: string;
|
|
@@ -3,11 +3,23 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { extractEndpointDefinition } from "../api/spec-reader.js";
|
|
5
5
|
import { tag } from "../utils/logger.js";
|
|
6
|
+
import { RequestMap } from "../utils/request-map.js";
|
|
7
|
+
import { isDynamicSegment } from "../utils/url-matcher.js";
|
|
6
8
|
export function createFishermanTools(apiClient, requestStore, opts) {
|
|
7
9
|
let finished = false;
|
|
8
|
-
let result =
|
|
9
|
-
const
|
|
10
|
+
let result = null;
|
|
11
|
+
const ledgerStart = requestStore.getMadeRequests().length;
|
|
12
|
+
const runRequests = () => requestStore.getMadeRequests().slice(ledgerStart);
|
|
13
|
+
const successfulWrites = () => runRequests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400);
|
|
14
|
+
const getResult = () => result ?? synthesizeResult(runRequests(), successfulWrites(), false);
|
|
10
15
|
const isFinished = () => finished;
|
|
16
|
+
const finishFromText = (text) => {
|
|
17
|
+
finished = true;
|
|
18
|
+
const synthesized = synthesizeResult(runRequests(), successfulWrites(), true);
|
|
19
|
+
if (text && synthesized.success)
|
|
20
|
+
synthesized.summary = text;
|
|
21
|
+
result = synthesized;
|
|
22
|
+
};
|
|
11
23
|
const tools = {
|
|
12
24
|
getEndpointSpec: tool({
|
|
13
25
|
description: dedent `
|
|
@@ -69,7 +81,7 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
69
81
|
request: tool({
|
|
70
82
|
description: dedent `
|
|
71
83
|
Make an HTTP request to the API.
|
|
72
|
-
Returns status,
|
|
84
|
+
Returns status, plus IDs and names auto-extracted from the response under 'extracted'.
|
|
73
85
|
`,
|
|
74
86
|
inputSchema: z.object({
|
|
75
87
|
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),
|
|
@@ -106,7 +118,7 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
106
118
|
return {
|
|
107
119
|
success: true,
|
|
108
120
|
status: reqResult.status,
|
|
109
|
-
|
|
121
|
+
extracted,
|
|
110
122
|
};
|
|
111
123
|
},
|
|
112
124
|
}),
|
|
@@ -130,9 +142,30 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
130
142
|
.describe('List of items that could not be created'),
|
|
131
143
|
}),
|
|
132
144
|
execute: async ({ summary, created, failed }) => {
|
|
145
|
+
const writes = successfulWrites();
|
|
146
|
+
if (writes.length === 0) {
|
|
147
|
+
tag('warning').log('Fisherman: finish rejected — no successful write request in this run');
|
|
148
|
+
return { finished: false, error: 'No successful write request was made in this run, so nothing was created. Keep working, or call stop if the data cannot be prepared.' };
|
|
149
|
+
}
|
|
150
|
+
const createdRequests = new RequestMap(writes);
|
|
151
|
+
const verified = [];
|
|
152
|
+
for (const item of created) {
|
|
153
|
+
if (item.id === undefined) {
|
|
154
|
+
verified.push(item);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const request = createdRequests.get(item.id);
|
|
158
|
+
if (!request) {
|
|
159
|
+
tag('warning').log(`Fisherman: dropped unverified created item ${item.type} (id: ${item.id})`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
verified.push({ ...item, request: request.toEndpoint() });
|
|
163
|
+
}
|
|
164
|
+
if (verified.length === 0)
|
|
165
|
+
verified.push(...writes.map(toCreatedItem));
|
|
133
166
|
tag('success').log(`Fisherman done: ${summary}`);
|
|
134
167
|
finished = true;
|
|
135
|
-
result = { success: true, summary, created, failed: failed || [] };
|
|
168
|
+
result = { success: true, summary, created: verified, failed: failed || [] };
|
|
136
169
|
return { finished: true };
|
|
137
170
|
},
|
|
138
171
|
}),
|
|
@@ -149,7 +182,20 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
149
182
|
},
|
|
150
183
|
}),
|
|
151
184
|
};
|
|
152
|
-
return { tools, getResult, isFinished };
|
|
185
|
+
return { tools, getResult, isFinished, finishFromText };
|
|
186
|
+
}
|
|
187
|
+
function synthesizeResult(made, writes, declaredDone) {
|
|
188
|
+
const failures = made.filter((r) => r.status >= 400 || r.error);
|
|
189
|
+
let summary = `Stopped before finishing: ${made.length} requests, ${writes.length} successful writes, ${failures.length} failed`;
|
|
190
|
+
const lastFailure = failures[failures.length - 1];
|
|
191
|
+
if (lastFailure)
|
|
192
|
+
summary += `; last failure: ${lastFailure.toSummary()}`;
|
|
193
|
+
return { success: declaredDone && writes.length > 0, summary, created: writes.map(toCreatedItem), failed: [] };
|
|
194
|
+
}
|
|
195
|
+
function toCreatedItem(write) {
|
|
196
|
+
const { id, title } = write.extractIdAndTitle();
|
|
197
|
+
const segments = write.path.split('/').filter((s) => s && !isDynamicSegment(s));
|
|
198
|
+
return { type: segments[segments.length - 1] || 'item', id, title, request: write.toEndpoint() };
|
|
153
199
|
}
|
|
154
200
|
function responseCategory(status) {
|
|
155
201
|
if (status === 400 || status === 422)
|
|
@@ -9,14 +9,15 @@ export declare class Fisherman implements Agent {
|
|
|
9
9
|
apiClient: ApiClient;
|
|
10
10
|
requestStore: RequestStore;
|
|
11
11
|
specLoader: () => Promise<any | null>;
|
|
12
|
-
|
|
12
|
+
browserHeaderProvider: () => Promise<Record<string, string>>;
|
|
13
13
|
configHeaders: Record<string, string>;
|
|
14
14
|
sessionName?: string;
|
|
15
15
|
baseEndpoint: string;
|
|
16
16
|
spec: any | null;
|
|
17
17
|
mode: 'replicate' | 'achieve' | 'disabled';
|
|
18
18
|
hasApiConfig: boolean;
|
|
19
|
-
|
|
19
|
+
scopeDegraded: boolean;
|
|
20
|
+
constructor(provider: Provider, apiClient: ApiClient, requestStore: RequestStore, specLoader: () => Promise<any | null>, baseEndpoint: string, browserHeaderProvider: () => Promise<Record<string, string>>, configHeaders?: Record<string, string>, hasApiConfig?: boolean);
|
|
20
21
|
isAvailable(): boolean;
|
|
21
22
|
ensureReady(scopeUrl?: string): Promise<void>;
|
|
22
23
|
getEndpointList(scopeUrl?: string): string;
|
|
@@ -25,5 +26,6 @@ export declare class Fisherman implements Agent {
|
|
|
25
26
|
refreshAuth(): Promise<void>;
|
|
26
27
|
buildEndpointList(scopeUrl?: string): string;
|
|
27
28
|
buildSystemPrompt(endpointList: string, toolNames: string[], scopeUrl?: string): string;
|
|
29
|
+
isStuckOnEndpoint(ledgerStart: number): boolean;
|
|
28
30
|
buildTaskPrompt(instructions: string): string;
|
|
29
31
|
}
|
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -7,26 +7,28 @@ import { createFishermanTools } from "./fisherman-tools.js";
|
|
|
7
7
|
import { dataProtectionRules } from "./rules.js";
|
|
8
8
|
const MAX_ITERATIONS = 15;
|
|
9
9
|
const MAX_TOOL_ROUNDTRIPS = 5;
|
|
10
|
+
const REPEATED_FAILURE_LIMIT = 4;
|
|
10
11
|
export class Fisherman {
|
|
11
12
|
emoji = '🎣';
|
|
12
13
|
provider;
|
|
13
14
|
apiClient;
|
|
14
15
|
requestStore;
|
|
15
16
|
specLoader;
|
|
16
|
-
|
|
17
|
+
browserHeaderProvider;
|
|
17
18
|
configHeaders;
|
|
18
19
|
sessionName;
|
|
19
20
|
baseEndpoint;
|
|
20
21
|
spec = null;
|
|
21
22
|
mode = 'disabled';
|
|
22
23
|
hasApiConfig;
|
|
23
|
-
|
|
24
|
+
scopeDegraded = false;
|
|
25
|
+
constructor(provider, apiClient, requestStore, specLoader, baseEndpoint, browserHeaderProvider, configHeaders = {}, hasApiConfig = false) {
|
|
24
26
|
this.provider = provider;
|
|
25
27
|
this.apiClient = apiClient;
|
|
26
28
|
this.requestStore = requestStore;
|
|
27
29
|
this.specLoader = specLoader;
|
|
28
30
|
this.baseEndpoint = baseEndpoint;
|
|
29
|
-
this.
|
|
31
|
+
this.browserHeaderProvider = browserHeaderProvider;
|
|
30
32
|
this.configHeaders = configHeaders;
|
|
31
33
|
this.hasApiConfig = hasApiConfig;
|
|
32
34
|
this.mode = hasApiConfig ? 'achieve' : 'replicate';
|
|
@@ -59,17 +61,17 @@ export class Fisherman {
|
|
|
59
61
|
}
|
|
60
62
|
await this.refreshAuth();
|
|
61
63
|
debugLog(`auth headers: ${Object.keys(this.apiClient.getHeaders()).join(', ')}`);
|
|
62
|
-
const { tools, getResult, isFinished } = createFishermanTools(this.apiClient, this.requestStore, {
|
|
64
|
+
const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, {
|
|
63
65
|
spec: this.spec,
|
|
64
66
|
baseEndpoint: this.baseEndpoint,
|
|
65
67
|
});
|
|
68
|
+
const ledgerStart = this.requestStore.getMadeRequests().length;
|
|
66
69
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(endpointList, Object.keys(tools), scopeUrl), 'fisherman');
|
|
67
70
|
conversation.addUserText(this.buildTaskPrompt(instructions));
|
|
68
71
|
await loop(async ({ stop, iteration }) => {
|
|
69
72
|
debugLog(`iteration ${iteration}`);
|
|
70
73
|
const invokeResult = await this.provider.invokeConversation(conversation, tools, {
|
|
71
74
|
maxToolRoundtrips: MAX_TOOL_ROUNDTRIPS,
|
|
72
|
-
toolChoice: 'required',
|
|
73
75
|
agentName: 'fisherman',
|
|
74
76
|
});
|
|
75
77
|
debugLog(`iteration ${iteration} done, text: ${invokeResult?.response?.text?.slice(0, 200) || '(none)'}`);
|
|
@@ -77,6 +79,17 @@ export class Fisherman {
|
|
|
77
79
|
stop();
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
82
|
+
if (!invokeResult?.toolExecutions?.length) {
|
|
83
|
+
debugLog('no tool call in this turn — treating as finish');
|
|
84
|
+
finishFromText(invokeResult?.response?.text);
|
|
85
|
+
stop();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (this.isStuckOnEndpoint(ledgerStart)) {
|
|
89
|
+
tag('warning').log('Fisherman: repeated failures on the same endpoint — stopping');
|
|
90
|
+
stop();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
80
93
|
if (iteration >= MAX_ITERATIONS) {
|
|
81
94
|
tag('warning').log('Fisherman: max iterations reached');
|
|
82
95
|
stop();
|
|
@@ -114,41 +127,40 @@ export class Fisherman {
|
|
|
114
127
|
this.mode = 'disabled';
|
|
115
128
|
}
|
|
116
129
|
async refreshAuth() {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (this.mode === 'replicate') {
|
|
131
|
+
const xhrHeaders = this.requestStore.extractAuthHeaders();
|
|
132
|
+
if (Object.keys(xhrHeaders).length > 0) {
|
|
133
|
+
this.apiClient.setHeaders(xhrHeaders);
|
|
134
|
+
}
|
|
135
|
+
const browserHeaders = await this.browserHeaderProvider();
|
|
136
|
+
if (Object.keys(browserHeaders).length > 0) {
|
|
137
|
+
this.apiClient.setHeaders(browserHeaders);
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
if (Object.keys(this.configHeaders).length > 0) {
|
|
126
141
|
this.apiClient.setHeaders(this.configHeaders);
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
buildEndpointList(scopeUrl) {
|
|
145
|
+
this.scopeDegraded = false;
|
|
130
146
|
if (this.mode === 'achieve' && this.spec) {
|
|
131
147
|
const specEndpoints = listAllEndpoints(this.spec, this.baseEndpoint);
|
|
132
148
|
if (specEndpoints)
|
|
133
149
|
return specEndpoints;
|
|
134
150
|
}
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const lines = [];
|
|
141
|
-
for (const req of writeRequests) {
|
|
142
|
-
const key = `${req.method} ${req.path}`;
|
|
143
|
-
if (seen.has(key))
|
|
144
|
-
continue;
|
|
145
|
-
seen.add(key);
|
|
146
|
-
lines.push(key);
|
|
147
|
-
}
|
|
148
|
-
return lines.join('\n');
|
|
151
|
+
const scoped = this.requestStore.toEndpointList(scopeUrl || '/');
|
|
152
|
+
if (scoped)
|
|
153
|
+
return scoped;
|
|
154
|
+
this.scopeDegraded = true;
|
|
155
|
+
return this.requestStore.toEndpointList();
|
|
149
156
|
}
|
|
150
157
|
buildSystemPrompt(endpointList, toolNames, scopeUrl) {
|
|
151
|
-
|
|
158
|
+
let scopeBlock = '';
|
|
159
|
+
if (scopeUrl) {
|
|
160
|
+
scopeBlock = `\n\nSCOPE: You are operating within ${scopeUrl}.\nAll created items must belong to this scope.`;
|
|
161
|
+
if (this.scopeDegraded)
|
|
162
|
+
scopeBlock += '\nThe endpoint list could not be narrowed to this scope and may include endpoints belonging to other scopes. Before writing, confirm the target belongs to this scope.';
|
|
163
|
+
}
|
|
152
164
|
return dedent `
|
|
153
165
|
You are Fisherman — a data preparation agent. You create test data by making API requests.
|
|
154
166
|
|
|
@@ -172,11 +184,20 @@ export class Fisherman {
|
|
|
172
184
|
- Chain requests logically — create parent resources before children
|
|
173
185
|
- Use the response category and error text to decide what failed: validation requires corrected data, authorization requires valid access, not_found requires a valid path or parent, and conflict requires resolving the conflicting state
|
|
174
186
|
- Retry temporary or server failures once. Retry other failures only when the specification or error text gives a concrete correction
|
|
187
|
+
- Create only the resource types that were requested. If no endpoint creates a requested type, call stop — never create a different type as a substitute
|
|
175
188
|
- Use realistic but unique data for each item (vary names, titles)
|
|
176
189
|
|
|
177
190
|
${dataProtectionRules}
|
|
178
191
|
`;
|
|
179
192
|
}
|
|
193
|
+
isStuckOnEndpoint(ledgerStart) {
|
|
194
|
+
const made = this.requestStore.getMadeRequests().slice(ledgerStart);
|
|
195
|
+
if (made.length < REPEATED_FAILURE_LIMIT)
|
|
196
|
+
return false;
|
|
197
|
+
const recent = made.slice(-REPEATED_FAILURE_LIMIT);
|
|
198
|
+
const first = recent[0];
|
|
199
|
+
return recent.every((r) => (r.status >= 400 || r.error) && r.method === first.method && r.path === first.path);
|
|
200
|
+
}
|
|
180
201
|
buildTaskPrompt(instructions) {
|
|
181
202
|
return dedent `
|
|
182
203
|
Prepare the following test data:
|
|
@@ -45,7 +45,7 @@ export function WithCodeceptJS(Base) {
|
|
|
45
45
|
lines.push('');
|
|
46
46
|
lines.push(`Feature('${escapeString(plan.title)}')`);
|
|
47
47
|
lines.push('');
|
|
48
|
-
const startUrl = plan.
|
|
48
|
+
const startUrl = plan.startUrl;
|
|
49
49
|
if (startUrl) {
|
|
50
50
|
lines.push('Before(({ I }) => {');
|
|
51
51
|
lines.push(` I.amOnPage('${escapeString(startUrl)}');`);
|
|
@@ -78,7 +78,7 @@ export function WithPlaywright(Base) {
|
|
|
78
78
|
lines.push(`import { test, expect } from '@playwright/test';`);
|
|
79
79
|
lines.push('');
|
|
80
80
|
lines.push(`test.describe('${escapeString(plan.title)}', () => {`);
|
|
81
|
-
const startUrl = plan.
|
|
81
|
+
const startUrl = plan.startUrl;
|
|
82
82
|
if (startUrl) {
|
|
83
83
|
lines.push(' test.beforeEach(async ({ page }) => {');
|
|
84
84
|
lines.push(` await page.goto('${escapeString(startUrl)}');`);
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -522,7 +522,7 @@ class Navigator {
|
|
|
522
522
|
return null;
|
|
523
523
|
}
|
|
524
524
|
const currentActionResult = actionResult || ActionResult.fromState(state);
|
|
525
|
-
const research = Researcher.getCachedResearch(
|
|
525
|
+
const research = Researcher.getCachedResearch(currentActionResult) || '';
|
|
526
526
|
const combinedHtml = await currentActionResult.combinedHtml();
|
|
527
527
|
const history = stateManager.getStateHistory();
|
|
528
528
|
const visitCounts = new Map();
|
package/dist/src/ai/pilot.d.ts
CHANGED
package/dist/src/ai/pilot.js
CHANGED
|
@@ -687,6 +687,8 @@ export class Pilot {
|
|
|
687
687
|
parts.push(`"${c.title}"`);
|
|
688
688
|
if (c.id)
|
|
689
689
|
parts.push(`(id: ${c.id})`);
|
|
690
|
+
if (c.request)
|
|
691
|
+
parts.push(`via ${c.request}`);
|
|
690
692
|
return parts.join(' ');
|
|
691
693
|
});
|
|
692
694
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
@@ -741,7 +743,16 @@ export class Pilot {
|
|
|
741
743
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
742
744
|
const focusArea = state.overlay;
|
|
743
745
|
if (focusArea.detected) {
|
|
744
|
-
|
|
746
|
+
let line = `modal: ${focusArea.name || focusArea.type}`;
|
|
747
|
+
if (focusArea.root)
|
|
748
|
+
line += ` (root: ${focusArea.root})`;
|
|
749
|
+
lines.push(line);
|
|
750
|
+
}
|
|
751
|
+
else if (focusArea.present) {
|
|
752
|
+
let line = `region: ${focusArea.name || 'unnamed'} (inline`;
|
|
753
|
+
if (focusArea.root)
|
|
754
|
+
line += `, root: ${focusArea.root}`;
|
|
755
|
+
lines.push(`${line})`);
|
|
745
756
|
}
|
|
746
757
|
else {
|
|
747
758
|
lines.push('modal: none');
|
|
@@ -1024,6 +1035,7 @@ export class Pilot {
|
|
|
1024
1035
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1025
1036
|
- Click failed + button in "disabled buttons" → required field missing. Instruct fill first.
|
|
1026
1037
|
- "modal: none" but Tester targets a modal → modal closed; re-trigger.
|
|
1038
|
+
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1027
1039
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
1028
1040
|
- MultipleElementsFound → xpathCheck() to identify the right one, then precise locator or visualClick().
|
|
1029
1041
|
- Wrong page (settings vs feature) → getVisitedStates() then back() or reset(). Don't try breadcrumbs (SPA back-nav is unreliable).
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -96,11 +96,15 @@ export class Planner extends PlannerBase {
|
|
|
96
96
|
Tests must be relevant to the page
|
|
97
97
|
Tests must be achievable from UI
|
|
98
98
|
Tests must be verifiable from UI
|
|
99
|
-
|
|
99
|
+
One test verifies ONE business operation: the steps that reach it, the action itself, and its verification.
|
|
100
|
+
Steps that only reach the action — opening a form, expanding a panel, creating or locating the item to act on — belong to that test. A second operation with its own verification does not.
|
|
100
101
|
Bad: "Open delete dropdown" + "Confirm deletion" — these are ONE test, not two.
|
|
101
102
|
Bad: "Search for X" + "Verify search results" — searching and verifying is ONE test.
|
|
102
103
|
Bad: "Leave field empty" + "Click submit" — that's one negative test, not two.
|
|
103
|
-
|
|
104
|
+
Bad: "Create a record, rename it, delete it" — three verified operations, so THREE tests, not one.
|
|
105
|
+
Good: "Rename existing record and verify the new title" — ONE test; creating is skipped, we assume record already exists, only the rename is verified.
|
|
106
|
+
You may rely on another test having run first in case we deal with empty state and no relevant data was created yet and we expect another our test creates it
|
|
107
|
+
When the page reports a record is missing or unavailable, it is not a testable surface — plan list-level or recovery behavior instead of operations on that record.${featureDirective}${focusExistingDataDirective}
|
|
104
108
|
</task>
|
|
105
109
|
|
|
106
110
|
${customPrompt || ''}
|
|
@@ -166,9 +170,6 @@ export class Planner extends PlannerBase {
|
|
|
166
170
|
if (!aiResult?.object?.scenarios) {
|
|
167
171
|
throw new Error('No tasks were created successfully');
|
|
168
172
|
}
|
|
169
|
-
if (aiResult.object.scenarios.length === 0 && !this.currentPlan) {
|
|
170
|
-
throw new Error('No tasks were created successfully');
|
|
171
|
-
}
|
|
172
173
|
const defaultStartUrl = this.getDefaultStartUrl(state);
|
|
173
174
|
const fromPlanning = aiResult.object.scenarios.map((s) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
|
|
174
175
|
return { tests: fromPlanning, planName: aiResult.object.planName };
|
|
@@ -295,6 +296,7 @@ export class Planner extends PlannerBase {
|
|
|
295
296
|
<task>
|
|
296
297
|
Based on the page research, create ${this.MIN_TASKS}-${this.MAX_TASKS} exploratory testing scenarios.
|
|
297
298
|
For each scenario provide specific steps and expected outcomes.
|
|
299
|
+
Exception: if the page reports the requested resource is missing, shows a failure state, or holds no content and no controls, return an empty scenarios list. Never invent tests for a page with nothing to exercise.
|
|
298
300
|
</task>
|
|
299
301
|
|
|
300
302
|
<rules>
|
|
@@ -308,13 +310,13 @@ export class Planner extends PlannerBase {
|
|
|
308
310
|
Focus on error or success messages as outcome.
|
|
309
311
|
Focus on URL page change or data persistency after page reload.
|
|
310
312
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
311
|
-
|
|
313
|
+
Plan CRUD operations in order: create, read, update, delete.
|
|
312
314
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
313
315
|
When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
|
|
314
316
|
If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
|
|
315
317
|
If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
|
|
316
318
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
317
|
-
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible
|
|
319
|
+
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
|
|
318
320
|
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
|
|
319
321
|
Do not assume hidden data exists just because a control is present.
|
|
320
322
|
For scenarios that act on existing items or search/filter by existing values, use only item names or values visible in research, visited pages, or prior observed flows.
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -385,9 +385,19 @@ export class Provider {
|
|
|
385
385
|
const config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
|
|
386
386
|
let attemptMessages = messages;
|
|
387
387
|
let invalidRequestFeedbackAdded = false;
|
|
388
|
+
const executedStepMessages = [];
|
|
388
389
|
try {
|
|
389
390
|
const response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
390
|
-
const
|
|
391
|
+
const stepMessages = [];
|
|
392
|
+
const onStepEnd = (step) => {
|
|
393
|
+
stepMessages.push(...(step.response?.messages || []));
|
|
394
|
+
};
|
|
395
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000).catch((error) => {
|
|
396
|
+
if (stepMessages.length > 0) {
|
|
397
|
+
tag('warning').log(`Keeping ${stepMessages.length} messages from tool steps that already ran before the failure`);
|
|
398
|
+
executedStepMessages.push(...stepMessages);
|
|
399
|
+
attemptMessages = [...attemptMessages, ...stepMessages];
|
|
400
|
+
}
|
|
391
401
|
if (!invalidRequestFeedbackAdded) {
|
|
392
402
|
const amended = withInvalidRequestFeedback(attemptMessages, error);
|
|
393
403
|
invalidRequestFeedbackAdded = amended !== attemptMessages;
|
|
@@ -403,6 +413,7 @@ export class Provider {
|
|
|
403
413
|
return result;
|
|
404
414
|
}, this.getRetryOptions(options)));
|
|
405
415
|
clearActivity();
|
|
416
|
+
withExecutedSteps(response, executedStepMessages);
|
|
406
417
|
// Log tool usage summary
|
|
407
418
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
408
419
|
responseLog(response.toolCalls);
|
|
@@ -416,14 +427,15 @@ export class Provider {
|
|
|
416
427
|
catch (error) {
|
|
417
428
|
clearActivity();
|
|
418
429
|
if (error?.message?.includes('Tool choice is required')) {
|
|
419
|
-
return { text: '', toolCalls: [], toolResults: [],
|
|
430
|
+
return { text: '', toolCalls: [], toolResults: [], responseMessages: executedStepMessages, usage: null };
|
|
420
431
|
}
|
|
421
432
|
if (error?.name === 'AbortError')
|
|
422
433
|
throw error;
|
|
423
434
|
if (error instanceof ContextLengthError)
|
|
424
435
|
throw error;
|
|
425
436
|
if (Provider.isContextLengthError(error)) {
|
|
426
|
-
|
|
437
|
+
const recovered = await this.recoverFromContextLength(error, attemptMessages, options, (m, o) => this.generateWithTools(m, model, tools, o));
|
|
438
|
+
return withExecutedSteps(recovered, executedStepMessages);
|
|
427
439
|
}
|
|
428
440
|
if (error.constructor?.name === 'AI_APICallError') {
|
|
429
441
|
responseLog(error.message);
|
|
@@ -630,6 +642,11 @@ function repairToolCall(options) {
|
|
|
630
642
|
return repairChannelMarker(options);
|
|
631
643
|
return repairHarmonyChannel(options);
|
|
632
644
|
}
|
|
645
|
+
function withExecutedSteps(result, executed) {
|
|
646
|
+
if (executed.length === 0)
|
|
647
|
+
return result;
|
|
648
|
+
return Object.defineProperty(result, 'responseMessages', { value: [...executed, ...(result.responseMessages || [])], configurable: true, enumerable: true });
|
|
649
|
+
}
|
|
633
650
|
function withInvalidRequestFeedback(messages, error) {
|
|
634
651
|
if (!(error instanceof APICallError) || error.statusCode !== 400)
|
|
635
652
|
return messages;
|
|
@@ -63,9 +63,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
63
63
|
}
|
|
64
64
|
async researchOverlay(current, previous, pageStateHash) {
|
|
65
65
|
const focusArea = current.overlay;
|
|
66
|
-
if (!focusArea.
|
|
67
|
-
return null;
|
|
68
|
-
if (focusArea.type !== 'dialog' && focusArea.type !== 'modal')
|
|
66
|
+
if (!focusArea.present || !focusArea.name)
|
|
69
67
|
return null;
|
|
70
68
|
const cached = getCachedResearch(pageStateHash);
|
|
71
69
|
if (!cached)
|
|
@@ -53,7 +53,9 @@ export class Researcher extends ResearcherBase {
|
|
|
53
53
|
throw new Error('not implemented');
|
|
54
54
|
}
|
|
55
55
|
static getCachedResearch(state) {
|
|
56
|
-
|
|
56
|
+
if (state instanceof ActionResult)
|
|
57
|
+
return getCachedResearch(state.baseHash);
|
|
58
|
+
return getCachedResearch(ActionResult.fromState(state).baseHash);
|
|
57
59
|
}
|
|
58
60
|
getSystemMessage() {
|
|
59
61
|
const currentUrl = this.stateManager.getCurrentState()?.url;
|
|
@@ -71,7 +73,7 @@ export class Researcher extends ResearcherBase {
|
|
|
71
73
|
const maxRetries = this.config.ai?.agents?.researcher?.retries ?? 2;
|
|
72
74
|
let retriesLeft = opts._retriesLeft ?? maxRetries;
|
|
73
75
|
this.actionResult = ActionResult.fromState(state);
|
|
74
|
-
const stateHash =
|
|
76
|
+
const stateHash = this.actionResult.baseHash;
|
|
75
77
|
const researchState = { ...state, hash: stateHash };
|
|
76
78
|
if (!force && stateHash) {
|
|
77
79
|
const cached = getCachedResearch(stateHash);
|
|
@@ -223,7 +225,7 @@ export class Researcher extends ResearcherBase {
|
|
|
223
225
|
}
|
|
224
226
|
if (!interrupted() && deep) {
|
|
225
227
|
try {
|
|
226
|
-
await this.performDeepAnalysis(
|
|
228
|
+
await this.performDeepAnalysis(researchState, result);
|
|
227
229
|
}
|
|
228
230
|
catch (err) {
|
|
229
231
|
tag('warning').log(`Deep analysis failed, continuing with best-effort research: ${err instanceof Error ? err.message : err}`);
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -145,11 +145,11 @@ export const protectionRule = dedent `
|
|
|
145
145
|
|
|
146
146
|
Pre-existing data on the page belongs to the application, not the test.
|
|
147
147
|
Items that were not created inside the current test scenario must not be deleted, removed, emptied, reset, archived, or otherwise destroyed.
|
|
148
|
-
If a scenario needs to verify destructive behaviour, the same scenario must first create
|
|
148
|
+
If a scenario needs to verify destructive behaviour, the same scenario must first create its own target and then destroy that specific target — never operate on data that was already there when the test started.
|
|
149
149
|
|
|
150
150
|
The resource that the current page URL represents is "under test".
|
|
151
151
|
The test must not destroy the resource it is running against — doing so invalidates every subsequent scenario that starts on the same URL.
|
|
152
|
-
Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on
|
|
152
|
+
Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on children created within the scenario itself.
|
|
153
153
|
</important>
|
|
154
154
|
`;
|
|
155
155
|
export const dataProtectionRules = dedent `
|
|
@@ -164,7 +164,7 @@ export const dataProtectionRules = dedent `
|
|
|
164
164
|
filter, tab, or list-inspection constraint. Use visible existing data when it is available.
|
|
165
165
|
If no suitable data exists, report the missing precondition instead of creating data.
|
|
166
166
|
|
|
167
|
-
Destructive actions are allowed only against
|
|
167
|
+
Destructive actions are allowed only against data created by the current scenario
|
|
168
168
|
or prepared for that scenario by Fisherman/API preconditions. Existing application data must
|
|
169
169
|
remain unchanged.
|
|
170
170
|
</data_protection_rules>
|