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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,4 @@ bundled/
/.mcpregistry_registry_token
/key.pem
.mcpli
.factory
485 changes: 485 additions & 0 deletions docs/session_management_plan.md

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions src/mcp/tools/session-management/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Tests for session-management workflow metadata
*/
import { describe, it, expect } from 'vitest';
import { workflow } from '../index.ts';

describe('session-management workflow metadata', () => {
describe('Workflow Structure', () => {
it('should export workflow object with required properties', () => {
expect(workflow).toHaveProperty('name');
expect(workflow).toHaveProperty('description');
expect(workflow).toHaveProperty('platforms');
expect(workflow).toHaveProperty('targets');
expect(workflow).toHaveProperty('capabilities');
});

it('should have correct workflow name', () => {
expect(workflow.name).toBe('session-management');
});

it('should have correct description', () => {
expect(workflow.description).toBe(
'Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values.',
);
});

it('should have correct platforms array', () => {
expect(workflow.platforms).toEqual(['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS']);
});

it('should have correct targets array', () => {
expect(workflow.targets).toEqual(['simulator', 'device']);
});

it('should have correct capabilities array', () => {
expect(workflow.capabilities).toEqual(['configuration', 'state-management']);
});
});

describe('Workflow Validation', () => {
it('should have valid string properties', () => {
expect(typeof workflow.name).toBe('string');
expect(typeof workflow.description).toBe('string');
expect(workflow.name.length).toBeGreaterThan(0);
expect(workflow.description.length).toBeGreaterThan(0);
});

it('should have valid array properties', () => {
expect(Array.isArray(workflow.platforms)).toBe(true);
expect(Array.isArray(workflow.targets)).toBe(true);
expect(Array.isArray(workflow.capabilities)).toBe(true);

expect(workflow.platforms.length).toBeGreaterThan(0);
expect(workflow.targets.length).toBeGreaterThan(0);
expect(workflow.capabilities.length).toBeGreaterThan(0);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts';

describe('session-clear-defaults tool', () => {
beforeEach(() => {
sessionStore.clear();
sessionStore.setDefaults({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beforeEach seeds the singleton sessionStore, but when validation fails (e.g. the "invalid" keys test) the store never gets cleared and stays populated after the suite finishes. That leaked state can break later tests that expect an empty store. Please add an afterEach(() => sessionStore.clear()); (or clear inside the failing test) so each spec leaves the store in a clean state.

scheme: 'MyScheme',
projectPath: '/path/to/proj.xcodeproj',
simulatorName: 'iPhone 16',
deviceId: 'DEVICE-123',
useLatestOS: true,
arch: 'arm64',
});
});

afterEach(() => {
sessionStore.clear();
});

describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(plugin.name).toBe('session-clear-defaults');
});

it('should have correct description', () => {
expect(plugin.description).toBe('Clear selected or all session defaults.');
});

it('should have handler function', () => {
expect(typeof plugin.handler).toBe('function');
});

it('should have schema object', () => {
expect(plugin.schema).toBeDefined();
expect(typeof plugin.schema).toBe('object');
});
});

describe('Handler Behavior', () => {
it('should clear specific keys when provided', async () => {
const result = await sessionClearDefaultsLogic({ keys: ['scheme', 'deviceId'] });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Session defaults cleared');

const current = sessionStore.getAll();
expect(current.scheme).toBeUndefined();
expect(current.deviceId).toBeUndefined();
expect(current.projectPath).toBe('/path/to/proj.xcodeproj');
expect(current.simulatorName).toBe('iPhone 16');
expect(current.useLatestOS).toBe(true);
expect(current.arch).toBe('arm64');
});

it('should clear all when all=true', async () => {
const result = await sessionClearDefaultsLogic({ all: true });
expect(result.isError).toBe(false);
expect(result.content[0].text).toBe('Session defaults cleared');

const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);
});

it('should clear all when no params provided', async () => {
const result = await sessionClearDefaultsLogic({});
expect(result.isError).toBe(false);
const current = sessionStore.getAll();
expect(Object.keys(current).length).toBe(0);
});

it('should validate keys enum', async () => {
const result = (await plugin.handler({ keys: ['invalid' as any] })) as any;
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Parameter validation failed');
expect(result.content[0].text).toContain('keys');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin, { sessionSetDefaultsLogic } from '../session_set_defaults.ts';

describe('session-set-defaults tool', () => {
beforeEach(() => {
sessionStore.clear();
});

describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(plugin.name).toBe('session-set-defaults');
});

it('should have correct description', () => {
expect(plugin.description).toBe(
'Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set the relevant defaults at the beginning of a session.',
);
});

it('should have handler function', () => {
expect(typeof plugin.handler).toBe('function');
});

it('should have schema object', () => {
expect(plugin.schema).toBeDefined();
expect(typeof plugin.schema).toBe('object');
});
});

describe('Handler Behavior', () => {
it('should set provided defaults and return updated state', async () => {
const result = await sessionSetDefaultsLogic({
scheme: 'MyScheme',
simulatorName: 'iPhone 16',
useLatestOS: true,
arch: 'arm64',
});

expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Defaults updated:');

const current = sessionStore.getAll();
expect(current.scheme).toBe('MyScheme');
expect(current.simulatorName).toBe('iPhone 16');
expect(current.useLatestOS).toBe(true);
expect(current.arch).toBe('arm64');
});

it('should validate parameter types via Zod', async () => {
const result = await plugin.handler({
useLatestOS: 'yes' as unknown as boolean,
});

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Parameter validation failed');
expect(result.content[0].text).toContain('useLatestOS');
});

it('should clear workspacePath when projectPath is set', async () => {
sessionStore.setDefaults({ workspacePath: '/old/App.xcworkspace' });
await sessionSetDefaultsLogic({ projectPath: '/new/App.xcodeproj' });
const current = sessionStore.getAll();
expect(current.projectPath).toBe('/new/App.xcodeproj');
expect(current.workspacePath).toBeUndefined();
});

it('should clear projectPath when workspacePath is set', async () => {
sessionStore.setDefaults({ projectPath: '/old/App.xcodeproj' });
await sessionSetDefaultsLogic({ workspacePath: '/new/App.xcworkspace' });
const current = sessionStore.getAll();
expect(current.workspacePath).toBe('/new/App.xcworkspace');
expect(current.projectPath).toBeUndefined();
});

it('should clear simulatorName when simulatorId is set', async () => {
sessionStore.setDefaults({ simulatorName: 'iPhone 16' });
await sessionSetDefaultsLogic({ simulatorId: 'SIM-UUID' });
const current = sessionStore.getAll();
expect(current.simulatorId).toBe('SIM-UUID');
expect(current.simulatorName).toBeUndefined();
});

it('should clear simulatorId when simulatorName is set', async () => {
sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
await sessionSetDefaultsLogic({ simulatorName: 'iPhone 16' });
const current = sessionStore.getAll();
expect(current.simulatorName).toBe('iPhone 16');
expect(current.simulatorId).toBeUndefined();
});

it('should reject when both projectPath and workspacePath are provided', async () => {
const res = await plugin.handler({
projectPath: '/app/App.xcodeproj',
workspacePath: '/app/App.xcworkspace',
});
expect(res.isError).toBe(true);
expect(res.content[0].text).toContain('Parameter validation failed');
expect(res.content[0].text).toContain('projectPath and workspacePath are mutually exclusive');
});

it('should reject when both simulatorId and simulatorName are provided', async () => {
const res = await plugin.handler({
simulatorId: 'SIM-1',
simulatorName: 'iPhone 16',
});
expect(res.isError).toBe(true);
expect(res.content[0].text).toContain('Parameter validation failed');
expect(res.content[0].text).toContain('simulatorId and simulatorName are mutually exclusive');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { sessionStore } from '../../../../utils/session-store.ts';
import plugin from '../session_show_defaults.ts';

describe('session-show-defaults tool', () => {
beforeEach(() => {
sessionStore.clear();
});

afterEach(() => {
sessionStore.clear();
});

describe('Export Field Validation (Literal)', () => {
it('should have correct name', () => {
expect(plugin.name).toBe('session-show-defaults');
});

it('should have correct description', () => {
expect(plugin.description).toBe('Show current session defaults.');
});

it('should have handler function', () => {
expect(typeof plugin.handler).toBe('function');
});

it('should have empty schema', () => {
expect(plugin.schema).toEqual({});
});
});

describe('Handler Behavior', () => {
it('should return empty defaults when none set', async () => {
const result = await plugin.handler({});
expect(result.isError).toBe(false);
const parsed = JSON.parse(result.content[0].text);
expect(parsed).toEqual({});
});

it('should return current defaults when set', async () => {
sessionStore.setDefaults({ scheme: 'MyScheme', simulatorId: 'SIM-123' });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting defaults in this test without clearing them leaves the singleton sessionStore populated after the suite ends. Because sessionStore is shared across all test files, running this spec before others will leak scheme/simulatorId into later tests that assume a clean store. Please add something like afterEach(() => sessionStore.clear()); (or clear at the end of this test) so the store is reset after every spec.

const result = await plugin.handler({});
expect(result.isError).toBe(false);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.scheme).toBe('MyScheme');
expect(parsed.simulatorId).toBe('SIM-123');
});
});
});
8 changes: 8 additions & 0 deletions src/mcp/tools/session-management/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const workflow = {
name: 'session-management',
description:
'Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values.',
platforms: ['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS'],
targets: ['simulator', 'device'],
capabilities: ['configuration', 'state-management'],
};
37 changes: 37 additions & 0 deletions src/mcp/tools/session-management/session_clear_defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from 'zod';
import { sessionStore } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import type { ToolResponse } from '../../../types/common.ts';

const keys = [
'projectPath',
'workspacePath',
'scheme',
'configuration',
'simulatorName',
'simulatorId',
'deviceId',
'useLatestOS',
'arch',
] as const;

const schemaObj = z.object({
keys: z.array(z.enum(keys)).optional(),
all: z.boolean().optional(),
});

type Params = z.infer<typeof schemaObj>;

export async function sessionClearDefaultsLogic(params: Params): Promise<ToolResponse> {
if (params.all || !params.keys) sessionStore.clear();
else sessionStore.clear(params.keys);
return { content: [{ type: 'text', text: 'Session defaults cleared' }], isError: false };
}

export default {
name: 'session-clear-defaults',
description: 'Clear selected or all session defaults.',
schema: schemaObj.shape,
handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor),
};
Loading
Loading