Files
KGStudio/src/agent/tools/ReadMusicTool.test.ts
T
Xiaohan-Tian 8ee7ddac77 feat: add track-aware tool targeting and new agent tools, lifted the requirement of selection a MIDI region to use AI Agent
Introduces a shared `toolTargeting.ts` module that resolves the active MIDI
region/track from the user's current selection, enabling tools to operate on
the correct target without requiring explicit track IDs in most cases.

Adds two new read-only tools — `list_all_tracks` and
`get_user_selected_music_range_and_track` — so the agent can inspect
available tracks and the current selection context before acting.

Also refactors `AddNotesTool` to auto-create MIDI regions when no region
exists, refactors `RemoveNotesTool`, `ReadMusicTool`, and
`ReadChordProgressionTool` to use the new targeting helpers, adds a
`buildToolHistoryContent` hook to `BaseTool` for cleaner chat history
display, and updates system prompts and tests throughout.
2026-06-04 19:29:37 -07:00

107 lines
4.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ReadMusicTool } from './ReadMusicTool';
import { KGCore } from '../../core/KGCore';
import { KGProject } from '../../core/KGProject';
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { KGMidiNote } from '../../core/midi/KGMidiNote';
function buildTrack(name: string, id: number, regionStartBeat: number, regionLength: number): KGMidiTrack {
const track = new KGMidiTrack(name, id);
const region = new KGMidiRegion(`region-${id}`, track.getId().toString(), track.getTrackIndex(), `${name} Region`, regionStartBeat, regionLength);
region.addNote(new KGMidiNote(`note-${id}`, 0, 4, 60));
track.setRegions([region]);
return track;
}
describe('ReadMusicTool', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('builds a compact summary for reading multiple tracks including empty ones', () => {
const project = new KGProject('read-music-project', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const leadTrack = buildTrack('Lead', 1, 0, 16);
const emptyTrack = new KGMidiTrack('Pads', 2);
project.setTracks([leadTrack, emptyTrack]);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
} as unknown as KGCore);
const tool = new ReadMusicTool();
const summary = tool.buildToolResultDisplayContent(
{
track_id: 'all',
start: 0,
length: 16,
},
{ success: true, result: 'raw result' },
);
expect(summary).toBe('Read tracks Lead and Pads from bars 1 to 4.');
});
it('includes empty MIDI tracks as rest-only ABC sections in all-track reads', async () => {
const project = new KGProject('read-music-project', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const leadTrack = buildTrack('Lead', 1, 0, 8);
const emptyTrack = new KGMidiTrack('Pads', 2);
project.setTracks([leadTrack, emptyTrack]);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
} as unknown as KGCore);
const tool = new ReadMusicTool();
const result = await tool.execute({ track_id: 'all', start: 0, length: 8 });
expect(result.success).toBe(true);
expect(result.result).toContain('track_id: 1');
expect(result.result).toContain('track_name: Lead');
expect(result.result).toContain('Instrument: Acoustic Grand Piano');
expect(result.result).toContain('track_id: 2');
expect(result.result).toContain('track_name: Pads');
expect(result.result).toContain('Instrument: Acoustic Grand Piano');
expect(result.result).not.toContain('Track 1 - Melody:');
expect(result.result).not.toContain('Track 2 - Pads:');
expect(result.result).not.toContain('\nT:');
expect(result.result).toContain('z4 | z4 | // No regions found');
});
it('returns a professional empty-project message when all MIDI tracks are empty', async () => {
const project = new KGProject('read-music-project', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const emptyTrack = new KGMidiTrack('Lead', 1);
emptyTrack.setRegions([
new KGMidiRegion('region-1', emptyTrack.getId().toString(), emptyTrack.getTrackIndex(), 'Lead Region', 0, 36),
]);
project.setTracks([emptyTrack]);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
} as unknown as KGCore);
const tool = new ReadMusicTool();
const result = await tool.execute({ track_id: 'all', start: 0, length: 36 });
expect(result.success).toBe(true);
expect(result.result).toBe('No musical content is present in the project yet.');
});
it('returns a professional empty-range message when the selected range has no MIDI notes', async () => {
const project = new KGProject('read-music-project', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const leadTrack = buildTrack('Lead', 1, 0, 8);
const emptyTrack = new KGMidiTrack('Pads', 2);
project.setTracks([leadTrack, emptyTrack]);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
} as unknown as KGCore);
const tool = new ReadMusicTool();
const result = await tool.execute({ track_id: 'all', start: 16, length: 8 });
expect(result.success).toBe(true);
expect(result.result).toBe('No musical content was found in the selected range.');
});
});