diff --git a/ts/packages/agents/calendar/jest.config.cjs b/ts/packages/agents/calendar/jest.config.cjs new file mode 100644 index 000000000..f475768a1 --- /dev/null +++ b/ts/packages/agents/calendar/jest.config.cjs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +module.exports = require("../../../jest.config.js"); diff --git a/ts/packages/agents/calendar/package.json b/ts/packages/agents/calendar/package.json index 407fc6fbf..9c1bac775 100644 --- a/ts/packages/agents/calendar/package.json +++ b/ts/packages/agents/calendar/package.json @@ -21,9 +21,12 @@ "asc": "asc -i ./src/calendarActionsSchemaV3.ts -o ./dist/calendarSchema.pas.json -t CalendarActionV3 -e CalendarEntities", "build": "concurrently npm:tsc npm:asc npm:agc", "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", + "jest-esm": "node --no-warnings --experimental-vm-modules ./node_modules/jest/bin/jest.js", "prettier": "prettier --check . --ignore-path ../../../.prettierignore", "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", - "tsc": "tsc -b" + "test": "npm run test:local", + "test:local": "pnpm run jest-esm --testPathPattern=\".*[.]spec[.]js\"", + "tsc": "tsc -b src test" }, "dependencies": { "@typeagent/agent-sdk": "workspace:*", @@ -36,8 +39,10 @@ "devDependencies": { "@typeagent/action-schema-compiler": "workspace:*", "@types/debug": "^4.1.12", + "@types/jest": "^29.5.7", "action-grammar-compiler": "workspace:*", "concurrently": "^9.1.2", + "jest": "^29.7.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", "typescript": "~5.4.5" diff --git a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts index b7a365e2b..1909ac298 100644 --- a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts +++ b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts @@ -1117,15 +1117,37 @@ export function instantiate(): AppAgent { // These will be called by the grammar matcher to validate wildcard matches export function validateCalendarDate(value: string): boolean { - // TODO: Implement sophisticated date parsing - // For now, accept any non-empty string - return value.trim().length > 0; + const trimmed = value.trim(); + if (trimmed.length === 0) return false; + if (/^(today|tomorrow|yesterday)$/i.test(trimmed)) return true; + if (/^(next|last|this)\s+\w+$/i.test(trimmed)) return true; + return parseFuzzyDateString(trimmed) !== undefined; } export function validateCalendarTime(value: string): boolean { - // TODO: Implement sophisticated time parsing - // For now, accept any non-empty string - return value.trim().length > 0; + const trimmed = value.trim(); + if (trimmed.length === 0) return false; + if (/^(noon|midnight|morning|evening|afternoon|night)$/i.test(trimmed)) + return true; + if (/^\d{1,2}:\d{2}$/.test(trimmed)) { + const [h, m] = trimmed.split(":").map(Number); + return h >= 0 && h <= 23 && m >= 0 && m <= 59; + } + if (/^\d{1,2}(am|pm)$/i.test(trimmed)) { + const h = parseInt(trimmed, 10); + return h >= 1 && h <= 12; + } + if (/^\d{1,2}:\d{2}(am|pm)$/i.test(trimmed)) { + const [timePart] = trimmed.split(/[ap]m/i); + const [h, m] = timePart.split(":").map(Number); + return h >= 1 && h <= 12 && m >= 0 && m <= 59; + } + try { + parseTimeString(trimmed); + return true; + } catch { + return false; + } } export function validateEventDescription(value: string): boolean { diff --git a/ts/packages/agents/calendar/test/calendarValidation.spec.ts b/ts/packages/agents/calendar/test/calendarValidation.spec.ts new file mode 100644 index 000000000..a9089cc64 --- /dev/null +++ b/ts/packages/agents/calendar/test/calendarValidation.spec.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + validateCalendarDate, + validateCalendarTime, +} from "../src/calendarActionHandlerV3.js"; + +describe("validateCalendarDate", () => { + test("rejects empty string", () => { + expect(validateCalendarDate("")).toBe(false); + expect(validateCalendarDate(" ")).toBe(false); + }); + + test("accepts today/tomorrow/yesterday", () => { + expect(validateCalendarDate("today")).toBe(true); + expect(validateCalendarDate("Tomorrow")).toBe(true); + expect(validateCalendarDate("YESTERDAY")).toBe(true); + }); + + test("accepts relative terms (next/last/this)", () => { + expect(validateCalendarDate("next Monday")).toBe(true); + expect(validateCalendarDate("last Friday")).toBe(true); + expect(validateCalendarDate("this week")).toBe(true); + }); + + test("accepts parseable date strings", () => { + expect(validateCalendarDate("March 15, 2025")).toBe(true); + expect(validateCalendarDate("2025-03-15")).toBe(true); + }); + + test("rejects nonsense strings", () => { + expect(validateCalendarDate("xyzzy")).toBe(false); + expect(validateCalendarDate("notadate")).toBe(false); + }); +}); + +describe("validateCalendarTime", () => { + test("rejects empty string", () => { + expect(validateCalendarTime("")).toBe(false); + expect(validateCalendarTime(" ")).toBe(false); + }); + + test("accepts named times", () => { + expect(validateCalendarTime("noon")).toBe(true); + expect(validateCalendarTime("midnight")).toBe(true); + expect(validateCalendarTime("morning")).toBe(true); + expect(validateCalendarTime("evening")).toBe(true); + expect(validateCalendarTime("afternoon")).toBe(true); + expect(validateCalendarTime("night")).toBe(true); + }); + + test("accepts HH:MM 24-hour format", () => { + expect(validateCalendarTime("9:00")).toBe(true); + expect(validateCalendarTime("14:30")).toBe(true); + expect(validateCalendarTime("23:59")).toBe(true); + expect(validateCalendarTime("0:00")).toBe(true); + }); + + test("rejects invalid HH:MM values", () => { + expect(validateCalendarTime("25:00")).toBe(false); + expect(validateCalendarTime("12:60")).toBe(false); + }); + + test("accepts 12-hour format strings", () => { + expect(validateCalendarTime("3pm")).toBe(true); + expect(validateCalendarTime("10am")).toBe(true); + expect(validateCalendarTime("3:30pm")).toBe(true); + }); + + test("rejects nonsense strings", () => { + expect(validateCalendarTime("xyzzy")).toBe(false); + expect(validateCalendarTime("notatime")).toBe(false); + }); +}); diff --git a/ts/packages/agents/calendar/test/tsconfig.json b/ts/packages/agents/calendar/test/tsconfig.json new file mode 100644 index 000000000..895a6f0d2 --- /dev/null +++ b/ts/packages/agents/calendar/test/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "../dist/test", + "types": ["node", "jest"] + }, + "include": ["./**/*"], + "ts-node": { + "esm": true + }, + "references": [{ "path": "../src" }] +} diff --git a/ts/packages/agents/calendar/tsconfig.json b/ts/packages/agents/calendar/tsconfig.json index acb9cb4a9..94dfc60bb 100644 --- a/ts/packages/agents/calendar/tsconfig.json +++ b/ts/packages/agents/calendar/tsconfig.json @@ -4,7 +4,7 @@ "composite": true }, "include": [], - "references": [{ "path": "./src" }], + "references": [{ "path": "./src" }, { "path": "./test" }], "ts-node": { "esm": true } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index e412949ef..e19a6b5b8 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -1609,12 +1609,18 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/jest': + specifier: ^29.5.7 + version: 29.5.14 action-grammar-compiler: specifier: workspace:* version: link:../../actionGrammarCompiler concurrently: specifier: ^9.1.2 version: 9.1.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3