Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
repeating the same rejected request.
- [Apibot] API test scenarios no longer mutate or delete records discovered as sample data. Chief plans a
scenario-owned target for destructive checks, and Curler stops when it cannot create one safely.
- [Navigator] A verification that could not run because the model call itself failed is now reported as
a failure, naming the error. It used to be swallowed and reported as a claim no assertion could
express, so a rate limit or a timed-out request reached Tester as a verdict about the page, and
Tester rewrote a correct assertion and asked again. Each such claim also retried the model up to
three more times on top of the retries the request already does, which made a rate limit worse
rather than passing it on.
- [Navigator] A verification is no longer reported as impossible to express when the answer shows a
snippet of page markup before its assertions. A code block written in any language other than
JavaScript used to shift the reading of every later block, so all the assertions the model had
Expand Down
5 changes: 1 addition & 4 deletions src/ai/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ class Navigator implements Agent {
const cachedVerification = actionResult.getVerification(message);
if (cachedVerification !== null) {
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
}

const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
Expand Down Expand Up @@ -832,9 +832,6 @@ class Navigator implements Agent {
observability: {
agent: 'navigator',
},
catch: async (error) => {
debugLog(error);
},
}
);
} finally {
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/navigator-verify-failure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'bun:test';
import { Navigator } from '../../src/ai/navigator.ts';

function createNavigator(invokeConversation: () => Promise<any>) {
const navigator = Object.create(Navigator.prototype) as any;
navigator.systemPrompt = 'system';
navigator.knowledgeTracker = { renderRelevantContext: () => '' };
navigator.experienceTracker = { renderExperienceTocFor: () => '' };
navigator.stateManager = { updateState: () => {} };
navigator.config = { playwright: {}, ai: { agents: { navigator: { verifyAttempts: 3, verifyTimeout: 1000 } } } };
navigator.explorer = {
page: null,
action: () => ({
assertionSteps: [],
exitIframe: async () => {},
attempt: async () => true,
}),
};
navigator.buildExperienceTools = () => ({});
navigator.provider = {
startConversation: () => ({ addUserText: () => {} }),
invokeConversation,
};
return navigator as Navigator;
}

function createActionResult() {
return {
url: '/widgets',
isInsideIframe: false,
verifications: {},
getVerification: () => null,
addVerification: () => {},
toAiContext: () => '<url>/widgets</url>',
combinedHtml: async () => '<html></html>',
} as any;
}

describe('Navigator.verifyState', () => {
it('reports a failed AI call as a failure instead of an unexpressible claim', async () => {
const navigator = createNavigator(async () => {
throw new Error('Rate limit reached for model on tokens per minute (TPM)');
});

const promise = navigator.verifyState('Widget is visible in the list', createActionResult());

expect(promise).rejects.toThrow('Rate limit reached');
});

it('reports an unexpressible claim when the model answers without assertion code', async () => {
const navigator = createNavigator(async () => ({ response: { text: 'I cannot express that as an assertion.' } }));

const result = await navigator.verifyState('Widget is visible in the list', createActionResult());

expect(result.inexpressible).toBe(true);
expect(result.verified).toBe(false);
});
});
Loading