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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The MCP server (TypeScriptServer~) exists as a separate component but is not the

The C# namespace is `io.github.hatayama.uLoopMCP` for historical reasons, but this is a CLI-based tool, not an MCP tool.

Comments in the code, commit messages, and PR titles and bodies should be written in English.
Comments in the code, commit messages, PR titles, and PR descriptions must all be written in English.

## Skill Description Guidelines

Expand Down
14 changes: 13 additions & 1 deletion Packages/src/Cli~/src/__tests__/cli-project-error.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getProjectResolutionErrorLines } from '../cli-project-error.js';
import { UnityNotRunningError } from '../port-resolver.js';
import { UnityNotRunningError, UnityServerNotRunningError } from '../port-resolver.js';
import { ProjectMismatchError } from '../project-validator.js';

describe('getProjectResolutionErrorLines', () => {
Expand Down Expand Up @@ -30,4 +30,16 @@ describe('getProjectResolutionErrorLines', () => {
'Start the Unity Editor for this project, or use --project-path to specify the target.',
]);
});

it('returns server-not-running guidance for UnityServerNotRunningError', () => {
const lines = getProjectResolutionErrorLines(new UnityServerNotRunningError('/project/root'));

expect(lines).toEqual([
'Error: Unity Editor is running, but Unity CLI Loop server is not.',
'',
' Project: /project/root',
'',
'Start the server from: Window > Unity CLI Loop > Server',
]);
});
});
69 changes: 67 additions & 2 deletions Packages/src/Cli~/src/__tests__/execute-tool.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { isTransportDisconnectError } from '../execute-tool.js';
import { UnityNotRunningError } from '../port-resolver.js';
import {
diagnoseRetryableProjectConnectionError,
isTransportDisconnectError,
} from '../execute-tool.js';
import { UnityNotRunningError, UnityServerNotRunningError } from '../port-resolver.js';
import { ProjectMismatchError } from '../project-validator.js';

describe('isTransportDisconnectError', () => {
Expand Down Expand Up @@ -35,7 +38,69 @@ describe('isTransportDisconnectError', () => {
expect(isTransportDisconnectError(new UnityNotRunningError('/project'))).toBe(false);
});

it('returns false for UnityServerNotRunningError', () => {
expect(isTransportDisconnectError(new UnityServerNotRunningError('/project'))).toBe(false);
});

it('returns false for ProjectMismatchError', () => {
expect(isTransportDisconnectError(new ProjectMismatchError('/a', '/b'))).toBe(false);
});
});

describe('diagnoseRetryableProjectConnectionError', () => {
it('returns UnityNotRunningError when connection fails and Unity is not running', async () => {
const error = await diagnoseRetryableProjectConnectionError(
new Error('Connection error: connect ECONNREFUSED 127.0.0.1:8711'),
'/project',
true,
{
findRunningUnityProcessForProjectFn: jest.fn().mockResolvedValue(null),
},
);

expect(error).toBeInstanceOf(UnityNotRunningError);
});

it('returns UnityServerNotRunningError when Unity is running but server is unavailable', async () => {
const error = await diagnoseRetryableProjectConnectionError(
new Error('UNITY_NO_RESPONSE'),
'/project',
true,
{
findRunningUnityProcessForProjectFn: jest.fn().mockResolvedValue({ pid: 1234 }),
},
);

expect(error).toBeInstanceOf(UnityServerNotRunningError);
});

it('preserves non-retryable errors', async () => {
const originalError = new ProjectMismatchError('/expected', '/actual');

const error = await diagnoseRetryableProjectConnectionError(originalError, '/project', true, {
findRunningUnityProcessForProjectFn: jest.fn(),
});

expect(error).toBe(originalError);
});

it('preserves retryable errors when project diagnosis is disabled', async () => {
const originalError = new Error('Connection error: connect ECONNREFUSED 127.0.0.1:8711');

const error = await diagnoseRetryableProjectConnectionError(originalError, '/project', false, {
findRunningUnityProcessForProjectFn: jest.fn(),
});

expect(error).toBe(originalError);
});

it('preserves the original error when OS-level process inspection fails', async () => {
const originalError = new Error('Connection error: connect ECONNREFUSED 127.0.0.1:8711');

const error = await diagnoseRetryableProjectConnectionError(originalError, '/project', true, {
findRunningUnityProcessForProjectFn: jest.fn().mockRejectedValue(new Error('ps failed')),
});

expect(error).toBe(originalError);
});
});
8 changes: 3 additions & 5 deletions Packages/src/Cli~/src/__tests__/port-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
resolvePortFromUnitySettings,
validateProjectPath,
resolveUnityPort,
UnityNotRunningError,
} from '../port-resolver.js';

describe('resolvePortFromUnitySettings', () => {
Expand Down Expand Up @@ -93,15 +92,14 @@ describe('resolveUnityPort with project settings', () => {
rmSync(tempProjectRoot, { recursive: true });
});

it('throws UnityNotRunningError when isServerRunning is false', async () => {
it('returns port when isServerRunning is false', async () => {
writeFileSync(
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json'),
JSON.stringify({ isServerRunning: false, customPort: 8700 }),
);

await expect(resolveUnityPort(undefined, tempProjectRoot)).rejects.toThrow(
UnityNotRunningError,
);
const port = await resolveUnityPort(undefined, tempProjectRoot);
expect(port).toBe(8700);
});

it('returns port when isServerRunning is true', async () => {
Expand Down
Loading