mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-18 15:55:59 +01:00
694 lines
24 KiB
TypeScript
694 lines
24 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import assert from 'assert';
|
|
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
|
|
import { URI } from '../../../../../base/common/uri.js';
|
|
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
|
import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugLogProvider, IChatDebugModelTurnEvent, IChatDebugResolvedEventContent, IChatDebugToolCallEvent } from '../../common/chatDebugService.js';
|
|
import { ChatDebugServiceImpl } from '../../common/chatDebugServiceImpl.js';
|
|
import { LocalChatSessionUri } from '../../common/model/chatUri.js';
|
|
|
|
suite('ChatDebugServiceImpl', () => {
|
|
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
|
|
|
let service: ChatDebugServiceImpl;
|
|
|
|
const session1 = URI.parse('vscode-chat-session://local/session-1');
|
|
const session2 = URI.parse('vscode-chat-session://local/session-2');
|
|
const sessionA = LocalChatSessionUri.forSession('a');
|
|
const sessionB = LocalChatSessionUri.forSession('b');
|
|
const sessionGeneric = URI.parse('vscode-chat-session://local/session');
|
|
const nonLocalSession = URI.parse('some-other-scheme://authority/session-1');
|
|
const copilotCliSession = URI.parse('copilotcli:/test-session-id');
|
|
const claudeCodeSession = URI.parse('claude-code:/test-session-id');
|
|
|
|
setup(() => {
|
|
service = disposables.add(new ChatDebugServiceImpl());
|
|
});
|
|
|
|
suite('addEvent and getEvents', () => {
|
|
test('should add and retrieve events', () => {
|
|
const event: IChatDebugGenericEvent = {
|
|
kind: 'generic',
|
|
sessionResource: session1,
|
|
created: new Date(),
|
|
name: 'test-event',
|
|
level: ChatDebugLogLevel.Info,
|
|
};
|
|
|
|
service.addEvent(event);
|
|
|
|
assert.deepStrictEqual(service.getEvents(), [event]);
|
|
});
|
|
|
|
test('should filter events by sessionResource', () => {
|
|
const event1: IChatDebugGenericEvent = {
|
|
kind: 'generic',
|
|
sessionResource: session1,
|
|
created: new Date(),
|
|
name: 'event-1',
|
|
level: ChatDebugLogLevel.Info,
|
|
};
|
|
const event2: IChatDebugGenericEvent = {
|
|
kind: 'generic',
|
|
sessionResource: session2,
|
|
created: new Date(),
|
|
name: 'event-2',
|
|
level: ChatDebugLogLevel.Warning,
|
|
};
|
|
|
|
service.addEvent(event1);
|
|
service.addEvent(event2);
|
|
|
|
assert.deepStrictEqual(service.getEvents(session1), [event1]);
|
|
assert.deepStrictEqual(service.getEvents(session2), [event2]);
|
|
assert.strictEqual(service.getEvents().length, 2);
|
|
});
|
|
|
|
test('should fire onDidAddEvent when event is added', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
const event: IChatDebugGenericEvent = {
|
|
kind: 'generic',
|
|
sessionResource: session1,
|
|
created: new Date(),
|
|
name: 'test',
|
|
level: ChatDebugLogLevel.Info,
|
|
};
|
|
service.addEvent(event);
|
|
|
|
assert.deepStrictEqual(firedEvents, [event]);
|
|
});
|
|
|
|
test('should handle different event kinds', () => {
|
|
const toolCall: IChatDebugToolCallEvent = {
|
|
kind: 'toolCall',
|
|
sessionResource: session1,
|
|
created: new Date(),
|
|
toolName: 'readFile',
|
|
toolCallId: 'call-1',
|
|
input: '{"path": "/foo.ts"}',
|
|
output: 'file contents',
|
|
result: 'success',
|
|
durationInMillis: 42,
|
|
};
|
|
const modelTurn: IChatDebugModelTurnEvent = {
|
|
kind: 'modelTurn',
|
|
sessionResource: session1,
|
|
created: new Date(),
|
|
model: 'gpt-4',
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
totalTokens: 150,
|
|
durationInMillis: 1200,
|
|
};
|
|
|
|
service.addEvent(toolCall);
|
|
service.addEvent(modelTurn);
|
|
|
|
const events = service.getEvents(session1);
|
|
assert.strictEqual(events.length, 2);
|
|
assert.strictEqual(events[0].kind, 'toolCall');
|
|
assert.strictEqual(events[1].kind, 'modelTurn');
|
|
});
|
|
});
|
|
|
|
suite('log', () => {
|
|
test('should create a generic event with defaults', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
service.log(session1, 'Some name', 'Some details');
|
|
|
|
assert.strictEqual(firedEvents.length, 1);
|
|
const event = firedEvents[0];
|
|
assert.strictEqual(event.kind, 'generic');
|
|
assert.strictEqual(event.sessionResource.toString(), session1.toString());
|
|
assert.strictEqual((event as IChatDebugGenericEvent).name, 'Some name');
|
|
assert.strictEqual((event as IChatDebugGenericEvent).details, 'Some details');
|
|
assert.strictEqual((event as IChatDebugGenericEvent).level, ChatDebugLogLevel.Info);
|
|
});
|
|
|
|
test('should accept custom level and options', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
service.log(session1, 'warning-event', 'oh no', ChatDebugLogLevel.Warning, {
|
|
id: 'my-id',
|
|
category: 'testing',
|
|
parentEventId: 'parent-1',
|
|
});
|
|
|
|
const event = firedEvents[0] as IChatDebugGenericEvent;
|
|
assert.strictEqual(event.level, ChatDebugLogLevel.Warning);
|
|
assert.strictEqual(event.id, 'my-id');
|
|
assert.strictEqual(event.category, 'testing');
|
|
assert.strictEqual(event.parentEventId, 'parent-1');
|
|
});
|
|
|
|
test('should not log events for ineligible session schemes', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
service.log(nonLocalSession, 'should-be-skipped', 'details');
|
|
|
|
assert.strictEqual(firedEvents.length, 0);
|
|
assert.strictEqual(service.getEvents(nonLocalSession).length, 0);
|
|
});
|
|
|
|
test('should log events for copilotcli sessions', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
service.log(copilotCliSession, 'cli-event', 'details');
|
|
|
|
assert.strictEqual(firedEvents.length, 1);
|
|
assert.strictEqual(service.getEvents(copilotCliSession).length, 1);
|
|
});
|
|
|
|
test('should log events for claude-code sessions', () => {
|
|
const firedEvents: IChatDebugEvent[] = [];
|
|
disposables.add(service.onDidAddEvent(e => firedEvents.push(e)));
|
|
|
|
service.log(claudeCodeSession, 'claude-event', 'details');
|
|
|
|
assert.strictEqual(firedEvents.length, 1);
|
|
assert.strictEqual(service.getEvents(claudeCodeSession).length, 1);
|
|
});
|
|
});
|
|
|
|
suite('getSessionResources', () => {
|
|
test('should return unique session resources', () => {
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionA, created: new Date(), name: 'e1', level: ChatDebugLogLevel.Info });
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionB, created: new Date(), name: 'e2', level: ChatDebugLogLevel.Info });
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionA, created: new Date(), name: 'e3', level: ChatDebugLogLevel.Info });
|
|
|
|
const resources = service.getSessionResources();
|
|
assert.strictEqual(resources.length, 2);
|
|
});
|
|
|
|
test('should return empty array when no events', () => {
|
|
assert.deepStrictEqual(service.getSessionResources(), []);
|
|
});
|
|
});
|
|
|
|
suite('clear', () => {
|
|
test('should clear all events', () => {
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionA, created: new Date(), name: 'e', level: ChatDebugLogLevel.Info });
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionB, created: new Date(), name: 'e', level: ChatDebugLogLevel.Info });
|
|
|
|
service.clear();
|
|
|
|
assert.strictEqual(service.getEvents().length, 0);
|
|
});
|
|
});
|
|
|
|
suite('MAX_EVENTS_PER_SESSION cap', () => {
|
|
test('should evict oldest events when exceeding per-session cap', () => {
|
|
// The max per session is 10_000. Add more than that to a single session.
|
|
for (let i = 0; i < 10_001; i++) {
|
|
service.addEvent({ kind: 'generic', sessionResource: sessionGeneric, created: new Date(), name: `event-${i}`, level: ChatDebugLogLevel.Info });
|
|
}
|
|
|
|
const events = service.getEvents();
|
|
assert.ok(events.length <= 10_000, 'Should not exceed MAX_EVENTS_PER_SESSION');
|
|
// The first event should have been evicted
|
|
assert.ok(!(events as IChatDebugGenericEvent[]).find(e => e.name === 'event-0'), 'Event-0 should have been evicted');
|
|
// The last event should be present
|
|
assert.ok((events as IChatDebugGenericEvent[]).find(e => e.name === 'event-10000'), 'Last event should be present');
|
|
});
|
|
|
|
test('should evict oldest session when exceeding MAX_SESSIONS', () => {
|
|
// MAX_SESSIONS is 5 — add events to 6 different sessions
|
|
const sessions: URI[] = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
const uri = URI.parse(`vscode-chat-session://local/session-lru-${i}`);
|
|
sessions.push(uri);
|
|
service.addEvent({ kind: 'generic', sessionResource: uri, created: new Date(), name: `event-${i}`, level: ChatDebugLogLevel.Info });
|
|
}
|
|
|
|
const resources = service.getSessionResources();
|
|
assert.strictEqual(resources.length, 5, 'Should not exceed MAX_SESSIONS');
|
|
// The first session should have been evicted
|
|
assert.ok(!resources.some(r => r.toString() === sessions[0].toString()), 'Session-0 should have been evicted');
|
|
assert.strictEqual(service.getEvents(sessions[0]).length, 0, 'Events from evicted session should be gone');
|
|
// The last session should be present
|
|
assert.ok(resources.some(r => r.toString() === sessions[5].toString()), 'Session-5 should be present');
|
|
});
|
|
|
|
test('should use LRU eviction — recently-used sessions are kept', () => {
|
|
// Fill to MAX_SESSIONS (5)
|
|
const sessions: URI[] = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const uri = URI.parse(`vscode-chat-session://local/session-lru2-${i}`);
|
|
sessions.push(uri);
|
|
service.addEvent({ kind: 'generic', sessionResource: uri, created: new Date(), name: `init-${i}`, level: ChatDebugLogLevel.Info });
|
|
}
|
|
|
|
// Touch session-0 so it moves to the back of the LRU order
|
|
service.addEvent({ kind: 'generic', sessionResource: sessions[0], created: new Date(), name: 'touch', level: ChatDebugLogLevel.Info });
|
|
|
|
// Add a 6th session — session-1 (the true LRU) should be evicted, not session-0
|
|
const session6 = URI.parse('vscode-chat-session://local/session-lru2-5');
|
|
service.addEvent({ kind: 'generic', sessionResource: session6, created: new Date(), name: 'new', level: ChatDebugLogLevel.Info });
|
|
|
|
const resources = service.getSessionResources();
|
|
assert.strictEqual(resources.length, 5);
|
|
assert.ok(resources.some(r => r.toString() === sessions[0].toString()), 'Session-0 should be kept (recently used)');
|
|
assert.ok(!resources.some(r => r.toString() === sessions[1].toString()), 'Session-1 should be evicted (LRU)');
|
|
assert.ok(resources.some(r => r.toString() === session6.toString()), 'Session-5 should be present');
|
|
});
|
|
});
|
|
|
|
suite('activeSessionResource', () => {
|
|
test('should default to undefined', () => {
|
|
assert.strictEqual(service.activeSessionResource, undefined);
|
|
});
|
|
|
|
test('should be settable', () => {
|
|
service.activeSessionResource = session1;
|
|
|
|
assert.strictEqual(service.activeSessionResource, session1);
|
|
});
|
|
});
|
|
|
|
suite('markDebugDataAttached', () => {
|
|
test('should track attached debug data per session', () => {
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), false);
|
|
|
|
const fired: URI[] = [];
|
|
disposables.add(service.onDidAttachDebugData(uri => fired.push(uri)));
|
|
|
|
service.markDebugDataAttached(sessionGeneric);
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), true);
|
|
assert.strictEqual(fired.length, 1);
|
|
assert.strictEqual(fired[0].toString(), sessionGeneric.toString());
|
|
|
|
// Idempotent — second call should not fire again
|
|
service.markDebugDataAttached(sessionGeneric);
|
|
assert.strictEqual(fired.length, 1);
|
|
|
|
// Other sessions remain unaffected
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionA), false);
|
|
});
|
|
|
|
test('should clear attached debug data on endSession', () => {
|
|
service.markDebugDataAttached(sessionGeneric);
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), true);
|
|
|
|
service.endSession(sessionGeneric);
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionGeneric), false);
|
|
});
|
|
|
|
test('should clear attached debug data on clear', () => {
|
|
service.markDebugDataAttached(sessionA);
|
|
service.markDebugDataAttached(sessionB);
|
|
|
|
service.clear();
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionA), false);
|
|
assert.strictEqual(service.hasAttachedDebugData(sessionB), false);
|
|
});
|
|
});
|
|
|
|
suite('registerProvider', () => {
|
|
test('should register and unregister a provider', async () => {
|
|
const extSession = URI.parse('vscode-chat-session://local/ext-session');
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => [{
|
|
kind: 'generic',
|
|
sessionResource: extSession,
|
|
created: new Date(),
|
|
name: 'from-provider',
|
|
level: ChatDebugLogLevel.Info,
|
|
}],
|
|
};
|
|
|
|
const reg = service.registerProvider(provider);
|
|
await service.invokeProviders(extSession);
|
|
|
|
const events = service.getEvents(extSession);
|
|
assert.ok(events.some(e => e.kind === 'generic' && (e as IChatDebugGenericEvent).name === 'from-provider'));
|
|
|
|
reg.dispose();
|
|
});
|
|
|
|
test('provider returning undefined should not add events', async () => {
|
|
const emptySession = URI.parse('vscode-chat-session://local/empty-session');
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => undefined,
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(emptySession);
|
|
|
|
assert.strictEqual(service.getEvents(emptySession).length, 0);
|
|
});
|
|
|
|
test('provider errors should be handled gracefully', async () => {
|
|
const errorSession = URI.parse('vscode-chat-session://local/error-session');
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => { throw new Error('boom'); },
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
// Should not throw
|
|
await service.invokeProviders(errorSession);
|
|
assert.strictEqual(service.getEvents(errorSession).length, 0);
|
|
});
|
|
});
|
|
|
|
suite('invokeProviders', () => {
|
|
test('should invoke multiple providers and merge events', async () => {
|
|
const providerA: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => [{
|
|
kind: 'generic',
|
|
sessionResource: sessionGeneric,
|
|
created: new Date(),
|
|
name: 'from-A',
|
|
level: ChatDebugLogLevel.Info,
|
|
}],
|
|
};
|
|
const providerB: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => [{
|
|
kind: 'generic',
|
|
sessionResource: sessionGeneric,
|
|
created: new Date(),
|
|
name: 'from-B',
|
|
level: ChatDebugLogLevel.Info,
|
|
}],
|
|
};
|
|
|
|
disposables.add(service.registerProvider(providerA));
|
|
disposables.add(service.registerProvider(providerB));
|
|
await service.invokeProviders(sessionGeneric);
|
|
|
|
const names = (service.getEvents(sessionGeneric) as IChatDebugGenericEvent[]).map(e => e.name);
|
|
assert.ok(names.includes('from-A'));
|
|
assert.ok(names.includes('from-B'));
|
|
});
|
|
|
|
test('should cancel previous invocation for same session', async () => {
|
|
let cancelledToken: CancellationToken | undefined;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async (_sessionResource, token) => {
|
|
cancelledToken = token;
|
|
return [];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
|
|
// First invocation
|
|
await service.invokeProviders(sessionGeneric);
|
|
const firstToken = cancelledToken!;
|
|
|
|
// Second invocation for same session should cancel the first
|
|
await service.invokeProviders(sessionGeneric);
|
|
assert.strictEqual(firstToken.isCancellationRequested, true);
|
|
});
|
|
|
|
test('should fire onDidClearProviderEvents when clearing provider events', async () => {
|
|
const clearedSessions: URI[] = [];
|
|
disposables.add(service.onDidClearProviderEvents(sessionResource => clearedSessions.push(sessionResource)));
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async (sessionResource) => [{
|
|
kind: 'generic',
|
|
sessionResource,
|
|
created: new Date(),
|
|
name: 'provider-event',
|
|
level: ChatDebugLogLevel.Info,
|
|
}],
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
|
|
// First invocation clears empty set and fires clear event
|
|
await service.invokeProviders(sessionGeneric);
|
|
assert.strictEqual(clearedSessions.length, 1, 'Clear event should fire on first invocation');
|
|
|
|
// Second invocation clears provider events from first invocation
|
|
await service.invokeProviders(sessionGeneric);
|
|
assert.strictEqual(clearedSessions.length, 2, 'Clear event should fire on second invocation');
|
|
assert.strictEqual(clearedSessions[1].toString(), sessionGeneric.toString());
|
|
});
|
|
|
|
test('should not cancel invocations for different sessions', async () => {
|
|
const tokens: Map<string, CancellationToken> = new Map();
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async (sessionResource, token) => {
|
|
tokens.set(sessionResource.toString(), token);
|
|
return [];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
|
|
await service.invokeProviders(sessionA);
|
|
await service.invokeProviders(sessionB);
|
|
|
|
const tokenA = tokens.get(sessionA.toString())!;
|
|
assert.strictEqual(tokenA.isCancellationRequested, false, 'session-a token should not be cancelled');
|
|
});
|
|
|
|
test('should not invoke providers for ineligible session schemes', async () => {
|
|
let providerCalled = false;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => {
|
|
providerCalled = true;
|
|
return [{
|
|
kind: 'generic',
|
|
sessionResource: nonLocalSession,
|
|
created: new Date(),
|
|
name: 'should-not-appear',
|
|
level: ChatDebugLogLevel.Info,
|
|
}];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(nonLocalSession);
|
|
|
|
assert.strictEqual(providerCalled, false);
|
|
assert.strictEqual(service.getEvents(nonLocalSession).length, 0);
|
|
});
|
|
|
|
test('should invoke providers for copilotcli sessions', async () => {
|
|
let providerCalled = false;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => {
|
|
providerCalled = true;
|
|
return [{
|
|
kind: 'generic',
|
|
sessionResource: copilotCliSession,
|
|
created: new Date(),
|
|
name: 'cli-provider-event',
|
|
level: ChatDebugLogLevel.Info,
|
|
}];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(copilotCliSession);
|
|
|
|
assert.strictEqual(providerCalled, true);
|
|
assert.ok(service.getEvents(copilotCliSession).length > 0);
|
|
});
|
|
|
|
test('should invoke providers for claude-code sessions', async () => {
|
|
let providerCalled = false;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => {
|
|
providerCalled = true;
|
|
return [{
|
|
kind: 'generic',
|
|
sessionResource: claudeCodeSession,
|
|
created: new Date(),
|
|
name: 'claude-provider-event',
|
|
level: ChatDebugLogLevel.Info,
|
|
}];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(claudeCodeSession);
|
|
|
|
assert.strictEqual(providerCalled, true);
|
|
assert.ok(service.getEvents(claudeCodeSession).length > 0);
|
|
});
|
|
|
|
test('newly registered provider should be invoked for active sessions', async () => {
|
|
// Start an invocation before the provider is registered
|
|
const firstProvider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => [],
|
|
};
|
|
disposables.add(service.registerProvider(firstProvider));
|
|
await service.invokeProviders(sessionGeneric);
|
|
|
|
// Now register a new provider — it should be invoked for the active session
|
|
const lateEvents: IChatDebugEvent[] = [];
|
|
const lateProvider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => {
|
|
const event: IChatDebugGenericEvent = {
|
|
kind: 'generic',
|
|
sessionResource: sessionGeneric,
|
|
created: new Date(),
|
|
name: 'late-provider-event',
|
|
level: ChatDebugLogLevel.Info,
|
|
};
|
|
lateEvents.push(event);
|
|
return [event];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(lateProvider));
|
|
|
|
// Give it a tick to let the async invocation complete
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
assert.ok(lateEvents.length > 0, 'Late provider should have been invoked');
|
|
});
|
|
});
|
|
|
|
suite('resolveEvent', () => {
|
|
test('should delegate to provider with resolveChatDebugLogEvent', async () => {
|
|
const resolved: IChatDebugResolvedEventContent = {
|
|
kind: 'text',
|
|
value: 'resolved detail text',
|
|
};
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => undefined,
|
|
resolveChatDebugLogEvent: async (eventId) => {
|
|
if (eventId === 'my-event') {
|
|
return resolved;
|
|
}
|
|
return undefined;
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
|
|
const result = await service.resolveEvent('my-event');
|
|
assert.deepStrictEqual(result, resolved);
|
|
});
|
|
|
|
test('should return undefined if no provider resolves the event', async () => {
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => undefined,
|
|
resolveChatDebugLogEvent: async () => undefined,
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
|
|
const result = await service.resolveEvent('nonexistent');
|
|
assert.strictEqual(result, undefined);
|
|
});
|
|
|
|
test('should return undefined when no providers registered', async () => {
|
|
const result = await service.resolveEvent('any-id');
|
|
assert.strictEqual(result, undefined);
|
|
});
|
|
|
|
test('should return first non-undefined resolution from multiple providers', async () => {
|
|
const provider1: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => undefined,
|
|
resolveChatDebugLogEvent: async () => undefined,
|
|
};
|
|
const provider2: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => undefined,
|
|
resolveChatDebugLogEvent: async () => ({ kind: 'text', value: 'from provider 2' }),
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider1));
|
|
disposables.add(service.registerProvider(provider2));
|
|
|
|
const result = await service.resolveEvent('any');
|
|
assert.deepStrictEqual(result, { kind: 'text', value: 'from provider 2' });
|
|
});
|
|
});
|
|
|
|
suite('endSession', () => {
|
|
test('should cancel and remove the CTS for a session', async () => {
|
|
let capturedToken: CancellationToken | undefined;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async (_sessionResource, token) => {
|
|
capturedToken = token;
|
|
return [];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(sessionGeneric);
|
|
|
|
assert.ok(capturedToken);
|
|
assert.strictEqual(capturedToken.isCancellationRequested, false);
|
|
|
|
service.endSession(sessionGeneric);
|
|
|
|
assert.strictEqual(capturedToken.isCancellationRequested, true);
|
|
});
|
|
|
|
test('should be safe to call for unknown session', () => {
|
|
// Should not throw
|
|
service.endSession(URI.parse('vscode-chat-session://local/nonexistent'));
|
|
});
|
|
|
|
test('late provider should not be invoked for ended session', async () => {
|
|
const firstProvider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => [],
|
|
};
|
|
disposables.add(service.registerProvider(firstProvider));
|
|
await service.invokeProviders(sessionGeneric);
|
|
|
|
service.endSession(sessionGeneric);
|
|
|
|
let lateCalled = false;
|
|
const lateProvider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async () => {
|
|
lateCalled = true;
|
|
return [];
|
|
},
|
|
};
|
|
disposables.add(service.registerProvider(lateProvider));
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
assert.strictEqual(lateCalled, false, 'Late provider should not be invoked for ended session');
|
|
});
|
|
});
|
|
|
|
suite('dispose', () => {
|
|
test('should cancel active invocations on dispose', async () => {
|
|
let capturedToken: CancellationToken | undefined;
|
|
|
|
const provider: IChatDebugLogProvider = {
|
|
provideChatDebugLog: async (_sessionResource, token) => {
|
|
capturedToken = token;
|
|
return [];
|
|
},
|
|
};
|
|
|
|
disposables.add(service.registerProvider(provider));
|
|
await service.invokeProviders(sessionGeneric);
|
|
|
|
const cts = new CancellationTokenSource();
|
|
disposables.add(cts);
|
|
|
|
service.dispose();
|
|
|
|
assert.ok(capturedToken);
|
|
assert.strictEqual(capturedToken.isCancellationRequested, true);
|
|
});
|
|
});
|
|
});
|