diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index 70cd93eb..d385ea38 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -1715,6 +1715,8 @@ const MapProject = () => { // event would arrive where the options object is expected. const onManualSave = () => onSave({source: 'manual'}) + // Work on a row other than the one open in the side panel (a run's, a grid + // button's) must pass its index (OpenConceptLab/ocl_issues#2887). const log = (data, index) => { let idx = index === undefined ? rowIndex : index const nextLogs = {...logsRef.current, [idx]: [{created_at: moment().toDate(), user: user.username || user.id, ...data}, ...(logsRef.current[idx] || [])]} @@ -5721,7 +5723,9 @@ const MapProject = () => { // Returns the row's AI outcome as a stage (1 analysed, -2 failed, -3 not // run), or null when the row was skipped or the run stopped. A run's call // that ends after a newer run started leaves that run's stages and failure - // count alone (ocl_issues#2872). + // count alone (ocl_issues#2872), but still logs on its own row, like every + // AI entry here: the row's history records each call made for it + // (OpenConceptLab/ocl_issues#2887). const runTicket = autoMatchRunTicketRef.current const isSuperseded = () => isBulk && runTicket !== autoMatchRunTicketRef.current const markRecommend = stage => { @@ -5842,14 +5846,14 @@ const MapProject = () => { let timestamp = moment().toDate() if(handlePreviewLimitError(response, isSuperseded() ? null : __index, 'recommend', {isRun: isBulk, stopPhase: 'ai'})) { markRecommend(-3) - log({created_at: timestamp, action: 'AIRecommendationLimitReached', extras: {model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}) + log({created_at: timestamp, action: 'AIRecommendationLimitReached', extras: {model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}, __index) return -3 } if(response?.detail) { markRecommend(-2) if(!isSuperseded()) aiFailuresInARowRef.current += 1 - log({created_at: timestamp, action: 'AIRecommendation', description: response.detail, extras: {error: response.detail, model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}) + log({created_at: timestamp, action: 'AIRecommendation', description: response.detail, extras: {error: response.detail, model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}, __index) setAlert({message: response.detail, severity: 'error'}) return -2 } @@ -5882,7 +5886,7 @@ const MapProject = () => { } catch (err) { if(handlePreviewLimitError(err, isSuperseded() ? null : __index, 'recommend', {isRun: isBulk, stopPhase: 'ai'})) { markRecommend(-3) - log({created_at: moment().toDate(), action: 'AIRecommendationLimitReached', extras: {model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}) + log({created_at: moment().toDate(), action: 'AIRecommendationLimitReached', extras: {model: requestedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}, __index) return -3 } markRecommend(-2) diff --git a/src/components/map-projects/__tests__/aiRecommendationLogs.test.js b/src/components/map-projects/__tests__/aiRecommendationLogs.test.js new file mode 100644 index 00000000..720059e3 --- /dev/null +++ b/src/components/map-projects/__tests__/aiRecommendationLogs.test.js @@ -0,0 +1,32 @@ +/** + * Every entry fetchRecommendation logs goes on the row it analysed, not the + * row open in the side panel (OpenConceptLab/ocl_issues#2887). + * + * fetchRecommendation is a closure inside MapProject, which these tests can't + * mount (they run without a JSX transform), so this reads its source: each + * log() call in it must pass __index, the row it was called for. + */ + +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import babel from '@babel/core' + +test('fetchRecommendation: every AI entry it logs goes on the row being analysed, superseded or not', () => { + const source = readFileSync(new URL('../MapProject.jsx', import.meta.url), 'utf8') + const calls = [] + babel.traverse(babel.parseSync(source, {babelrc: false, configFile: false, parserOpts: {plugins: ['jsx']}}), { + VariableDeclarator(path) { + if(path.node.id.name === 'fetchRecommendation') + path.traverse({ + CallExpression({node}) { + if(node.callee.name === 'log') + calls.push({line: node.loc.start.line, row: node.arguments[1]?.name}) + } + }) + } + }) + + assert.ok(calls.length > 0, 'no log() calls found in fetchRecommendation') + assert.deepEqual(calls.filter(call => call.row !== '__index'), []) +})