Skip to content

Commit 8cf2c23

Browse files
Copilottnaum-ms
andcommitted
Implement dummy and pausable tasks with comprehensive tests
Co-authored-by: tnaum-ms <171359267+tnaum-ms@users.noreply.github.com>
1 parent 6ce1527 commit 8cf2c23

5 files changed

Lines changed: 921 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TaskState } from '../taskService';
7+
import { DummyTask } from './DummyTask';
8+
9+
describe('DummyTask', () => {
10+
let task: DummyTask;
11+
12+
beforeEach(() => {
13+
task = new DummyTask('Test Dummy Task');
14+
});
15+
16+
afterEach(async () => {
17+
await task.delete();
18+
});
19+
20+
describe('constructor', () => {
21+
it('should initialize with correct properties', () => {
22+
const status = task.getStatus();
23+
24+
expect(task.id).toMatch(/^dummy-task-\d+$/);
25+
expect(task.type).toBe('dummy-task');
26+
expect(task.name).toBe('Test Dummy Task');
27+
expect(status.state).toBe(TaskState.Pending);
28+
expect(status.progress).toBe(0);
29+
expect(status.message).toBe('Task created and ready to start');
30+
});
31+
32+
it('should generate unique IDs for multiple instances', () => {
33+
const task1 = new DummyTask();
34+
const task2 = new DummyTask();
35+
36+
expect(task1.id).not.toBe(task2.id);
37+
38+
// Cleanup
39+
void task1.delete();
40+
void task2.delete();
41+
});
42+
43+
it('should use default name when none provided', () => {
44+
const defaultTask = new DummyTask();
45+
expect(defaultTask.name).toMatch(/^Dummy Task \d+$/);
46+
47+
void defaultTask.delete();
48+
});
49+
});
50+
51+
describe('start', () => {
52+
it('should transition through initialization states', async () => {
53+
const startPromise = task.start();
54+
55+
// Should be initializing briefly
56+
await new Promise(resolve => setTimeout(resolve, 50));
57+
let status = task.getStatus();
58+
expect(status.state).toBe(TaskState.Initializing);
59+
expect(status.message).toBe('Initializing task...');
60+
61+
await startPromise;
62+
63+
// Should now be running
64+
status = task.getStatus();
65+
expect(status.state).toBe(TaskState.Running);
66+
expect(status.message).toBe('Task execution started');
67+
});
68+
69+
it('should not allow starting twice', async () => {
70+
await task.start();
71+
72+
await expect(task.start()).rejects.toThrow('Cannot start task in state: running');
73+
});
74+
75+
it('should update progress over time', async () => {
76+
await task.start();
77+
78+
// Wait for at least one progress update
79+
await new Promise(resolve => setTimeout(resolve, 1100));
80+
81+
const status = task.getStatus();
82+
expect(status.state).toBe(TaskState.Running);
83+
expect(status.progress).toBeGreaterThan(0);
84+
expect(status.progress).toBeLessThanOrEqual(100);
85+
expect(status.message).toContain('Processing...');
86+
expect(status.message).toContain('% complete');
87+
});
88+
89+
it('should complete after approximately 10 seconds', async () => {
90+
await task.start();
91+
92+
// Wait for completion (with some buffer for timing)
93+
await new Promise(resolve => setTimeout(resolve, 11000));
94+
95+
const status = task.getStatus();
96+
expect(status.state).toBe(TaskState.Completed);
97+
expect(status.progress).toBe(100);
98+
expect(status.message).toBe('Task completed successfully');
99+
}, 15000); // Increase Jest timeout for this test
100+
});
101+
102+
describe('stop', () => {
103+
it('should stop a running task', async () => {
104+
await task.start();
105+
106+
// Let it run for a bit
107+
await new Promise(resolve => setTimeout(resolve, 1100));
108+
109+
await task.stop();
110+
111+
const status = task.getStatus();
112+
expect(status.state).toBe(TaskState.Stopped);
113+
expect(status.message).toBe('Task was stopped');
114+
});
115+
116+
it('should handle stopping before start', async () => {
117+
await task.stop();
118+
119+
const status = task.getStatus();
120+
expect(status.state).toBe(TaskState.Stopped);
121+
});
122+
123+
it('should handle multiple stop calls', async () => {
124+
await task.start();
125+
await task.stop();
126+
127+
// Second stop should not throw
128+
await expect(task.stop()).resolves.toBeUndefined();
129+
130+
const status = task.getStatus();
131+
expect(status.state).toBe(TaskState.Stopped);
132+
});
133+
134+
it('should preserve progress when stopped', async () => {
135+
await task.start();
136+
137+
// Wait for some progress
138+
await new Promise(resolve => setTimeout(resolve, 2100));
139+
140+
const progressBeforeStop = task.getStatus().progress;
141+
await task.stop();
142+
143+
const status = task.getStatus();
144+
expect(status.progress).toBe(progressBeforeStop);
145+
});
146+
});
147+
148+
describe('delete', () => {
149+
it('should stop and cleanup the task', async () => {
150+
await task.start();
151+
152+
// Let it run briefly
153+
await new Promise(resolve => setTimeout(resolve, 500));
154+
155+
await task.delete();
156+
157+
const status = task.getStatus();
158+
expect(status.state).toBe(TaskState.Stopped);
159+
});
160+
161+
it('should handle delete on pending task', async () => {
162+
await expect(task.delete()).resolves.toBeUndefined();
163+
164+
const status = task.getStatus();
165+
expect(status.state).toBe(TaskState.Stopped);
166+
});
167+
});
168+
169+
describe('abort signal handling', () => {
170+
it('should handle abort during initialization', async () => {
171+
const startPromise = task.start();
172+
173+
// Give it a tiny bit of time to enter initialization, then stop
174+
await new Promise(resolve => setTimeout(resolve, 10));
175+
await task.stop();
176+
await startPromise;
177+
178+
const status = task.getStatus();
179+
// The task might be in running state if start() completed before stop()
180+
// or stopped if stop() was processed first
181+
expect([TaskState.Stopped, TaskState.Running].includes(status.state)).toBe(true);
182+
});
183+
184+
it('should handle abort during execution', async () => {
185+
await task.start();
186+
187+
// Let it start running
188+
await new Promise(resolve => setTimeout(resolve, 1100));
189+
190+
await task.stop();
191+
192+
const status = task.getStatus();
193+
expect(status.state).toBe(TaskState.Stopped);
194+
});
195+
});
196+
197+
describe('getStatus', () => {
198+
it('should return a copy of status to prevent mutation', () => {
199+
const status1 = task.getStatus();
200+
const status2 = task.getStatus();
201+
202+
expect(status1).toEqual(status2);
203+
expect(status1).not.toBe(status2); // Different object references
204+
});
205+
});
206+
});

