2026-02-03 15:33:00 -05:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe , expect } from 'vitest' ;
import { evalTest } from './test-helper.js' ;
2026-02-03 15:54:33 -05:00
import fs from 'node:fs/promises' ;
2026-02-03 15:33:00 -05:00
import path from 'node:path' ;
2026-02-03 15:54:33 -05:00
import yaml from 'js-yaml' ;
2026-02-03 15:33:00 -05:00
// Read the workflow file to extract the prompt
const workflowPath = path . join (
process . cwd ( ) ,
'.github/workflows/gemini-automated-issue-triage.yml' ,
) ;
2026-02-03 15:54:33 -05:00
const workflowContent = await fs . readFile ( workflowPath , 'utf8' ) ;
// Use a YAML parser for robustness
const workflowData = yaml . load ( workflowContent ) as {
jobs ? : {
'triage-issue' ? : {
2026-02-03 16:07:55 -05:00
steps ? : {
id? : string ;
with ? : { prompt? : string ; script? : string } ;
} [ ] ;
2026-02-03 15:54:33 -05:00
} ;
} ;
} ;
2026-02-03 15:33:00 -05:00
2026-02-03 15:54:33 -05:00
const triageStep = workflowData . jobs ? . [ 'triage-issue' ] ? . steps ? . find (
( step ) = > step . id === 'gemini_issue_analysis' ,
2026-02-03 15:33:00 -05:00
) ;
2026-02-03 16:07:55 -05:00
const labelsStep = workflowData . jobs ? . [ 'triage-issue' ] ? . steps ? . find (
( step ) = > step . id === 'get_labels' ,
) ;
2026-02-03 15:54:33 -05:00
const TRIAGE_PROMPT_TEMPLATE = triageStep ? . with ? . prompt ;
2026-02-03 16:07:55 -05:00
const LABELS_SCRIPT = labelsStep ? . with ? . script ;
2026-02-03 15:54:33 -05:00
if ( ! TRIAGE_PROMPT_TEMPLATE ) {
2026-02-03 15:33:00 -05:00
throw new Error (
2026-02-03 15:54:33 -05:00
'Could not extract prompt from workflow file. Check for `jobs.triage-issue.steps[id=gemini_issue_analysis].with.prompt` in the YAML file.' ,
2026-02-03 15:33:00 -05:00
) ;
}
2026-02-03 16:07:55 -05:00
// Extract available labels from the script
let availableLabels = '' ;
if ( LABELS_SCRIPT ) {
const match = LABELS_SCRIPT . match ( /const allowedLabels = \[([\s\S]+?)\];/ ) ;
if ( match && match [ 1 ] ) {
// Clean up the extracted string: remove quotes, commas, and whitespace
availableLabels = match [ 1 ]
. replace ( /['"\n\r]/g , '' )
. split ( ',' )
. map ( ( s ) = > s . trim ( ) )
. filter ( ( s ) = > s . length > 0 )
. join ( ', ' ) ;
}
}
if ( ! availableLabels ) {
throw new Error (
'Could not extract available labels from workflow file. Check for `jobs.triage-issue.steps[id=get_labels].with.script` containing `const allowedLabels = [...]`.' ,
) ;
}
2026-02-03 15:33:00 -05:00
const createPrompt = ( title : string , body : string ) = > {
// The placeholders in the YAML are ${{ env.ISSUE_TITLE }} etc.
// We need to replace them with the actual values for the test.
return TRIAGE_PROMPT_TEMPLATE . replace ( '${{ env.ISSUE_TITLE }}' , title )
. replace ( '${{ env.ISSUE_BODY }}' , body )
2026-02-03 16:07:55 -05:00
. replace ( '${{ env.AVAILABLE_LABELS }}' , availableLabels ) ;
2026-02-03 15:33:00 -05:00
} ;
2026-02-03 19:17:38 -05:00
const TRIAGE_SETTINGS = { } ;
2026-02-03 15:54:33 -05:00
const escapeHtml = ( str : string ) = > {
return str . replace ( /[<>&'"]/g , ( c ) = > {
switch ( c ) {
case '<' :
return '<' ;
case '>' :
return '>' ;
case '&' :
return '&' ;
case "'" :
return ''' ;
case '"' :
return '"' ;
}
return '' ; // Should not happen
} ) ;
} ;
const assertHasLabel = ( expectedLabel : string ) = > {
2026-02-03 19:17:38 -05:00
return async ( rig : any , result : string ) = > {
// Verify JSON output stats
const output = JSON . parse ( result ) ;
expect ( output . stats ) . toBeDefined ( ) ;
// The model response JSON is in the 'response' field
const responseText = output . response ;
2026-02-03 19:55:00 -05:00
let jsonString : string ;
const match = responseText . match ( /```json\s*([\s\S]*?)\s*```/ ) ;
if ( match ? . [ 1 ] ) {
jsonString = match [ 1 ] ;
} else {
const firstBrace = responseText . indexOf ( '{' ) ;
const lastBrace = responseText . lastIndexOf ( '}' ) ;
if ( firstBrace === - 1 || lastBrace === - 1 || lastBrace < firstBrace ) {
throw new Error (
` Could not find a JSON object in the response: " ${ escapeHtml ( responseText ) } " ` ,
) ;
}
jsonString = responseText . substring ( firstBrace , lastBrace + 1 ) ;
2026-02-03 15:54:33 -05:00
}
let data : { labels_to_set? : string [ ] } ;
try {
2026-02-03 16:07:55 -05:00
data = JSON . parse ( jsonString ) ;
2026-02-03 15:54:33 -05:00
} catch ( e ) {
const err = e as Error ;
throw new Error (
2026-02-03 19:17:38 -05:00
` Failed to parse JSON. Error: ${ err . message } . Response: " ${ escapeHtml ( responseText ) } " ` ,
2026-02-03 15:54:33 -05:00
) ;
}
expect ( data ) . toHaveProperty ( 'labels_to_set' ) ;
expect ( Array . isArray ( data . labels_to_set ) ) . toBe ( true ) ;
expect ( data . labels_to_set ) . toContain ( expectedLabel ) ;
} ;
} ;
2026-02-03 15:33:00 -05:00
describe ( 'triage_agent' , ( ) = > {
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/core for windows installation issues' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'CLI failed to install on Windows' ,
'I tried running npm install but it failed with an error on Windows 11.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/core' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/platform for CI/CD failures' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Tests are failing in the CI/CD pipeline' ,
'The github action is failing with a 500 error.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/platform' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/platform for quota issues' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Resource Exhausted 429' ,
'I am getting a 429 error when running the CLI.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/platform' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/core for local build failures' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Local build failing' ,
'I cannot build the project locally. npm run build fails.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/core' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/platform for sandbox issues' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Sandbox connection failed' ,
'I cannot connect to the docker sandbox environment.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/platform' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/core for local test failures' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Local tests failing' ,
'I am running npm test locally and it fails.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/core' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/agent for questions about tools' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Bug with web search?' ,
'I am trying to use web search but I do not know the syntax. Is it @web or /web?' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/agent' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/extensions for feature requests' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Please add a python extension' ,
'I want to write python scripts as an extension.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/extensions' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/unknown for off-topic spam' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt ( 'Buy cheap rolex' , 'Click here for discount.' ) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/unknown' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/core for crash reports phrased as questions' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Why does it segfault?' ,
'Why does the CLI segfault immediately when I run it on Ubuntu?' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/core' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/agent for feature requests for built-in tools' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Can we have a diff tool?' ,
'Is it possible to add a built-in tool to show diffs before editing?' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/agent' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/enterprise for license questions' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'License key issue' ,
'Where do I enter my enterprise license key? I cannot find the setting.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/enterprise' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/unknown for extremely vague reports' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt ( 'It does not work' , 'I tried to use it and it failed.' ) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 15:54:33 -05:00
assert : assertHasLabel ( 'area/unknown' ) ,
2026-02-03 15:33:00 -05:00
} ) ;
2026-02-03 16:12:03 -05:00
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/security for prompt injection reports' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Prompt injection vulnerability' ,
'I found a way to make the agent ignore instructions by saying "Ignore all previous instructions".' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 16:12:03 -05:00
assert : assertHasLabel ( 'area/security' ) ,
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/non-interactive for headless crashes' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Headless mode segfault' ,
'When I run with --headless, the CLI crashes immediately.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 16:12:03 -05:00
assert : assertHasLabel ( 'area/non-interactive' ) ,
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/agent for mixed feedback and tool bugs' ,
2026-02-03 19:17:38 -05:00
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Great tool but web search fails' ,
'I love using Gemini CLI, it is amazing! However, the @web tool gives me an error every time I search for "react".' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
2026-02-03 16:12:03 -05:00
assert : assertHasLabel ( 'area/agent' ) ,
} ) ;
2026-02-03 19:17:38 -05:00
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/core for UI performance issues' ,
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'UI is very slow' ,
'The new interface is lagging and unresponsive when I scroll.' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
assert : assertHasLabel ( 'area/core' ) ,
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/security for accidental secret leakage' ,
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt (
'Leaked API key in logs' ,
'I accidentally posted my API key in a previous issue comment. Can you delete it?' ,
) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
assert : assertHasLabel ( 'area/security' ) ,
} ) ;
evalTest ( 'USUALLY_PASSES' , {
name : 'should identify area/unknown for nonsensical input' ,
prompt : [
'--output-format' ,
'json' ,
'--prompt' ,
createPrompt ( 'asdfasdf' , 'qwerqwer zxcvbnm' ) ,
] ,
params : { settings : TRIAGE_SETTINGS } ,
assert : assertHasLabel ( 'area/unknown' ) ,
} ) ;
2026-02-03 15:33:00 -05:00
} ) ;