From ebed286d4e0a37c607dc6c226b431569bcf21db8 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 1 Sep 2026 21:51:03 +0300 Subject: [PATCH 1/2] Give Fisherman's run one Haul and thin the finish tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One `Haul` now owns "the requests this run made": it captures the start mark once, after `refreshAuth()`, and both the stuck-detector and the tools read their run views from it. The failure predicate lives once, as `isFailedRequest` in the data tier, and `finish` delegates its verification to `verifyFinish` instead of judging inline. `src/utils/request-map.ts` is gone — `Haul.byId()` replaces it, so `src/utils/` no longer depends on `src/api/`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01B46vQvvc47LJeCmqQxAhme --- src/ai/fisherman-tools.ts | 71 ++++++++++++++++-------------- src/ai/fisherman.ts | 14 +++--- src/api/request-store.ts | 34 ++++++++++++++ src/utils/request-map.ts | 19 -------- tests/unit/fisherman-tools.test.ts | 29 +++++++----- tests/unit/request-store.test.ts | 31 ++++++++++++- 6 files changed, 126 insertions(+), 72 deletions(-) delete mode 100644 src/utils/request-map.ts diff --git a/src/ai/fisherman-tools.ts b/src/ai/fisherman-tools.ts index 19cc0969..a23fd7c2 100644 --- a/src/ai/fisherman-tools.ts +++ b/src/ai/fisherman-tools.ts @@ -3,24 +3,20 @@ import dedent from 'dedent'; import { z } from 'zod'; import type { ApiClient } from '../api/api-client.ts'; import type { RequestResult } from '../api/request-result.ts'; -import type { RequestStore } from '../api/request-store.ts'; +import type { Haul, RequestStore } from '../api/request-store.ts'; import { extractEndpointDefinition } from '../api/spec-reader.ts'; import { tag } from '../utils/logger.ts'; -import { RequestMap } from '../utils/request-map.ts'; import { isDynamicSegment } from '../utils/url-matcher.ts'; -export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, opts: { spec?: any; baseEndpoint?: string }) { +export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: Haul, opts: { spec?: any; baseEndpoint?: string }) { let finished = false; let result: FishermanResult | null = null; - const ledgerStart = requestStore.getMadeRequests().length; - const runRequests = () => requestStore.getMadeRequests().slice(ledgerStart); - const successfulWrites = () => runRequests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400); - const getResult = () => result ?? synthesizeResult(runRequests(), successfulWrites(), false); + const getResult = () => result ?? synthesizeResult(haul, false); const isFinished = () => finished; const finishFromText = (text?: string) => { finished = true; - const synthesized = synthesizeResult(runRequests(), successfulWrites(), true); + const synthesized = synthesizeResult(haul, true); if (text && synthesized.success) synthesized.summary = text; result = synthesized; }; @@ -160,32 +156,12 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request .describe('List of items that could not be created'), }), execute: async ({ summary, created, failed }) => { - const writes = successfulWrites(); - if (writes.length === 0) { - tag('warning').log('Fisherman: finish rejected — no successful write request in this run'); - 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.' }; - } - - const createdRequests = new RequestMap(writes); - - const verified: FishermanResult['created'] = []; - for (const item of created) { - if (item.id === undefined) { - verified.push(item); - continue; - } - const request = createdRequests.get(item.id); - if (!request) { - tag('warning').log(`Fisherman: dropped unverified created item ${item.type} (id: ${item.id})`); - continue; - } - verified.push({ ...item, request: request.toEndpoint() }); - } - if (verified.length === 0) verified.push(...writes.map(toCreatedItem)); + const { result: verified, error } = verifyFinish(haul, { summary, created, failed }); + if (!verified) return { finished: false, error }; tag('success').log(`Fisherman done: ${summary}`); finished = true; - result = { success: true, summary, created: verified, failed: failed || [] }; + result = verified; return { finished: true }; }, }), @@ -207,8 +183,37 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request return { tools, getResult, isFinished, finishFromText }; } -function synthesizeResult(made: RequestResult[], writes: RequestResult[], declaredDone: boolean): FishermanResult { - const failures = made.filter((r) => r.status >= 400 || r.error); +export function verifyFinish(haul: Haul, input: { summary: string; created: FishermanResult['created']; failed?: FishermanResult['failed'] }): { result: FishermanResult | null; error?: string } { + const writes = haul.successfulWrites(); + if (writes.length === 0) { + tag('warning').log('Fisherman: finish rejected — no successful write request in this run'); + return { result: null, 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.' }; + } + + const createdRequests = haul.byId(); + + const verified: FishermanResult['created'] = []; + for (const item of input.created) { + if (item.id === undefined) { + verified.push(item); + continue; + } + const request = createdRequests.get(String(item.id)); + if (!request) { + tag('warning').log(`Fisherman: dropped unverified created item ${item.type} (id: ${item.id})`); + continue; + } + verified.push({ ...item, request: request.toEndpoint() }); + } + if (verified.length === 0) verified.push(...writes.map(toCreatedItem)); + + return { result: { success: true, summary: input.summary, created: verified, failed: input.failed || [] } }; +} + +function synthesizeResult(haul: Haul, declaredDone: boolean): FishermanResult { + const made = haul.requests(); + const writes = haul.successfulWrites(); + const failures = haul.failed(); let summary = `Stopped before finishing: ${made.length} requests, ${writes.length} successful writes, ${failures.length} failed`; const lastFailure = failures[failures.length - 1]; if (lastFailure) summary += `; last failure: ${lastFailure.toSummary()}`; diff --git a/src/ai/fisherman.ts b/src/ai/fisherman.ts index c6dd6e4c..d640e665 100644 --- a/src/ai/fisherman.ts +++ b/src/ai/fisherman.ts @@ -1,6 +1,6 @@ import dedent from 'dedent'; import type { ApiClient } from '../api/api-client.ts'; -import type { RequestStore } from '../api/request-store.ts'; +import { Haul, type RequestStore, isFailedRequest } from '../api/request-store.ts'; import { listAllEndpoints } from '../api/spec-reader.ts'; import { createDebug, tag } from '../utils/logger.ts'; @@ -79,11 +79,11 @@ export class Fisherman implements Agent { await this.refreshAuth(); debugLog(`auth headers: ${Object.keys(this.apiClient.getHeaders()).join(', ')}`); - const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, { + const haul = new Haul(this.requestStore); + const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, haul, { spec: this.spec, baseEndpoint: this.baseEndpoint, }); - const ledgerStart = this.requestStore.getMadeRequests().length; const conversation = this.provider.startConversation(this.buildSystemPrompt(endpointList, Object.keys(tools), scopeUrl), 'fisherman'); conversation.addUserText(this.buildTaskPrompt(instructions)); @@ -109,7 +109,7 @@ export class Fisherman implements Agent { return; } - if (this.isStuckOnEndpoint(ledgerStart)) { + if (this.isStuckOnEndpoint(haul)) { tag('warning').log('Fisherman: repeated failures on the same endpoint — stopping'); stop(); return; @@ -228,12 +228,12 @@ export class Fisherman implements Agent { `; } - private isStuckOnEndpoint(ledgerStart: number): boolean { - const made = this.requestStore.getMadeRequests().slice(ledgerStart); + private isStuckOnEndpoint(haul: Haul): boolean { + const made = haul.requests(); if (made.length < REPEATED_FAILURE_LIMIT) return false; const recent = made.slice(-REPEATED_FAILURE_LIMIT); const first = recent[0]; - return recent.every((r) => (r.status >= 400 || r.error) && r.method === first.method && r.path === first.path); + return recent.every((r) => isFailedRequest(r) && r.method === first.method && r.path === first.path); } private buildTaskPrompt(instructions: string): string { diff --git a/src/api/request-store.ts b/src/api/request-store.ts index 9217921c..4ffbc479 100644 --- a/src/api/request-store.ts +++ b/src/api/request-store.ts @@ -191,6 +191,40 @@ export class RequestStore { } } +export class Haul { + private start: number; + + constructor(private store: RequestStore) { + this.start = store.getMadeRequests().length; + } + + requests(): RequestResult[] { + return this.store.getMadeRequests().slice(this.start); + } + + failed(): RequestResult[] { + return this.requests().filter(isFailedRequest); + } + + successfulWrites(): RequestResult[] { + return this.requests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400); + } + + byId(): Map { + const map = new Map(); + for (const request of this.successfulWrites()) { + const { id } = request.extractIdAndTitle(); + if (id === undefined) continue; + map.set(String(id), request); + } + return map; + } +} + +export function isFailedRequest(request: RequestResult): boolean { + return request.status >= 400 || Boolean(request.error); +} + function normalizePathPattern(urlPath: string): string { return urlPath .split('/') diff --git a/src/utils/request-map.ts b/src/utils/request-map.ts deleted file mode 100644 index 0c320392..00000000 --- a/src/utils/request-map.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RequestResult } from '../api/request-result.ts'; - -export class RequestMap { - private requests = new Map(); - - constructor(requests: RequestResult[] = []) { - for (const request of requests) this.add(request); - } - - add(request: RequestResult): void { - const { id } = request.extractIdAndTitle(); - if (id === undefined) return; - this.requests.set(String(id), request); - } - - get(id: string | number): RequestResult | undefined { - return this.requests.get(String(id)); - } -} diff --git a/tests/unit/fisherman-tools.test.ts b/tests/unit/fisherman-tools.test.ts index 752a572f..e2c94e48 100644 --- a/tests/unit/fisherman-tools.test.ts +++ b/tests/unit/fisherman-tools.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from 'bun:test'; import { createFishermanTools } from '../../src/ai/fisherman-tools.ts'; +import { Haul } from '../../src/api/request-store.ts'; describe('Fisherman tools', () => { it('does not present a rejected capture as a request example', async () => { const captured = { method: 'POST', path: '/plans', status: 400, requestBody: { plan: 'wrong' } }; - const { tools } = createFishermanTools({} as any, store(captured), {}); + const { tools } = fishermanTools({} as any, store(captured), {}); const result: any = await tools.getEndpointSpec.execute({ method: 'POST', path: '/plans' }, {} as any); @@ -16,7 +17,7 @@ describe('Fisherman tools', () => { it('prefers the specification when the captured request was rejected', async () => { const captured = { method: 'POST', path: '/plans', status: 422, requestBody: { plan: 'wrong' } }; const spec = { paths: { '/plans': { post: { requestBody: { required: true } } } } }; - const { tools } = createFishermanTools({} as any, store(captured), { spec }); + const { tools } = fishermanTools({} as any, store(captured), { spec }); const result: any = await tools.getEndpointSpec.execute({ method: 'POST', path: '/plans' }, {} as any); @@ -27,7 +28,7 @@ describe('Fisherman tools', () => { it('keeps a successful captured request as a usable example', async () => { const captured = { method: 'POST', path: '/plans', status: 201, requestBody: { title: 'Plan' } }; - const { tools } = createFishermanTools({} as any, store(captured), {}); + const { tools } = fishermanTools({} as any, store(captured), {}); const result: any = await tools.getEndpointSpec.execute({ method: 'POST', path: '/plans' }, {} as any); @@ -48,7 +49,7 @@ describe('Fisherman tools', () => { const apiClient = { request: async () => ({ status, statusText: String(status), rawResponseBody: '{}', responseBody: null }), }; - const { tools } = createFishermanTools(apiClient as any, store(), {}); + const { tools } = fishermanTools(apiClient as any, store(), {}); const result: any = await tools.request.execute({ method: 'POST', path: '/plans' }, {} as any); @@ -60,7 +61,7 @@ describe('Fisherman tools', () => { const apiClient = { request: async () => ({ status: 201, statusText: 'Created', rawResponseBody: '', responseBody: { id: 7, status: 'draft' } }), }; - const { tools } = createFishermanTools(apiClient as any, store(), {}); + const { tools } = fishermanTools(apiClient as any, store(), {}); const result: any = await tools.request.execute({ method: 'POST', path: '/items' }, {} as any); @@ -71,7 +72,7 @@ describe('Fisherman tools', () => { describe('ledger-derived results', () => { it('rejects finish when no successful write was made in this run', async () => { - const { tools, isFinished } = createFishermanTools({} as any, store(), {}); + const { tools, isFinished } = fishermanTools({} as any, store(), {}); const result: any = await tools.finish.execute({ summary: 'done', created: [{ type: 'suite', id: '1' }] }, {} as any); @@ -82,7 +83,7 @@ describe('ledger-derived results', () => { it('ignores writes made before this run started', async () => { const made = [madeWrite('POST', '/api/suites', 201, { id: 's1' })]; - const { tools, isFinished } = createFishermanTools({} as any, store(undefined, made), {}); + const { tools, isFinished } = fishermanTools({} as any, store(undefined, made), {}); const result: any = await tools.finish.execute({ summary: 'done', created: [{ type: 'suite', id: 's1' }] }, {} as any); @@ -92,7 +93,7 @@ describe('ledger-derived results', () => { it('drops created items whose id no write response returned, keeps verified ones with their request', async () => { const made: any[] = []; - const { tools, getResult } = createFishermanTools({} as any, store(undefined, made), {}); + const { tools, getResult } = fishermanTools({} as any, store(undefined, made), {}); made.push(madeWrite('POST', '/api/suites', 201, { id: 's1', title: 'Suite A' })); await tools.finish.execute( @@ -113,7 +114,7 @@ describe('ledger-derived results', () => { it('reports failure when the loop ends without finish, even after a successful write', async () => { const made: any[] = []; - const { getResult } = createFishermanTools({} as any, store(undefined, made), {}); + const { getResult } = fishermanTools({} as any, store(undefined, made), {}); made.push(madeWrite('POST', '/api/suites', 201, { id: 's1', title: 'Suite A' })); made.push(madeWrite('POST', '/api/tests', 400)); @@ -126,7 +127,7 @@ describe('ledger-derived results', () => { it('reports failure with a reason when the loop ends with no successful writes', async () => { const made: any[] = []; - const { getResult } = createFishermanTools({} as any, store(undefined, made), {}); + const { getResult } = fishermanTools({} as any, store(undefined, made), {}); made.push(madeWrite('POST', '/api/tests', 400)); const result = getResult(); @@ -136,7 +137,7 @@ describe('ledger-derived results', () => { it('treats a text-only turn as finish, using the text as summary when writes succeeded', () => { const made: any[] = []; - const { finishFromText, getResult, isFinished } = createFishermanTools({} as any, store(undefined, made), {}); + const { finishFromText, getResult, isFinished } = fishermanTools({} as any, store(undefined, made), {}); made.push(madeWrite('POST', '/api/suites', 201, { id: 's1' })); finishFromText('Created the suite'); @@ -150,7 +151,7 @@ describe('ledger-derived results', () => { it('keeps the honest failure summary when a text-only turn ends a run with no successful writes', () => { const made: any[] = []; - const { finishFromText, getResult } = createFishermanTools({} as any, store(undefined, made), {}); + const { finishFromText, getResult } = fishermanTools({} as any, store(undefined, made), {}); made.push(madeWrite('POST', '/api/tests', 400)); finishFromText('All done successfully'); @@ -161,6 +162,10 @@ describe('ledger-derived results', () => { }); }); +function fishermanTools(apiClient: any, requestStore: any, opts: any): any { + return createFishermanTools(apiClient, requestStore, new Haul(requestStore), opts); +} + function store(captured?: any, made: any[] = []): any { return { findCapturedRequest: () => captured, diff --git a/tests/unit/request-store.test.ts b/tests/unit/request-store.test.ts index bfc27802..190970f5 100644 --- a/tests/unit/request-store.test.ts +++ b/tests/unit/request-store.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node: import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { RequestResult } from '../../src/api/request-result.js'; -import { RequestStore } from '../../src/api/request-store.js'; +import { Haul, RequestStore } from '../../src/api/request-store.js'; let counter = 0; function makeRequest(method: string, path: string, status: number, id?: string, headers: Record = {}): RequestResult { @@ -317,3 +317,32 @@ describe('extractAuthHeaders session gating', () => { expect(store.extractAuthHeaders()).toEqual({}); }); }); + +describe('Haul', () => { + let outputDir: string; + + beforeEach(() => { + outputDir = mkdtempSync(join(tmpdir(), 'reqstore-')); + }); + + afterEach(() => { + if (existsSync(outputDir)) rmSync(outputDir, { recursive: true, force: true }); + }); + + it('sees only the requests made after it was created', () => { + const store = new RequestStore(outputDir); + store.addMadeRequest(makeRequest('GET', '/api/suites', 200)); + store.addMadeRequest(makeRequest('POST', '/api/suites', 201)); + + const haul = new Haul(store); + store.addMadeRequest(makeRequest('POST', '/api/tests', 422)); + const created = makeRequest('POST', '/api/tests', 201); + created.rawResponseBodyValue = JSON.stringify({ id: 42, title: 'Test A' }); + store.addMadeRequest(created); + + expect(haul.requests()).toHaveLength(2); + expect(haul.failed()).toHaveLength(1); + expect(haul.successfulWrites()).toHaveLength(1); + expect(haul.byId().get('42')?.path).toBe('/api/tests'); + }); +}); From 77ed9c4007c2babee4f64b0a5248b4cfb8703093 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 2 Sep 2026 00:14:48 +0300 Subject: [PATCH 2/2] Rename Haul to RequestHaul and move it under the Fisherman agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class is used by Fisherman alone, so it belongs beside the agent rather than in the shared request data tier: `src/ai/fisherman/request-haul.ts`, matching how other agents keep their utility classes (`ai/researcher/*`). `isFailedRequest` stays in `request-store.ts` — it is still the one shared failure predicate, called by both `RequestHaul.failed()` and Fisherman's `isStuckOnEndpoint`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BYfsBh3TXThD5XLxa23X5d --- src/ai/fisherman-tools.ts | 9 +++++---- src/ai/fisherman.ts | 7 ++++--- src/ai/fisherman/request-haul.ts | 32 ++++++++++++++++++++++++++++++ src/api/request-store.ts | 30 ---------------------------- tests/unit/fisherman-tools.test.ts | 4 ++-- tests/unit/request-store.test.ts | 7 ++++--- 6 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 src/ai/fisherman/request-haul.ts diff --git a/src/ai/fisherman-tools.ts b/src/ai/fisherman-tools.ts index a23fd7c2..912af3a0 100644 --- a/src/ai/fisherman-tools.ts +++ b/src/ai/fisherman-tools.ts @@ -3,12 +3,13 @@ import dedent from 'dedent'; import { z } from 'zod'; import type { ApiClient } from '../api/api-client.ts'; import type { RequestResult } from '../api/request-result.ts'; -import type { Haul, RequestStore } from '../api/request-store.ts'; +import type { RequestStore } from '../api/request-store.ts'; import { extractEndpointDefinition } from '../api/spec-reader.ts'; import { tag } from '../utils/logger.ts'; import { isDynamicSegment } from '../utils/url-matcher.ts'; +import type { RequestHaul } from './fisherman/request-haul.ts'; -export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: Haul, opts: { spec?: any; baseEndpoint?: string }) { +export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: RequestHaul, opts: { spec?: any; baseEndpoint?: string }) { let finished = false; let result: FishermanResult | null = null; @@ -183,7 +184,7 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request return { tools, getResult, isFinished, finishFromText }; } -export function verifyFinish(haul: Haul, input: { summary: string; created: FishermanResult['created']; failed?: FishermanResult['failed'] }): { result: FishermanResult | null; error?: string } { +export function verifyFinish(haul: RequestHaul, input: { summary: string; created: FishermanResult['created']; failed?: FishermanResult['failed'] }): { result: FishermanResult | null; error?: string } { const writes = haul.successfulWrites(); if (writes.length === 0) { tag('warning').log('Fisherman: finish rejected — no successful write request in this run'); @@ -210,7 +211,7 @@ export function verifyFinish(haul: Haul, input: { summary: string; created: Fish return { result: { success: true, summary: input.summary, created: verified, failed: input.failed || [] } }; } -function synthesizeResult(haul: Haul, declaredDone: boolean): FishermanResult { +function synthesizeResult(haul: RequestHaul, declaredDone: boolean): FishermanResult { const made = haul.requests(); const writes = haul.successfulWrites(); const failures = haul.failed(); diff --git a/src/ai/fisherman.ts b/src/ai/fisherman.ts index d640e665..4d6f9d6b 100644 --- a/src/ai/fisherman.ts +++ b/src/ai/fisherman.ts @@ -1,6 +1,6 @@ import dedent from 'dedent'; import type { ApiClient } from '../api/api-client.ts'; -import { Haul, type RequestStore, isFailedRequest } from '../api/request-store.ts'; +import { type RequestStore, isFailedRequest } from '../api/request-store.ts'; import { listAllEndpoints } from '../api/spec-reader.ts'; import { createDebug, tag } from '../utils/logger.ts'; @@ -8,6 +8,7 @@ const debugLog = createDebug('explorbot:fisherman'); import { loop } from '../utils/loop.ts'; import type { Agent } from './agent.ts'; import { type FishermanResult, createFishermanTools } from './fisherman-tools.ts'; +import { RequestHaul } from './fisherman/request-haul.ts'; import type { Provider } from './provider.ts'; import { dataProtectionRules } from './rules.ts'; @@ -79,7 +80,7 @@ export class Fisherman implements Agent { await this.refreshAuth(); debugLog(`auth headers: ${Object.keys(this.apiClient.getHeaders()).join(', ')}`); - const haul = new Haul(this.requestStore); + const haul = new RequestHaul(this.requestStore); const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, haul, { spec: this.spec, baseEndpoint: this.baseEndpoint, @@ -228,7 +229,7 @@ export class Fisherman implements Agent { `; } - private isStuckOnEndpoint(haul: Haul): boolean { + private isStuckOnEndpoint(haul: RequestHaul): boolean { const made = haul.requests(); if (made.length < REPEATED_FAILURE_LIMIT) return false; const recent = made.slice(-REPEATED_FAILURE_LIMIT); diff --git a/src/ai/fisherman/request-haul.ts b/src/ai/fisherman/request-haul.ts new file mode 100644 index 00000000..c3213fc4 --- /dev/null +++ b/src/ai/fisherman/request-haul.ts @@ -0,0 +1,32 @@ +import type { RequestResult } from '../../api/request-result.ts'; +import { type RequestStore, isFailedRequest } from '../../api/request-store.ts'; + +export class RequestHaul { + private start: number; + + constructor(private store: RequestStore) { + this.start = store.getMadeRequests().length; + } + + requests(): RequestResult[] { + return this.store.getMadeRequests().slice(this.start); + } + + failed(): RequestResult[] { + return this.requests().filter(isFailedRequest); + } + + successfulWrites(): RequestResult[] { + return this.requests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400); + } + + byId(): Map { + const map = new Map(); + for (const request of this.successfulWrites()) { + const { id } = request.extractIdAndTitle(); + if (id === undefined) continue; + map.set(String(id), request); + } + return map; + } +} diff --git a/src/api/request-store.ts b/src/api/request-store.ts index 4ffbc479..e5cf8903 100644 --- a/src/api/request-store.ts +++ b/src/api/request-store.ts @@ -191,36 +191,6 @@ export class RequestStore { } } -export class Haul { - private start: number; - - constructor(private store: RequestStore) { - this.start = store.getMadeRequests().length; - } - - requests(): RequestResult[] { - return this.store.getMadeRequests().slice(this.start); - } - - failed(): RequestResult[] { - return this.requests().filter(isFailedRequest); - } - - successfulWrites(): RequestResult[] { - return this.requests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400); - } - - byId(): Map { - const map = new Map(); - for (const request of this.successfulWrites()) { - const { id } = request.extractIdAndTitle(); - if (id === undefined) continue; - map.set(String(id), request); - } - return map; - } -} - export function isFailedRequest(request: RequestResult): boolean { return request.status >= 400 || Boolean(request.error); } diff --git a/tests/unit/fisherman-tools.test.ts b/tests/unit/fisherman-tools.test.ts index e2c94e48..308755a9 100644 --- a/tests/unit/fisherman-tools.test.ts +++ b/tests/unit/fisherman-tools.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test'; import { createFishermanTools } from '../../src/ai/fisherman-tools.ts'; -import { Haul } from '../../src/api/request-store.ts'; +import { RequestHaul } from '../../src/ai/fisherman/request-haul.ts'; describe('Fisherman tools', () => { it('does not present a rejected capture as a request example', async () => { @@ -163,7 +163,7 @@ describe('ledger-derived results', () => { }); function fishermanTools(apiClient: any, requestStore: any, opts: any): any { - return createFishermanTools(apiClient, requestStore, new Haul(requestStore), opts); + return createFishermanTools(apiClient, requestStore, new RequestHaul(requestStore), opts); } function store(captured?: any, made: any[] = []): any { diff --git a/tests/unit/request-store.test.ts b/tests/unit/request-store.test.ts index 190970f5..e92f95ae 100644 --- a/tests/unit/request-store.test.ts +++ b/tests/unit/request-store.test.ts @@ -2,8 +2,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { RequestHaul } from '../../src/ai/fisherman/request-haul.js'; import { RequestResult } from '../../src/api/request-result.js'; -import { Haul, RequestStore } from '../../src/api/request-store.js'; +import { RequestStore } from '../../src/api/request-store.js'; let counter = 0; function makeRequest(method: string, path: string, status: number, id?: string, headers: Record = {}): RequestResult { @@ -318,7 +319,7 @@ describe('extractAuthHeaders session gating', () => { }); }); -describe('Haul', () => { +describe('RequestHaul', () => { let outputDir: string; beforeEach(() => { @@ -334,7 +335,7 @@ describe('Haul', () => { store.addMadeRequest(makeRequest('GET', '/api/suites', 200)); store.addMadeRequest(makeRequest('POST', '/api/suites', 201)); - const haul = new Haul(store); + const haul = new RequestHaul(store); store.addMadeRequest(makeRequest('POST', '/api/tests', 422)); const created = makeRequest('POST', '/api/tests', 201); created.rawResponseBodyValue = JSON.stringify({ id: 42, title: 'Test A' });