-
-
Notifications
You must be signed in to change notification settings - Fork 193
feat(simulator): migrate build_sim to session-aware defaults; concise description #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a72960d
a613b9f
654025b
99c77be
1d57c06
0307df8
e1c4629
9046aa9
cb6156a
de72b23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,3 +108,4 @@ bundled/ | |
| /.mcpregistry_registry_token | ||
| /key.pem | ||
| .mcpli | ||
| .factory | ||
Large diffs are not rendered by default.
| 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({ | ||
| 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' }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting defaults in this test without clearing them leaves the singleton |
||
| 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'); | ||
| }); | ||
| }); | ||
| }); | ||
| 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'], | ||
| }; |
| 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), | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beforeEachseeds the singletonsessionStore, 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 anafterEach(() => sessionStore.clear());(or clear inside the failing test) so each spec leaves the store in a clean state.