CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
turn-interruption.test.ts227 linesDownload Raw Back to core
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, expect, it } from 'vitest';8import type { Content } from '@google/genai';9import {10  buildSyntheticToolResponseParts,11  detectTurnInterruption,12  TURN_INTERRUPTION_HISTORY_TAIL_COUNT,13} from './turn-interruption.js';14 15const reminder = (text: string) => ({16  text: `<system-reminder>\n${text}\n</system-reminder>`,17});18 19describe('detectTurnInterruption', () => {20  it('uses a bounded history tail count for continuation detection callers', () => {21    expect(TURN_INTERRUPTION_HISTORY_TAIL_COUNT).toBe(50);22  });23 24  it('returns none for empty history', () => {25    expect(detectTurnInterruption([])).toEqual({ kind: 'none' });26  });27 28  it('returns none when the last turn is a clean model text response', () => {29    const history: Content[] = [30      { role: 'user', parts: [{ text: 'hello' }] },31      { role: 'model', parts: [{ text: 'hi there' }] },32    ];33    expect(detectTurnInterruption(history)).toEqual({ kind: 'none' });34  });35 36  it('returns none for a pure system-reminder user tail', () => {37    const history: Content[] = [38      { role: 'model', parts: [{ text: 'done' }] },39      { role: 'user', parts: [reminder('mcp tool added')] },40    ];41    expect(detectTurnInterruption(history)).toEqual({ kind: 'none' });42  });43 44  it('classifies a trailing user prompt as interrupted_prompt', () => {45    const history: Content[] = [46      { role: 'model', parts: [{ text: 'earlier answer' }] },47      { role: 'user', parts: [{ text: 'do the thing' }] },48    ];49    const result = detectTurnInterruption(history);50    expect(result).toEqual({51      kind: 'interrupted_prompt',52      parts: [{ text: 'do the thing' }],53    });54  });55 56  it('preserves per-turn reminder parts verbatim in the re-submission', () => {57    // The Retry send path does not re-inject per-turn reminders, so the58    // captured entry must keep them — the resumed request has to be59    // complete and belongs to the same logical turn.60    const history: Content[] = [61      {62        role: 'user',63        parts: [reminder('plan mode is on'), { text: 'real prompt' }],64      },65    ];66    const result = detectTurnInterruption(history);67    expect(result).toEqual({68      kind: 'interrupted_prompt',69      parts: [reminder('plan mode is on'), { text: 'real prompt' }],70    });71  });72 73  it('classifies a trailing tool_result submission as interrupted_prompt', () => {74    const frPart = {75      functionResponse: {76        id: 'call-1',77        name: 'read_file',78        response: { output: 'contents' },79      },80    };81    const history: Content[] = [82      {83        role: 'model',84        parts: [{ functionCall: { id: 'call-1', name: 'read_file' } }],85      },86      { role: 'user', parts: [frPart] },87    ];88    const result = detectTurnInterruption(history);89    expect(result).toEqual({ kind: 'interrupted_prompt', parts: [frPart] });90  });91 92  it('captures all consecutive trailing user entries with functionResponses first', () => {93    const history: Content[] = [94      { role: 'model', parts: [{ text: 'waiting on tool result' }] },95      { role: 'user', parts: [{ text: 'IDE context' }] },96      {97        role: 'user',98        parts: [99          {100            functionResponse: {101              id: 'call-1',102              name: 'read_file',103              response: { output: 'contents' },104            },105          },106        ],107      },108    ];109 110    const result = detectTurnInterruption(history);111 112    expect(result).toEqual({113      kind: 'interrupted_prompt',114      parts: [115        {116          functionResponse: {117            id: 'call-1',118            name: 'read_file',119            response: { output: 'contents' },120          },121        },122        { text: 'IDE context' },123      ],124    });125  });126 127  it('returns cloned parts that do not alias the history entry', () => {128    const history: Content[] = [129      { role: 'user', parts: [{ text: 'original' }] },130    ];131    const result = detectTurnInterruption(history);132    if (result.kind !== 'interrupted_prompt') {133      throw new Error(`expected interrupted_prompt, got ${result.kind}`);134    }135    result.parts[0]!.text = 'mutated';136    expect(history[0]!.parts![0]!.text).toBe('original');137  });138 139  it('classifies a dangling functionCall tail as interrupted_turn', () => {140    const history: Content[] = [141      { role: 'user', parts: [{ text: 'run the tool' }] },142      {143        role: 'model',144        parts: [145          { text: 'running…' },146          { functionCall: { id: 'call-1', name: 'shell' } },147          { functionCall: { id: 'call-2', name: 'read_file' } },148        ],149      },150    ];151    expect(detectTurnInterruption(history)).toEqual({152      kind: 'interrupted_turn',153      danglingCalls: [154        { callId: 'call-1', name: 'shell' },155        { callId: 'call-2', name: 'read_file' },156      ],157    });158  });159 160  it('ignores functionCalls without an id (unpairable on the wire)', () => {161    const history: Content[] = [162      {163        role: 'model',164        parts: [{ functionCall: { name: 'shell' } }],165      },166    ];167    expect(detectTurnInterruption(history)).toEqual({ kind: 'none' });168  });169 170  it('falls back to "unknown" for a dangling call without a name', () => {171    const history: Content[] = [172      { role: 'model', parts: [{ functionCall: { id: 'call-9' } }] },173    ];174    expect(detectTurnInterruption(history)).toEqual({175      kind: 'interrupted_turn',176      danglingCalls: [{ callId: 'call-9', name: 'unknown' }],177    });178  });179 180  it('ignores earlier dangling calls when the final entry is clean', () => {181    // The mid-history dangling call is covered by the defensive repair182    // passes in the send path, not by continue detection.183    const history: Content[] = [184      {185        role: 'model',186        parts: [{ functionCall: { id: 'old-call', name: 'shell' } }],187      },188      { role: 'user', parts: [{ text: 'never mind' }] },189      { role: 'model', parts: [{ text: 'ok' }] },190    ];191    expect(detectTurnInterruption(history)).toEqual({ kind: 'none' });192  });193 194  it('returns none for a user tail with no parts', () => {195    const history: Content[] = [{ role: 'user', parts: [] }];196    expect(detectTurnInterruption(history)).toEqual({ kind: 'none' });197  });198});199 200describe('buildSyntheticToolResponseParts', () => {201  it('builds one error functionResponse per dangling call, matching repair shape', () => {202    const parts = buildSyntheticToolResponseParts(203      [204        { callId: 'call-1', name: 'shell' },205        { callId: 'call-2', name: 'read_file' },206      ],207      'interrupted',208    );209    expect(parts).toEqual([210      {211        functionResponse: {212          id: 'call-1',213          name: 'shell',214          response: { error: 'interrupted' },215        },216      },217      {218        functionResponse: {219          id: 'call-2',220          name: 'read_file',221          response: { error: 'interrupted' },222        },223      },224    ]);225  });226});227 
basant307/AI_Governance_Project · CoolFace