src/services/tasks/DummyTask.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TaskState, type Task, type TaskStatus } from '../taskService';
7+
8+
/**
9+
* A dummy task implementation that demonstrates basic task interface usage.
10+
*
11+
* This task runs for 10 seconds with 1-second progress intervals and demonstrates:
12+
* - Basic task state transitions
13+
* - Progress reporting
14+
* - Abort signal handling
15+
*/
16+
export class DummyTask implements Task {
17+
private static _instanceCounter = 0;
18+
19+
public readonly id: string;
20+
public readonly type: string = 'dummy-task';
21+
public readonly name: string;
22+
23+
private _status: TaskStatus;
24+
private _abortController: AbortController | undefined;
25+
private _progressInterval: NodeJS.Timeout | undefined;
26+
private _startTime: number | undefined;
27+
28+
constructor(name?: string) {
29+
DummyTask._instanceCounter++;
30+
this.id = `dummy-task-${DummyTask._instanceCounter}`;
31+
this.name = name ?? `Dummy Task ${DummyTask._instanceCounter}`;
32+
33+
this._status = {
34+
state: TaskState.Pending,
35+
progress: 0,
36+
message: 'Task created and ready to start'
37+
};
38+
}
39+
40+
public getStatus(): TaskStatus {
41+
return { ...this._status };
42+
}
43+
44+
public async start(): Promise<void> {
45+
if (this._status.state !== TaskState.Pending) {
46+
throw new Error(`Cannot start task in state: ${this._status.state}`);
47+
}
48+
49+
this._abortController = new AbortController();
50+
51+
this._status = {
52+
state: TaskState.Initializing,
53+
progress: 0,
54+
message: 'Initializing task...'
55+
};
56+
57+
// Brief initialization delay to simulate setup
58+
await this._delay(100);
59+
60+
if (this._abortController?.signal.aborted) {
61+
await this._handleAbort();
62+
return;
63+
}
64+
65+
this._status = {
66+
state: TaskState.Running,
67+
progress: 0,
68+
message: 'Task execution started'
69+
};
70+
71+
this._startTime = Date.now();
72+
73+
this._startProgressLoop();
74+
}
75+
76+
public async stop(): Promise<void> {
77+
if (this._status.state === TaskState.Completed ||
78+
this._status.state === TaskState.Failed ||
79+
this._status.state === TaskState.Stopped) {
80+
return; // Already in terminal state
81+
}
82+
83+
this._status = {
84+
...this._status,
85+
state: TaskState.Stopping,
86+
message: 'Stopping task...'
87+
};
88+
89+
if (this._abortController) {
90+
this._abortController.abort();
91+
}
92+
await this._cleanup();
93+
94+
this._status = {
95+
state: TaskState.Stopped,
96+
progress: this._status.progress,
97+
message: 'Task was stopped'
98+
};
99+
}
100+
101+
public async delete(): Promise<void> {
102+
await this.stop();
103+
await this._cleanup();
104+
}
105+
106+
private _startProgressLoop(): void {
107+
this._progressInterval = setInterval(() => {
108+
if (this._abortController?.signal.aborted) {
109+
void this._handleAbort();
110+
return;
111+
}
112+
113+
const elapsed = Date.now() - (this._startTime ?? Date.now());
114+
const progress = Math.min(100, Math.floor((elapsed / 10000) * 100)); // 10 seconds = 100%
115+
116+
this._status = {
117+
state: TaskState.Running,
118+
progress,
119+
message: `Processing... ${progress}% complete`
120+
};
121+
122+
if (progress >= 100) {
123+
this._status = {
124+
state: TaskState.Completed,
125+
progress: 100,
126+
message: 'Task completed successfully'
127+
};
128+
void this._cleanup();
129+
}
130+
}, 1000); // Update every second
131+
}
132+
133+
private async _handleAbort(): Promise<void> {
134+
await this._cleanup();
135+
this._status = {
136+
state: TaskState.Stopped,
137+
progress: this._status.progress ?? 0,
138+
message: 'Task was aborted'
139+
};
140+
}
141+
142+
private async _cleanup(): Promise<void> {
143+
if (this._progressInterval) {
144+
clearInterval(this._progressInterval);
145+
this._progressInterval = undefined;
146+
}
147+
this._abortController = undefined;
148+
}
149+
150+
private async _delay(ms: number): Promise<void> {
151+
return new Promise(resolve => setTimeout(resolve, ms));
152+
}
153+
}

0 commit comments

Comments
 (0)