Skip to content

Commit e1c4629

Browse files
committed
fix(session-aware): trigger exclusivePairs pruning when user explicitly provides null/undefined; add tests for null/undefined activation
1 parent 0307df8 commit e1c4629

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

src/utils/__tests__/session-aware-tool-factory.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,48 @@ describe('createSessionAwareTool', () => {
109109
expect(result.content[0].text).toContain('Parameter validation failed');
110110
expect(result.content[0].text).toContain('Tip: set session defaults');
111111
});
112+
113+
it('exclusivePairs should prune conflicting session defaults when user provides null', async () => {
114+
const handlerWithExclusive = createSessionAwareTool<Params>({
115+
internalSchema,
116+
logicFunction: logic,
117+
getExecutor: () => createMockExecutor({ success: true }),
118+
requirements: [
119+
{ allOf: ['scheme'], message: 'scheme is required' },
120+
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
121+
],
122+
exclusivePairs: [['projectPath', 'workspacePath']],
123+
});
124+
125+
sessionStore.setDefaults({
126+
scheme: 'App',
127+
projectPath: '/path/proj.xcodeproj',
128+
});
129+
130+
const res = await handlerWithExclusive({ workspacePath: null as unknown as string });
131+
expect(res.isError).toBe(true);
132+
expect(res.content[0].text).toContain('Provide a project or workspace');
133+
});
134+
135+
it('exclusivePairs should prune when user provides undefined (key present)', async () => {
136+
const handlerWithExclusive = createSessionAwareTool<Params>({
137+
internalSchema,
138+
logicFunction: logic,
139+
getExecutor: () => createMockExecutor({ success: true }),
140+
requirements: [
141+
{ allOf: ['scheme'], message: 'scheme is required' },
142+
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
143+
],
144+
exclusivePairs: [['projectPath', 'workspacePath']],
145+
});
146+
147+
sessionStore.setDefaults({
148+
scheme: 'App',
149+
projectPath: '/path/proj.xcodeproj',
150+
});
151+
152+
const res = await handlerWithExclusive({ workspacePath: undefined as unknown as string });
153+
expect(res.isError).toBe(true);
154+
expect(res.content[0].text).toContain('Provide a project or workspace');
155+
});
112156
});

src/utils/typed-tool-factory.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,12 @@ export function createSessionAwareTool<TParams>(opts: {
9292
// Start with session defaults merged with explicit args (args override session)
9393
const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...rawArgs };
9494

95-
// Apply exclusive pair pruning: if caller provided a key in a pair, remove other keys
96-
// from that pair which came only from session defaults (not explicitly provided)
95+
// Apply exclusive pair pruning: if caller provided/touched any key in a pair (even null/undefined),
96+
// remove other keys from that pair which came only from session defaults (not explicitly provided).
97+
// This ensures requirements and validation reflect the effective, post-prune payload.
9798
for (const pair of exclusivePairs) {
98-
const provided = pair.filter((k) => rawArgs[k] != null);
99-
if (provided.length > 0) {
99+
const userTouched = pair.some((k) => Object.prototype.hasOwnProperty.call(rawArgs, k));
100+
if (userTouched) {
100101
for (const k of pair) {
101102
if (rawArgs[k] == null && merged[k] != null) {
102103
delete merged[k];

0 commit comments

Comments
 (0)