Skip to content

Commit 174191c

Browse files
committed
Merge branch 'master' into release
2 parents 7bd9cd0 + 1033b70 commit 174191c

File tree

5 files changed

+321
-47
lines changed

5 files changed

+321
-47
lines changed

libs/nx-mcp/nx-mcp-server/.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": ["../../../.eslintrc.json"],
3-
"ignorePatterns": ["!**/*"],
3+
"ignorePatterns": ["!**/*", "out-tsc", "test-output", "dist"],
44
"overrides": [
55
{
66
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],

libs/nx-mcp/nx-mcp-server/.spec.swcrc

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
/* eslint-disable */
2-
import { readFileSync } from 'fs';
3-
4-
// Reading the SWC compilation config for the spec files
5-
const swcJestConfig = JSON.parse(
6-
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'),
7-
);
8-
9-
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
10-
swcJestConfig.swcrc = false;
11-
122
export default {
133
displayName: 'nx-mcp-server',
144
preset: '../../../jest.preset.js',
5+
globals: {},
156
testEnvironment: 'node',
167
transform: {
17-
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
8+
'^.+\\.[tj]sx?$': [
9+
'ts-jest',
10+
{
11+
tsconfig: '<rootDir>/tsconfig.spec.json',
12+
},
13+
],
1814
},
19-
moduleFileExtensions: ['ts', 'js', 'html'],
15+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
2016
coverageDirectory: 'test-output/jest/coverage',
21-
passWithNoTests: true,
2217
};
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { nxCurrentlyRunningTaskOutput } from './nx-tasks';
2+
import { IdeProvider } from '../ide-provider';
3+
import { RunningTasksMap, TaskStatus } from '@nx-console/shared-running-tasks';
4+
5+
describe('nxCurrentlyRunningTaskOutput - bottom-up pagination', () => {
6+
const TASK_OUTPUT_CHUNK_SIZE = 10000;
7+
8+
function generateOutput(size: number): string {
9+
const line =
10+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n';
11+
let output = '';
12+
while (output.length < size) {
13+
output += line;
14+
}
15+
return output;
16+
}
17+
18+
let mockIdeProvider: IdeProvider;
19+
let mockRunningTasks: RunningTasksMap;
20+
let taskOutputHandler: ReturnType<typeof nxCurrentlyRunningTaskOutput>;
21+
22+
beforeEach(() => {
23+
const largeOutput = generateOutput(25000); // 25KB output
24+
const smallOutput = 'Small task output\n'.repeat(10);
25+
const uniqueOutput =
26+
'START\n' + 'x'.repeat(TASK_OUTPUT_CHUNK_SIZE * 2.5) + '\nEND';
27+
28+
mockRunningTasks = {
29+
'large-task-id': {
30+
name: 'build:production',
31+
status: 'running' as TaskStatus,
32+
continuous: false,
33+
output: largeOutput,
34+
connectionId: 'test-connection',
35+
overallRunStatus: 'running' as TaskStatus,
36+
},
37+
'small-task-id': {
38+
name: 'test:unit',
39+
status: 'completed' as TaskStatus,
40+
continuous: false,
41+
output: smallOutput,
42+
connectionId: 'test-connection',
43+
overallRunStatus: 'completed' as TaskStatus,
44+
},
45+
'unique-task-id': {
46+
name: 'serve:app',
47+
status: 'running' as TaskStatus,
48+
continuous: true,
49+
output: uniqueOutput,
50+
connectionId: 'test-connection',
51+
overallRunStatus: 'running' as TaskStatus,
52+
},
53+
'empty-task-id': {
54+
name: 'empty:task',
55+
status: 'completed' as TaskStatus,
56+
continuous: false,
57+
output: '',
58+
connectionId: 'test-connection',
59+
overallRunStatus: 'completed' as TaskStatus,
60+
},
61+
};
62+
63+
mockIdeProvider = {
64+
isAvailable: () => true,
65+
focusProject: jest.fn(),
66+
focusTask: jest.fn(),
67+
showFullProjectGraph: jest.fn(),
68+
openGenerateUi: jest.fn(),
69+
getRunningTasks: jest.fn().mockResolvedValue(mockRunningTasks),
70+
onConnectionChange: jest.fn(() => {
71+
// Return cleanup function
72+
return () => {
73+
// Cleanup logic
74+
};
75+
}),
76+
dispose: jest.fn(),
77+
};
78+
79+
taskOutputHandler = nxCurrentlyRunningTaskOutput(
80+
undefined,
81+
mockIdeProvider,
82+
);
83+
});
84+
85+
it('should return most recent chunk (from end) on page 0 for large output', async () => {
86+
const result = await taskOutputHandler({
87+
taskId: 'large-task-id',
88+
});
89+
90+
expect(result.content.length).toBe(2);
91+
92+
const taskOutput = result.content[0]?.text;
93+
expect(taskOutput).toContain('TaskId: build:production');
94+
expect(taskOutput).toContain('(status: running)');
95+
expect(taskOutput).toContain('Output:');
96+
expect(taskOutput).toContain('...[older output on page 1]');
97+
expect(taskOutput).not.toContain('(currently on page');
98+
99+
const paginationMessage = result.content[1]?.text;
100+
expect(paginationMessage).toContain('Next page token: 1');
101+
expect(paginationMessage).toContain('retrieve older output');
102+
103+
// Page 0 should contain the last part of the output
104+
const largeOutput = mockRunningTasks['large-task-id'].output;
105+
const lastChars = largeOutput.slice(-100);
106+
expect(taskOutput).toContain(lastChars);
107+
});
108+
109+
it('should return older content on page 1', async () => {
110+
const page0 = await taskOutputHandler({
111+
taskId: 'unique-task-id',
112+
pageToken: 0,
113+
});
114+
115+
const page1 = await taskOutputHandler({
116+
taskId: 'unique-task-id',
117+
pageToken: 1,
118+
});
119+
120+
const page0Text = page0.content[0]?.text;
121+
const page1Text = page1.content[0]?.text;
122+
123+
expect(page0Text).toContain('TaskId: serve:app');
124+
expect(page1Text).toContain('TaskId: serve:app');
125+
126+
// Page 0 should contain END marker (most recent)
127+
expect(page0Text).toContain('END');
128+
// Page 1 should not contain END marker (older content)
129+
expect(page1Text).not.toContain('END');
130+
// Page 1 should have "older output on page 2" indicator
131+
expect(page1Text).toContain('...[older output on page 2]');
132+
// Page 1 should have "currently on page 1" indicator
133+
expect(page1Text).toContain('(currently on page 1)');
134+
});
135+
136+
it('should not paginate when output fits in one chunk', async () => {
137+
const result = await taskOutputHandler({
138+
taskId: 'small-task-id',
139+
});
140+
141+
expect(result.content).toHaveLength(1);
142+
143+
const taskOutput = result.content[0]?.text;
144+
expect(taskOutput).toContain('TaskId: test:unit');
145+
expect(taskOutput).toContain('(status: completed)');
146+
expect(taskOutput).not.toContain('Next page token');
147+
expect(taskOutput).not.toContain('...[older output');
148+
149+
expect(result.content[1]).toBeUndefined();
150+
});
151+
152+
it('should return "no more content" when page token beyond content', async () => {
153+
const result = await taskOutputHandler({
154+
taskId: 'large-task-id',
155+
pageToken: 10,
156+
});
157+
158+
expect(result.content).toHaveLength(1);
159+
expect(result.content[0]?.text).toContain(
160+
'build:production - no more content on page 10',
161+
);
162+
});
163+
164+
it('should handle empty output', async () => {
165+
const result = await taskOutputHandler({
166+
taskId: 'empty-task-id',
167+
});
168+
169+
expect(result.content).toHaveLength(1);
170+
expect(result.content[0]?.text).toContain(
171+
'No task outputs available for empty:task',
172+
);
173+
});
174+
175+
it('should handle task not found', async () => {
176+
const result = await taskOutputHandler({
177+
taskId: 'nonexistent-task',
178+
});
179+
180+
expect(result.content).toHaveLength(1);
181+
expect(result.content[0]?.text).toContain(
182+
'No task found with ID nonexistent-task',
183+
);
184+
});
185+
186+
it('should find task by partial name match', async () => {
187+
const result = await taskOutputHandler({
188+
taskId: 'build',
189+
});
190+
191+
expect(result.content.length).toBeGreaterThan(0);
192+
const taskOutput = result.content[0]?.text;
193+
expect(taskOutput).toContain('TaskId: build:production');
194+
});
195+
196+
it('should include continuous flag for continuous tasks', async () => {
197+
const result = await taskOutputHandler({
198+
taskId: 'unique-task-id',
199+
});
200+
201+
const taskOutput = result.content[0]?.text;
202+
expect(taskOutput).toContain('(continuous)');
203+
expect(taskOutput).toContain('serve:app');
204+
});
205+
206+
it('should correctly paginate exactly 2 chunks', async () => {
207+
const exactSizeOutput = 'x'.repeat(TASK_OUTPUT_CHUNK_SIZE * 2);
208+
mockRunningTasks['exact-task'] = {
209+
name: 'exact:task',
210+
status: 'running' as TaskStatus,
211+
continuous: false,
212+
output: exactSizeOutput,
213+
connectionId: 'test-connection',
214+
overallRunStatus: 'running' as TaskStatus,
215+
};
216+
217+
const page0 = await taskOutputHandler({
218+
taskId: 'exact-task',
219+
pageToken: 0,
220+
});
221+
const page1 = await taskOutputHandler({
222+
taskId: 'exact-task',
223+
pageToken: 1,
224+
});
225+
const page2 = await taskOutputHandler({
226+
taskId: 'exact-task',
227+
pageToken: 2,
228+
});
229+
230+
// Page 0 should have more content
231+
expect(page0.content.length).toBe(2);
232+
expect(page0.content[1]?.text).toContain('Next page token: 1');
233+
234+
// Page 1 should be the last page (no more content)
235+
expect(page1.content.length).toBe(1);
236+
expect(page1.content[0]?.text).not.toContain('Next page token');
237+
238+
// Page 2 should be beyond content
239+
expect(page2.content[0]?.text).toContain('no more content on page 2');
240+
});
241+
});

0 commit comments

Comments
 (0)