From 8f0c8094a3047a08cccd56d94df4902664351577 Mon Sep 17 00:00:00 2001 From: Luca Chang Date: Wed, 17 Dec 2025 15:03:07 -0800 Subject: [PATCH] fix: disallow null (infinite) requested TTL The specification only allows the server to return a null TTL, it doesn't allow the client to request that. This is intentional, as it forces clients to make a choice betwen a specific TTL or not caring what the TTL is, rather than defaulting to keeping tasks forever all the time. Allowing the requested value to be null was an implementation oversight that diverged from the spec. --- src/shared/protocol.ts | 2 +- src/types.ts | 5 ++-- .../tasks/stores/in-memory.test.ts | 11 ++++---- test/experimental/tasks/task.test.ts | 28 +++++++++++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index aa242a647..c0670c4e9 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -265,7 +265,7 @@ export type RequestHandlerExtra { expect(task).toBeNull(); }); - it('should support null TTL for unlimited lifetime', async () => { - // Test that null TTL means unlimited lifetime - const taskParams: TaskCreationParams = { - ttl: null - }; + it('should support omitted TTL for unlimited lifetime', async () => { + // Test that omitting TTL means unlimited lifetime (server returns null) + // Per spec: clients omit ttl to let server decide, server returns null for unlimited + const taskParams: TaskCreationParams = {}; const createdTask = await store.createTask(taskParams, 2222, { method: 'tools/call', params: {} }); - // The returned task should have null TTL + // The returned task should have null TTL (unlimited) expect(createdTask.ttl).toBeNull(); // Task should not be cleaned up even after a long time diff --git a/test/experimental/tasks/task.test.ts b/test/experimental/tasks/task.test.ts index 37e3938d2..de613a325 100644 --- a/test/experimental/tasks/task.test.ts +++ b/test/experimental/tasks/task.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { isTerminal } from '../../../src/experimental/tasks/interfaces.js'; import type { Task } from '../../../src/types.js'; +import { TaskCreationParamsSchema } from '../../../src/types.js'; describe('Task utility functions', () => { describe('isTerminal', () => { @@ -115,3 +116,30 @@ describe('Task Schema Validation', () => { }); }); }); + +describe('TaskCreationParams Schema Validation', () => { + it('should accept ttl as a number', () => { + const result = TaskCreationParamsSchema.safeParse({ ttl: 60000 }); + expect(result.success).toBe(true); + }); + + it('should accept missing ttl (optional)', () => { + const result = TaskCreationParamsSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + it('should reject null ttl (not allowed in request, only response)', () => { + const result = TaskCreationParamsSchema.safeParse({ ttl: null }); + expect(result.success).toBe(false); + }); + + it('should accept pollInterval as a number', () => { + const result = TaskCreationParamsSchema.safeParse({ pollInterval: 1000 }); + expect(result.success).toBe(true); + }); + + it('should accept both ttl and pollInterval', () => { + const result = TaskCreationParamsSchema.safeParse({ ttl: 60000, pollInterval: 1000 }); + expect(result.success).toBe(true); + }); +});