Skip to content

Commit ea42521

Browse files
authored
feat(simulator): migrate build_sim to session-aware defaults; concise description (getsentry#120)
* feat(session-management): add session defaults store, tools, and session-aware middleware; add tests; update plan doc - Add in-memory SessionStore (set/get/clear/show) - New workflow: session-management (session-set-defaults, session-clear-defaults, session-show-defaults) - Add createSessionAwareTool with requirements preflight and args-over-defaults merge - Comprehensive tests for tools, store, and factory - Update session_management_plan.md with DI checklist, commit protocol, and pre-commit hook note Existing tools unchanged; no remote push. * feat(simulator): migrate build_sim to session-aware defaults and concise description docs(session-management): add Tool Description Policy and update plan example test: update build_sim and session-management tests for session-aware preflight and description * fix(session-management): enforce afterEach cleanup; correct CLI example; improve oneOf error hints; add exclusivePairs pruning; clear([]) no-op; typo fix in description * docs(session-management): clarify required local checks (format + build) and mandate quick manual CLI test before review * docs(session-management): note mcpli dash/underscore tool-name normalization; hyphenated CLI samples are valid * fix(session-aware): evaluate allOf/oneOf requirements against post-prune merged payload to align with exclusivePairs pruning * fix(session-aware): trigger exclusivePairs pruning when user explicitly provides null/undefined; add tests for null/undefined activation * fix(session-management): session-set-defaults clears mutually exclusive counterparts before merge to prevent invalid store state; add tests * fix(session-management): reject mutually exclusive defaults in session-set-defaults via schema refine; fix exclusivePairs pruning to ignore null/undefined and not override session defaults; update tests * fix(session-aware): reject multiple explicit args in exclusivePairs at factory level; add test; update build_sim tests to expect factory message
1 parent e9d9b7d commit ea42521

16 files changed

Lines changed: 1348 additions & 153 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,4 @@ bundled/
108108
/.mcpregistry_registry_token
109109
/key.pem
110110
.mcpli
111+
.factory

docs/session_management_plan.md

Lines changed: 485 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Tests for session-management workflow metadata
3+
*/
4+
import { describe, it, expect } from 'vitest';
5+
import { workflow } from '../index.ts';
6+
7+
describe('session-management workflow metadata', () => {
8+
describe('Workflow Structure', () => {
9+
it('should export workflow object with required properties', () => {
10+
expect(workflow).toHaveProperty('name');
11+
expect(workflow).toHaveProperty('description');
12+
expect(workflow).toHaveProperty('platforms');
13+
expect(workflow).toHaveProperty('targets');
14+
expect(workflow).toHaveProperty('capabilities');
15+
});
16+
17+
it('should have correct workflow name', () => {
18+
expect(workflow.name).toBe('session-management');
19+
});
20+
21+
it('should have correct description', () => {
22+
expect(workflow.description).toBe(
23+
'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.',
24+
);
25+
});
26+
27+
it('should have correct platforms array', () => {
28+
expect(workflow.platforms).toEqual(['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS']);
29+
});
30+
31+
it('should have correct targets array', () => {
32+
expect(workflow.targets).toEqual(['simulator', 'device']);
33+
});
34+
35+
it('should have correct capabilities array', () => {
36+
expect(workflow.capabilities).toEqual(['configuration', 'state-management']);
37+
});
38+
});
39+
40+
describe('Workflow Validation', () => {
41+
it('should have valid string properties', () => {
42+
expect(typeof workflow.name).toBe('string');
43+
expect(typeof workflow.description).toBe('string');
44+
expect(workflow.name.length).toBeGreaterThan(0);
45+
expect(workflow.description.length).toBeGreaterThan(0);
46+
});
47+
48+
it('should have valid array properties', () => {
49+
expect(Array.isArray(workflow.platforms)).toBe(true);
50+
expect(Array.isArray(workflow.targets)).toBe(true);
51+
expect(Array.isArray(workflow.capabilities)).toBe(true);
52+
53+
expect(workflow.platforms.length).toBeGreaterThan(0);
54+
expect(workflow.targets.length).toBeGreaterThan(0);
55+
expect(workflow.capabilities.length).toBeGreaterThan(0);
56+
});
57+
});
58+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { sessionStore } from '../../../../utils/session-store.ts';
3+
import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts';
4+
5+
describe('session-clear-defaults tool', () => {
6+
beforeEach(() => {
7+
sessionStore.clear();
8+
sessionStore.setDefaults({
9+
scheme: 'MyScheme',
10+
projectPath: '/path/to/proj.xcodeproj',
11+
simulatorName: 'iPhone 16',
12+
deviceId: 'DEVICE-123',
13+
useLatestOS: true,
14+
arch: 'arm64',
15+
});
16+
});
17+
18+
afterEach(() => {
19+
sessionStore.clear();
20+
});
21+
22+
describe('Export Field Validation (Literal)', () => {
23+
it('should have correct name', () => {
24+
expect(plugin.name).toBe('session-clear-defaults');
25+
});
26+
27+
it('should have correct description', () => {
28+
expect(plugin.description).toBe('Clear selected or all session defaults.');
29+
});
30+
31+
it('should have handler function', () => {
32+
expect(typeof plugin.handler).toBe('function');
33+
});
34+
35+
it('should have schema object', () => {
36+
expect(plugin.schema).toBeDefined();
37+
expect(typeof plugin.schema).toBe('object');
38+
});
39+
});
40+
41+
describe('Handler Behavior', () => {
42+
it('should clear specific keys when provided', async () => {
43+
const result = await sessionClearDefaultsLogic({ keys: ['scheme', 'deviceId'] });
44+
expect(result.isError).toBe(false);
45+
expect(result.content[0].text).toContain('Session defaults cleared');
46+
47+
const current = sessionStore.getAll();
48+
expect(current.scheme).toBeUndefined();
49+
expect(current.deviceId).toBeUndefined();
50+
expect(current.projectPath).toBe('/path/to/proj.xcodeproj');
51+
expect(current.simulatorName).toBe('iPhone 16');
52+
expect(current.useLatestOS).toBe(true);
53+
expect(current.arch).toBe('arm64');
54+
});
55+
56+
it('should clear all when all=true', async () => {
57+
const result = await sessionClearDefaultsLogic({ all: true });
58+
expect(result.isError).toBe(false);
59+
expect(result.content[0].text).toBe('Session defaults cleared');
60+
61+
const current = sessionStore.getAll();
62+
expect(Object.keys(current).length).toBe(0);
63+
});
64+
65+
it('should clear all when no params provided', async () => {
66+
const result = await sessionClearDefaultsLogic({});
67+
expect(result.isError).toBe(false);
68+
const current = sessionStore.getAll();
69+
expect(Object.keys(current).length).toBe(0);
70+
});
71+
72+
it('should validate keys enum', async () => {
73+
const result = (await plugin.handler({ keys: ['invalid' as any] })) as any;
74+
expect(result.isError).toBe(true);
75+
expect(result.content[0].text).toContain('Parameter validation failed');
76+
expect(result.content[0].text).toContain('keys');
77+
});
78+
});
79+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { sessionStore } from '../../../../utils/session-store.ts';
3+
import plugin, { sessionSetDefaultsLogic } from '../session_set_defaults.ts';
4+
5+
describe('session-set-defaults tool', () => {
6+
beforeEach(() => {
7+
sessionStore.clear();
8+
});
9+
10+
describe('Export Field Validation (Literal)', () => {
11+
it('should have correct name', () => {
12+
expect(plugin.name).toBe('session-set-defaults');
13+
});
14+
15+
it('should have correct description', () => {
16+
expect(plugin.description).toBe(
17+
'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.',
18+
);
19+
});
20+
21+
it('should have handler function', () => {
22+
expect(typeof plugin.handler).toBe('function');
23+
});
24+
25+
it('should have schema object', () => {
26+
expect(plugin.schema).toBeDefined();
27+
expect(typeof plugin.schema).toBe('object');
28+
});
29+
});
30+
31+
describe('Handler Behavior', () => {
32+
it('should set provided defaults and return updated state', async () => {
33+
const result = await sessionSetDefaultsLogic({
34+
scheme: 'MyScheme',
35+
simulatorName: 'iPhone 16',
36+
useLatestOS: true,
37+
arch: 'arm64',
38+
});
39+
40+
expect(result.isError).toBe(false);
41+
expect(result.content[0].text).toContain('Defaults updated:');
42+
43+
const current = sessionStore.getAll();
44+
expect(current.scheme).toBe('MyScheme');
45+
expect(current.simulatorName).toBe('iPhone 16');
46+
expect(current.useLatestOS).toBe(true);
47+
expect(current.arch).toBe('arm64');
48+
});
49+
50+
it('should validate parameter types via Zod', async () => {
51+
const result = await plugin.handler({
52+
useLatestOS: 'yes' as unknown as boolean,
53+
});
54+
55+
expect(result.isError).toBe(true);
56+
expect(result.content[0].text).toContain('Parameter validation failed');
57+
expect(result.content[0].text).toContain('useLatestOS');
58+
});
59+
60+
it('should clear workspacePath when projectPath is set', async () => {
61+
sessionStore.setDefaults({ workspacePath: '/old/App.xcworkspace' });
62+
await sessionSetDefaultsLogic({ projectPath: '/new/App.xcodeproj' });
63+
const current = sessionStore.getAll();
64+
expect(current.projectPath).toBe('/new/App.xcodeproj');
65+
expect(current.workspacePath).toBeUndefined();
66+
});
67+
68+
it('should clear projectPath when workspacePath is set', async () => {
69+
sessionStore.setDefaults({ projectPath: '/old/App.xcodeproj' });
70+
await sessionSetDefaultsLogic({ workspacePath: '/new/App.xcworkspace' });
71+
const current = sessionStore.getAll();
72+
expect(current.workspacePath).toBe('/new/App.xcworkspace');
73+
expect(current.projectPath).toBeUndefined();
74+
});
75+
76+
it('should clear simulatorName when simulatorId is set', async () => {
77+
sessionStore.setDefaults({ simulatorName: 'iPhone 16' });
78+
await sessionSetDefaultsLogic({ simulatorId: 'SIM-UUID' });
79+
const current = sessionStore.getAll();
80+
expect(current.simulatorId).toBe('SIM-UUID');
81+
expect(current.simulatorName).toBeUndefined();
82+
});
83+
84+
it('should clear simulatorId when simulatorName is set', async () => {
85+
sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
86+
await sessionSetDefaultsLogic({ simulatorName: 'iPhone 16' });
87+
const current = sessionStore.getAll();
88+
expect(current.simulatorName).toBe('iPhone 16');
89+
expect(current.simulatorId).toBeUndefined();
90+
});
91+
92+
it('should reject when both projectPath and workspacePath are provided', async () => {
93+
const res = await plugin.handler({
94+
projectPath: '/app/App.xcodeproj',
95+
workspacePath: '/app/App.xcworkspace',
96+
});
97+
expect(res.isError).toBe(true);
98+
expect(res.content[0].text).toContain('Parameter validation failed');
99+
expect(res.content[0].text).toContain('projectPath and workspacePath are mutually exclusive');
100+
});
101+
102+
it('should reject when both simulatorId and simulatorName are provided', async () => {
103+
const res = await plugin.handler({
104+
simulatorId: 'SIM-1',
105+
simulatorName: 'iPhone 16',
106+
});
107+
expect(res.isError).toBe(true);
108+
expect(res.content[0].text).toContain('Parameter validation failed');
109+
expect(res.content[0].text).toContain('simulatorId and simulatorName are mutually exclusive');
110+
});
111+
});
112+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { sessionStore } from '../../../../utils/session-store.ts';
3+
import plugin from '../session_show_defaults.ts';
4+
5+
describe('session-show-defaults tool', () => {
6+
beforeEach(() => {
7+
sessionStore.clear();
8+
});
9+
10+
afterEach(() => {
11+
sessionStore.clear();
12+
});
13+
14+
describe('Export Field Validation (Literal)', () => {
15+
it('should have correct name', () => {
16+
expect(plugin.name).toBe('session-show-defaults');
17+
});
18+
19+
it('should have correct description', () => {
20+
expect(plugin.description).toBe('Show current session defaults.');
21+
});
22+
23+
it('should have handler function', () => {
24+
expect(typeof plugin.handler).toBe('function');
25+
});
26+
27+
it('should have empty schema', () => {
28+
expect(plugin.schema).toEqual({});
29+
});
30+
});
31+
32+
describe('Handler Behavior', () => {
33+
it('should return empty defaults when none set', async () => {
34+
const result = await plugin.handler({});
35+
expect(result.isError).toBe(false);
36+
const parsed = JSON.parse(result.content[0].text);
37+
expect(parsed).toEqual({});
38+
});
39+
40+
it('should return current defaults when set', async () => {
41+
sessionStore.setDefaults({ scheme: 'MyScheme', simulatorId: 'SIM-123' });
42+
const result = await plugin.handler({});
43+
expect(result.isError).toBe(false);
44+
const parsed = JSON.parse(result.content[0].text);
45+
expect(parsed.scheme).toBe('MyScheme');
46+
expect(parsed.simulatorId).toBe('SIM-123');
47+
});
48+
});
49+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const workflow = {
2+
name: 'session-management',
3+
description:
4+
'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.',
5+
platforms: ['iOS', 'macOS', 'tvOS', 'watchOS', 'visionOS'],
6+
targets: ['simulator', 'device'],
7+
capabilities: ['configuration', 'state-management'],
8+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { z } from 'zod';
2+
import { sessionStore } from '../../../utils/session-store.ts';
3+
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
4+
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
5+
import type { ToolResponse } from '../../../types/common.ts';
6+
7+
const keys = [
8+
'projectPath',
9+
'workspacePath',
10+
'scheme',
11+
'configuration',
12+
'simulatorName',
13+
'simulatorId',
14+
'deviceId',
15+
'useLatestOS',
16+
'arch',
17+
] as const;
18+
19+
const schemaObj = z.object({
20+
keys: z.array(z.enum(keys)).optional(),
21+
all: z.boolean().optional(),
22+
});
23+
24+
type Params = z.infer<typeof schemaObj>;
25+
26+
export async function sessionClearDefaultsLogic(params: Params): Promise<ToolResponse> {
27+
if (params.all || !params.keys) sessionStore.clear();
28+
else sessionStore.clear(params.keys);
29+
return { content: [{ type: 'text', text: 'Session defaults cleared' }], isError: false };
30+
}
31+
32+
export default {
33+
name: 'session-clear-defaults',
34+
description: 'Clear selected or all session defaults.',
35+
schema: schemaObj.shape,
36+
handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor),
37+
};

0 commit comments

Comments
 (0)