Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion apps/cli/ai/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { readCliConfig } from 'cli/lib/cli-config/core';
import { getSiteByFolder } from 'cli/lib/cli-config/sites';
import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager';
import { getProgressCallback, setProgressCallback } from 'cli/logger';
import { resolveStudioToolDefinitions, studioToolDefinitions } from '../tools';
import {
captureCommandOutput,
resolveStudioToolDefinitions,
studioToolDefinitions,
} from '../tools';

vi.mock( 'cli/ai/block-validator', () => ( {
validateBlocks: vi.fn(),
Expand Down Expand Up @@ -276,6 +280,23 @@ describe( 'Studio AI MCP tools', () => {
expect( previousCallback ).toHaveBeenCalledWith( 'Almost done…', undefined );
} );

it( 'coalesces progress updates in captured command output', async () => {
const previousCallback = vi.fn();
setProgressCallback( previousCallback );

const result = await captureCommandOutput( async () => {
const currentCallback = getProgressCallback();
currentCallback?.( 'Applying changes… (74%)' );
currentCallback?.( 'Applying changes… (75%)', true );
currentCallback?.( 'Applying changes… (76%)', true );
currentCallback?.( 'Push complete' );
} );

expect( result.progressOutput ).toBe( 'Applying changes… (76%)\nPush complete' );
expect( previousCallback ).toHaveBeenCalledWith( 'Applying changes… (75%)', true );
expect( previousCallback ).toHaveBeenCalledWith( 'Applying changes… (76%)', true );
} );

it( 'rejects shell syntax in wp_cli post content before dispatching to WP-CLI', async () => {
vi.mocked( isServerRunning ).mockResolvedValue( {
name: 'site-123',
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/ai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,11 @@ export async function captureCommandOutput( fn: () => Promise< void > ): Promise
};
process.exitCode = undefined;
setProgressCallback( ( message, update ) => {
progressMessages.push( message );
if ( update && progressMessages.length > 0 ) {
progressMessages[ progressMessages.length - 1 ] = message;
} else {
progressMessages.push( message );
}
previousCallback?.( message, update );
} );

Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ export async function runCommand( options: {
);
}

setProgressCallback( ( message ) => {
setProgressCallback( ( message, update ) => {
const timestamp = new Date().toISOString();
ui.setLoaderMessage( message );
ui.setLoaderMessage( message, update );
void persist( ( recorder ) => recorder.recordToolProgress( message, timestamp ) );
} );

Expand Down
18 changes: 17 additions & 1 deletion apps/cli/commands/ai/tests/ai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { registerCommand as registerAiSessionsDeleteCommand } from 'cli/commands
import { registerCommand as registerAiSessionsListCommand } from 'cli/commands/ai/sessions/list';
import { registerCommand as registerAiSessionsResumeCommand } from 'cli/commands/ai/sessions/resume';
import { readCliConfig } from 'cli/lib/cli-config/core';
import { setProgressCallback } from 'cli/logger';
import { StudioArgv } from 'cli/types';

const {
Expand All @@ -27,6 +28,7 @@ const {
recordSessionContextMock,
recordSiteSelectedMock,
reportErrorMock,
setLoaderMessageMock,
waitForInputMock,
activeSiteRef,
latestUiRef,
Expand All @@ -40,6 +42,7 @@ const {
recordSessionContextMock: vi.fn(),
recordSiteSelectedMock: vi.fn(),
reportErrorMock: vi.fn(),
setLoaderMessageMock: vi.fn(),
waitForInputMock: vi.fn(),
activeSiteRef: {
current: null as {
Expand Down Expand Up @@ -180,7 +183,9 @@ vi.mock( 'cli/ai/ui', () => ( {
finishReplay() {}
beginAgentTurn() {}
endAgentTurn() {}
setLoaderMessage() {}
setLoaderMessage( ...args: unknown[] ) {
setLoaderMessageMock( ...args );
}
setActiveSite( site: {
name: string;
path: string;
Expand Down Expand Up @@ -320,6 +325,17 @@ describe( 'CLI: studio code sessions command', () => {
expect( ( AiSessionRecorder as typeof AiSessionRecorder ).create ).toHaveBeenCalledTimes( 1 );
} );

it( 'passes progress update signals through to the loader', async () => {
waitForInputMock.mockResolvedValueOnce( 'Hello' ).mockResolvedValueOnce( '/exit' );

await buildParser().parseAsync( [ 'ai' ] );

const progressCallback = vi.mocked( setProgressCallback ).mock.calls.at( -1 )?.[ 0 ];
progressCallback?.( 'Applying changes… (75%)', true );

expect( setLoaderMessageMock ).toHaveBeenCalledWith( 'Applying changes… (75%)', true );
} );

it( 'does not show the server retry prompt when the user interrupts a turn', async () => {
const interruptMock = vi.fn().mockResolvedValue( undefined );
waitForInputMock.mockResolvedValueOnce( 'Build a site' ).mockResolvedValueOnce( '/exit' );
Expand Down
Loading