diff --git a/command-snapshot.json b/command-snapshot.json index 050e1343..4a81a72e 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -107,8 +107,18 @@ "alias": [], "command": "agent:preview:end", "flagAliases": [], - "flagChars": ["n", "o"], - "flags": ["api-name", "api-version", "authoring-bundle", "flags-dir", "json", "session-id", "target-org"], + "flagChars": ["n", "o", "p"], + "flags": [ + "all", + "api-name", + "api-version", + "authoring-bundle", + "flags-dir", + "json", + "no-prompt", + "session-id", + "target-org" + ], "plugin": "@salesforce/plugin-agent" }, { diff --git a/messages/agent.preview.end.md b/messages/agent.preview.end.md index fc75cfe6..7704b925 100644 --- a/messages/agent.preview.end.md +++ b/messages/agent.preview.end.md @@ -6,7 +6,9 @@ End an existing programmatic agent preview session and get trace location. You must have previously started a programmatic agent preview session with the "agent preview start" command to then use this command to end it. This command also displays the local directory where the session trace files are stored. -The original "agent preview start" command outputs a session ID which you then use with the --session-id flag of this command to end the session. You don't have to specify the --session-id flag if an agent has only one active preview session. You must also use either the --authoring-bundle or --api-name flag to specify the API name of the authoring bundle or the published agent, respecitvely. To find either API name, navigate to your package directory in your DX project. The API name of an authoring bundle is the same as its directory name under the "aiAuthoringBundles" metadata directory. Similarly, the published agent's API name is the same as its directory name under the "Bots" metadata directory. +The original "agent preview start" command outputs a session ID which you then use with the --session-id flag of this command to end the session. You don't have to specify the --session-id flag if an agent has only one active preview session. You must also use either the --authoring-bundle or --api-name flag to specify the API name of the authoring bundle or the published agent, respectively. To find either API name, navigate to your package directory in your DX project. The API name of an authoring bundle is the same as its directory name under the "aiAuthoringBundles" metadata directory. Similarly, the published agent's API name is the same as its directory name under the "Bots" metadata directory. + +Use the --all flag together with either --api-name or --authoring-bundle to end all active preview sessions for a specific agent at once. # flags.session-id.summary @@ -20,6 +22,14 @@ API name of the activated published agent you want to preview. API name of the authoring bundle metadata component that contains the agent's Agent Script file. +# flags.all.summary + +End all active preview sessions for the specified agent. Must be combined with --api-name or --authoring-bundle. + +# flags.no-prompt.summary + +Don't prompt for confirmation before ending sessions. Has an effect only when used with --all. + # error.noSession No agent preview session found. Run "sf agent preview start" to start a new agent preview session. @@ -44,9 +54,21 @@ Failed to end preview session: %s Session traces: %s +# output.noSessionsFound + +No active preview sessions found. + +# output.endedAll + +Ended %s preview session(s). + +# prompt.confirmAll + +About to end %s preview session(s) for agent '%s'. Continue? + # examples -- End a preview session of a published agent by specifying its session ID and API name ; use the default org: +- End a preview session of a published agent by specifying its session ID and API name; use the default org: <%= config.bin %> <%= command.id %> --session-id --api-name My_Published_Agent @@ -57,3 +79,7 @@ Session traces: %s - End a preview session of an agent using its authoring bundle API name; you get an error if the agent has more than one active session. <%= config.bin %> <%= command.id %> --authoring-bundle My_Local_Agent + +- End all active preview sessions for a specific agent without prompting: + + <%= config.bin %> <%= command.id %> --all --authoring-bundle My_Local_Agent --no-prompt diff --git a/schemas/agent-preview-end.json b/schemas/agent-preview-end.json index 9ce8737d..5458bdd6 100644 --- a/schemas/agent-preview-end.json +++ b/schemas/agent-preview-end.json @@ -3,6 +3,26 @@ "$ref": "#/definitions/AgentPreviewEndResult", "definitions": { "AgentPreviewEndResult": { + "anyOf": [ + { + "type": "object", + "properties": { + "ended": { + "type": "array", + "items": { + "$ref": "#/definitions/EndedSession" + } + } + }, + "required": ["ended"], + "additionalProperties": false + }, + { + "$ref": "#/definitions/EndedSession" + } + ] + }, + "EndedSession": { "type": "object", "properties": { "sessionId": { diff --git a/src/commands/agent/preview/end.ts b/src/commands/agent/preview/end.ts index 4e801002..318341a0 100644 --- a/src/commands/agent/preview/end.ts +++ b/src/commands/agent/preview/end.ts @@ -14,19 +14,33 @@ * limitations under the License. */ -import { Flags, SfCommand, toHelpSection } from '@salesforce/sf-plugins-core'; +import { Flags, SfCommand, toHelpSection, prompts } from '@salesforce/sf-plugins-core'; import { Messages, SfError, Lifecycle, EnvironmentVariable } from '@salesforce/core'; import { Agent, ProductionAgent, ScriptAgent } from '@salesforce/agents'; +import type { Connection } from '@salesforce/core'; +import type { Interfaces } from '@oclif/core'; import { getCachedSessionIds, removeCache, validatePreviewSession } from '../../../previewSessionStore.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview.end'); -export type AgentPreviewEndResult = { +async function callPreviewEnd(agent: ScriptAgent | ProductionAgent): Promise { + if (agent instanceof ScriptAgent) { + await agent.preview.end(); + } else if (agent instanceof ProductionAgent) { + await agent.preview.end('UserRequest'); + } +} + +export type EndedSession = { sessionId: string; tracesPath: string; }; +export type AgentPreviewEndResult = { ended: EndedSession[] } | EndedSession; + +type SessionTask = { sessionId: string }; + export default class AgentPreviewEnd extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -46,41 +60,44 @@ export default class AgentPreviewEnd extends SfCommand { }); public static readonly flags = { - 'target-org': Flags.requiredOrg(), + 'target-org': Flags.optionalOrg(), 'api-version': Flags.orgApiVersion(), 'session-id': Flags.string({ summary: messages.getMessage('flags.session-id.summary'), required: false, + exclusive: ['all'], }), 'api-name': Flags.string({ summary: messages.getMessage('flags.api-name.summary'), char: 'n', exactlyOne: ['api-name', 'authoring-bundle'], + dependsOn: ['target-org'], }), 'authoring-bundle': Flags.string({ summary: messages.getMessage('flags.authoring-bundle.summary'), exactlyOne: ['api-name', 'authoring-bundle'], }), + all: Flags.boolean({ + summary: messages.getMessage('flags.all.summary'), + exclusive: ['session-id'], + }), + 'no-prompt': Flags.boolean({ + summary: messages.getMessage('flags.no-prompt.summary'), + char: 'p', + }), }; public async run(): Promise { const { flags } = await this.parse(AgentPreviewEnd); - const conn = flags['target-org'].getConnection(flags['api-version']); - const agentIdentifier = flags['authoring-bundle'] ?? flags['api-name']!; - // Initialize agent with error tracking - let agent; - try { - agent = flags['authoring-bundle'] - ? await Agent.init({ connection: conn, project: this.project!, aabName: flags['authoring-bundle'] }) - : await Agent.init({ connection: conn, project: this.project!, apiNameOrId: flags['api-name']! }); - } catch (error) { - const wrapped = SfError.wrap(error); - await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_agent_not_found' }); - throw new SfError(messages.getMessage('error.agentNotFound', [agentIdentifier]), 'AgentNotFound', [], 2, wrapped); + const conn = flags['target-org']?.getConnection(flags['api-version']); + + if (flags['all']) { + return this.endAll(flags, conn); } - // Get or validate session ID + const agent = await this.initAgent(flags, conn); + let sessionId = flags['session-id']; if (sessionId === undefined) { const cached = await getCachedSessionIds(this.project!, agent); @@ -102,7 +119,6 @@ export default class AgentPreviewEnd extends SfCommand { agent.setSessionId(sessionId); - // Validate session try { await validatePreviewSession(agent); } catch (error) { @@ -120,13 +136,8 @@ export default class AgentPreviewEnd extends SfCommand { const tracesPath = await agent.getHistoryDir(); await removeCache(agent); - // End preview with error tracking try { - if (agent instanceof ScriptAgent) { - await agent.preview.end(); - } else if (agent instanceof ProductionAgent) { - await agent.preview.end('UserRequest'); - } + await callPreviewEnd(agent); } catch (error) { const wrapped = SfError.wrap(error); await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_failed' }); @@ -140,8 +151,100 @@ export default class AgentPreviewEnd extends SfCommand { } await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_success' }); - const result = { sessionId, tracesPath }; + const result: EndedSession = { sessionId, tracesPath }; this.log(messages.getMessage('output.tracesPath', [tracesPath])); return result; } + + private async initAgent( + flags: Pick, + conn: Connection | undefined + ): Promise { + const agentIdentifier = flags['authoring-bundle'] ?? flags['api-name']!; + try { + // conn is always defined when --api-name is used (validated in run()); for --authoring-bundle + // ScriptAgent performs only local operations so it may not need a connection at runtime. + // We pass conn as-is and let the agents library throw if it actually requires a connection. + return flags['authoring-bundle'] + ? await Agent.init({ + connection: conn as Connection, + project: this.project!, + aabName: flags['authoring-bundle'], + }) + : await Agent.init({ connection: conn as Connection, project: this.project!, apiNameOrId: flags['api-name']! }); + } catch (error) { + const wrapped = SfError.wrap(error); + await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_end_agent_not_found' }); + throw new SfError(messages.getMessage('error.agentNotFound', [agentIdentifier]), 'AgentNotFound', [], 2, wrapped); + } + } + + private async endAll( + flags: Pick, + conn: Connection | undefined + ): Promise<{ ended: EndedSession[] }> { + const agent = await this.initAgent(flags, conn); + const agentId = agent.getAgentIdForStorage(); + const sessionIds = await getCachedSessionIds(this.project!, agent); + const sessionsToEnd: SessionTask[] = sessionIds.map((sessionId) => ({ sessionId })); + + if (sessionsToEnd.length === 0) { + this.log(messages.getMessage('output.noSessionsFound')); + return { ended: [] }; + } + + if (!flags['no-prompt']) { + const confirmed = await prompts.confirm({ + message: messages.getMessage('prompt.confirmAll', [sessionsToEnd.length, agentId]), + }); + if (!confirmed) { + return { ended: [] }; + } + } + + // Process sessions serially so that agent.setSessionId() / agent.preview.end() calls on the + // shared agent object do not race with each other. + const ended: EndedSession[] = []; + const failed: Array<{ task: SessionTask; error: string }> = []; + + for (const task of sessionsToEnd) { + const { sessionId } = task; + try { + // ScriptAgent flushes traces to disk; ProductionAgent issues the server-side request. + agent.setSessionId(sessionId); + // eslint-disable-next-line no-await-in-loop + const tracesPath = await agent.getHistoryDir(); + // eslint-disable-next-line no-await-in-loop + await callPreviewEnd(agent); + // eslint-disable-next-line no-await-in-loop + await removeCache(agent); + ended.push({ sessionId, tracesPath }); + } catch (error) { + failed.push({ task, error: SfError.wrap(error).message }); + } + } + + if (failed.length > 0) { + const failedList = failed.map((f) => `${f.task.sessionId}: ${f.error}`).join(', '); + const endedIds = ended.map((e) => e.sessionId).join(', '); + const msg = `Failed to end ${failed.length} session(s): [${failedList}]. Successfully ended ${ + ended.length + } session(s)${ended.length > 0 ? `: [${endedIds}]` : ''}.`; + await Lifecycle.getInstance().emitTelemetry({ + eventName: 'agent_preview_end_all_partial_failure', + failedCount: failed.length, + succeededCount: ended.length, + }); + throw new SfError(msg, 'PreviewEndPartialFailure', [], 4); + } + + await Lifecycle.getInstance().emitTelemetry({ + eventName: 'agent_preview_end_all_success', + sessionCount: ended.length, + }); + this.log(messages.getMessage('output.endedAll', [ended.length])); + return { ended }; + } } + +type CommandFlags = Interfaces.InferredFlags; diff --git a/test/commands/agent/preview/end.test.ts b/test/commands/agent/preview/end.test.ts new file mode 100644 index 00000000..42041c1b --- /dev/null +++ b/test/commands/agent/preview/end.test.ts @@ -0,0 +1,298 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */ + +import { join } from 'node:path'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; +import { TestContext } from '@salesforce/core/testSetup'; +import { SfProject } from '@salesforce/core'; + +const MOCK_PROJECT_DIR = join(process.cwd(), 'test', 'mock-projects', 'agent-generate-template'); +const SESSION_ID = 'test-session-123'; +const AGENT_ID = 'my_agent_id'; +const TRACES_PATH = `/mock/.sfdx/agents/${AGENT_ID}/sessions/${SESSION_ID}`; + +describe('agent preview end', () => { + const $$ = new TestContext(); + let AgentPreviewEnd: any; + let initStub: sinon.SinonStub; + let getCachedSessionIdsStub: sinon.SinonStub; + let removeCacheStub: sinon.SinonStub; + let validatePreviewSessionStub: sinon.SinonStub; + let confirmStub: sinon.SinonStub; + let agentPreviewEndStub: sinon.SinonStub; + + beforeEach(async () => { + agentPreviewEndStub = $$.SANDBOX.stub().resolves(); + getCachedSessionIdsStub = $$.SANDBOX.stub().resolves([SESSION_ID]); + removeCacheStub = $$.SANDBOX.stub().resolves(); + validatePreviewSessionStub = $$.SANDBOX.stub().resolves(); + confirmStub = $$.SANDBOX.stub().resolves(true); + + const MockScriptAgent = class MockScriptAgent { + public preview = { end: agentPreviewEndStub }; + public name = 'TestAgent'; + public setSessionId = sinon.stub(); + public getHistoryDir = sinon.stub().resolves(TRACES_PATH); + public getAgentIdForStorage = sinon.stub().returns(AGENT_ID); + }; + const MockProductionAgent = class MockProductionAgent {}; + + const mockAgentInstance = new MockScriptAgent(); + initStub = $$.SANDBOX.stub().resolves(mockAgentInstance); + + const mod = await esmock('../../../../src/commands/agent/preview/end.js', { + '@salesforce/agents': { + Agent: { init: initStub }, + ScriptAgent: MockScriptAgent, + ProductionAgent: MockProductionAgent, + }, + '../../../../src/previewSessionStore.js': { + getCachedSessionIds: getCachedSessionIdsStub, + removeCache: removeCacheStub, + validatePreviewSession: validatePreviewSessionStub, + }, + '@salesforce/sf-plugins-core': { + Flags: (await import('@salesforce/sf-plugins-core')).Flags, + SfCommand: (await import('@salesforce/sf-plugins-core')).SfCommand, + toHelpSection: (await import('@salesforce/sf-plugins-core')).toHelpSection, + prompts: { + confirm: confirmStub, + }, + }, + }); + + AgentPreviewEnd = mod.default; + + $$.inProject(true); + const mockProject = { + getPath: () => MOCK_PROJECT_DIR, + getDefaultPackage: () => ({ fullPath: join(MOCK_PROJECT_DIR, 'force-app') }), + } as unknown as SfProject; + $$.SANDBOX.stub(SfProject, 'resolve').resolves(mockProject); + $$.SANDBOX.stub(SfProject, 'getInstance').returns(mockProject); + }); + + afterEach(() => { + $$.restore(); + }); + + describe('single-session end (default behaviour)', () => { + it('ends a session for an authoring bundle using the cached session ID', async () => { + const result = await AgentPreviewEnd.run(['--authoring-bundle', 'My_Local_Agent']); + + expect(initStub.calledOnce).to.be.true; + expect(validatePreviewSessionStub.calledOnce).to.be.true; + expect(removeCacheStub.calledOnce).to.be.true; + expect(agentPreviewEndStub.calledOnce).to.be.true; + expect(result).to.deep.include({ sessionId: SESSION_ID, tracesPath: TRACES_PATH }); + }); + + it('ends a session with an explicit --session-id flag, skipping the cache lookup', async () => { + const explicitSessionId = 'explicit-session-456'; + + const result = await AgentPreviewEnd.run([ + '--authoring-bundle', + 'My_Local_Agent', + '--session-id', + explicitSessionId, + ]); + + expect(getCachedSessionIdsStub.called).to.be.false; + expect(result).to.deep.include({ sessionId: explicitSessionId }); + }); + + it('throws when no session is cached for the agent', async () => { + getCachedSessionIdsStub.resolves([]); + + try { + await AgentPreviewEnd.run(['--authoring-bundle', 'My_Local_Agent']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.include('No agent preview session found'); + } + }); + + it('throws when multiple sessions are cached for the agent', async () => { + getCachedSessionIdsStub.resolves(['session-1', 'session-2']); + + try { + await AgentPreviewEnd.run(['--authoring-bundle', 'My_Local_Agent']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.include('Multiple preview sessions found'); + } + }); + + it('throws when --api-name is provided without --target-org', async () => { + try { + await AgentPreviewEnd.run(['--api-name', 'My_Published_Agent']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.include('--target-org'); + } + }); + + it('throws when neither --api-name, --authoring-bundle, nor --all is provided', async () => { + try { + await AgentPreviewEnd.run([]); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.match( + /exactly one of the following must be provided.*--api-name.*--authoring-bundle/is + ); + } + }); + + it('throws when both --api-name and --authoring-bundle are provided at the same time', async () => { + try { + await AgentPreviewEnd.run(['--api-name', 'My_Published_Agent', '--authoring-bundle', 'My_Local_Agent']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.match(/--api-name cannot also be provided when using --authoring-bundle/i); + } + }); + + it('throws when --session-id and --all are both provided', async () => { + try { + await AgentPreviewEnd.run(['--authoring-bundle', 'My_Local_Agent', '--session-id', 'sid', '--all']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.match(/cannot also be provided/i); + } + }); + }); + + describe('--all flag: ends all sessions across all agents', () => { + it('throws when --all is used without --api-name or --authoring-bundle', async () => { + try { + await AgentPreviewEnd.run(['--all', '--no-prompt']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + const err = error as any; + expect(err.message).to.match(/exactly one of the following must be provided.*--api-name.*--authoring-bundle/is); + } + }); + + it('filters to the specified agent when combined with --authoring-bundle', async () => { + getCachedSessionIdsStub.resolves(['session-1', 'session-2']); + + const result = await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent', '--no-prompt']); + + expect(initStub.calledOnce).to.be.true; + expect(removeCacheStub.callCount).to.equal(2); + expect((result as { ended: unknown[] }).ended).to.have.length(2); + }); + + it('filters to the specified agent when combined with --api-name and --target-org (happy path)', async () => { + getCachedSessionIdsStub.resolves(['session-a', 'session-b']); + + const result = await AgentPreviewEnd.run([ + '--all', + '--api-name', + 'My_Published_Agent', + '--target-org', + 'test@org.com', + '--no-prompt', + ]); + + expect(initStub.calledOnce).to.be.true; + expect(removeCacheStub.callCount).to.equal(2); + expect((result as { ended: unknown[] }).ended).to.have.length(2); + }); + + it('throws when --all + --api-name is used without --target-org', async () => { + try { + await AgentPreviewEnd.run(['--all', '--api-name', 'My_Published_Agent']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.include('--target-org'); + } + }); + + it('logs a message and returns an empty list when no sessions are found', async () => { + getCachedSessionIdsStub.resolves([]); + + const result = await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent', '--no-prompt']); + + expect(result).to.deep.equal({ ended: [] }); + expect(removeCacheStub.called).to.be.false; + }); + + it('records partial results and throws a structured error when agent.preview.end() throws mid-loop', async () => { + // Three sessions: session-1 succeeds, session-2 fails, session-3 succeeds + getCachedSessionIdsStub.resolves(['session-1', 'session-2', 'session-3']); + // Fail only on the second call (session-2) + agentPreviewEndStub + .onFirstCall() + .resolves() + .onSecondCall() + .rejects(new Error('network timeout')) + .onThirdCall() + .resolves(); + + try { + await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent', '--no-prompt']); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + const err = error as any; + // Structured error: lists which sessions failed + expect(err.message).to.include('Failed to end 1 session(s)'); + expect(err.message).to.include('session-2'); + expect(err.message).to.include('network timeout'); + // Also mentions the ones that succeeded + expect(err.message).to.include('Successfully ended 2 session(s)'); + // Only 2 successful removes happened (session-1 and session-3) + expect(removeCacheStub.callCount).to.equal(2); + expect(err.name).to.equal('PreviewEndPartialFailure'); + } + }); + }); + + describe('--all flag: confirmation prompt', () => { + it('prompts for confirmation before ending sessions', async () => { + getCachedSessionIdsStub.resolves([SESSION_ID]); + confirmStub.resolves(true); + + await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent']); + + expect(confirmStub.calledOnce).to.be.true; + expect(removeCacheStub.calledOnce).to.be.true; + }); + + it('returns an empty ended list when user declines the confirmation prompt', async () => { + getCachedSessionIdsStub.resolves([SESSION_ID]); + confirmStub.resolves(false); + + const result = await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent']); + + expect(removeCacheStub.called).to.be.false; + expect(result).to.deep.equal({ ended: [] }); + }); + + it('skips the confirmation prompt when --no-prompt is provided', async () => { + getCachedSessionIdsStub.resolves([SESSION_ID]); + + await AgentPreviewEnd.run(['--all', '--authoring-bundle', 'My_Local_Agent', '--no-prompt']); + + expect(confirmStub.called).to.be.false; + expect(removeCacheStub.calledOnce).to.be.true; + }); + }); +}); diff --git a/test/nuts/z3.agent.preview.nut.ts b/test/nuts/z3.agent.preview.nut.ts index cbaf7724..3aba0890 100644 --- a/test/nuts/z3.agent.preview.nut.ts +++ b/test/nuts/z3.agent.preview.nut.ts @@ -80,7 +80,7 @@ describe('agent preview', function () { const endResult = execCmd( `agent preview end --session-id ${sessionId} --authoring-bundle ${bundleApiName} --target-org ${targetOrg} --json` - ).jsonOutput?.result; + ).jsonOutput?.result as import('../../src/commands/agent/preview/end.js').EndedSession | undefined; expect(endResult?.sessionId).to.equal(sessionId); expect(endResult?.tracesPath).to.be.a('string').and.include('.sfdx').and.include('agents'); }); @@ -154,7 +154,7 @@ describe('agent preview', function () { `agent preview end --session-id ${sessionId} --api-name ${ publishedAgent!.DeveloperName } --target-org ${targetOrg} --json` - ).jsonOutput?.result; + ).jsonOutput?.result as import('../../src/commands/agent/preview/end.js').EndedSession | undefined; expect(endResult?.sessionId).to.equal(sessionId); expect(endResult?.tracesPath).to.be.a('string'); });