sync: update from private repo (9a86f49b)
CI / build-and-test (push) Successful in 9m8s
CI / build-and-test (pull_request) Successful in 9m42s

This commit is contained in:
oss-sync
2026-07-12 23:51:48 +00:00
parent a67c40d33c
commit ed3dc5529a
159 changed files with 11696 additions and 1600 deletions
+30
View File
@@ -34,6 +34,36 @@ describe('calendar tools', () => {
it('exposes TOOL_DEFS entries', () => {
expect(TOOL_DEFS.AddCalendarEvent?.function.name).toBe('AddCalendarEvent');
expect(TOOL_DEFS.ListCalendarEvents?.function.name).toBe('ListCalendarEvents');
expect(TOOL_DEFS.CreateReminder?.function.name).toBe('CreateReminder');
expect(TOOL_DEFS.ListReminders?.function.name).toBe('ListReminders');
expect(TOOL_DEFS.CancelReminder?.function.name).toBe('CancelReminder');
});
it('creates, lists, and cancels an owner-scoped relative reminder', async () => {
const created = await executeTool('CreateReminder', { body: '休憩する', delay_minutes: 20, title: '休憩' }, ctx());
expect(created?.isError).toBe(false);
const id = created!.output.match(/id=([^)]+)/)?.[1]!;
expect((await executeTool('ListReminders', {}, ctx()))?.output).toContain(id);
expect((await executeTool('CancelReminder', { id }, ctx()))?.isError).toBe(false);
expect((await executeTool('ListReminders', {}, ctx()))?.output).toContain('ありません');
});
it('rejects past or ambiguous reminder times', async () => {
expect((await executeTool('CreateReminder', { body: 'x', due_at: 'not-a-time' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: '2026-07-10T09:00' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: '2026-02-30T09:00:00Z' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: '2026-07-10T24:00Z' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: '2026-07-10T09:00+23:59' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: '2020-01-01T00:00:00.000Z' }, ctx()))?.isError).toBe(true);
expect((await executeTool('CreateReminder', { body: 'x', due_at: new Date(Date.now() + 60_000).toISOString(), delay_minutes: 1 }, ctx()))?.isError).toBe(true);
});
it('does not expose or cancel another owner\'s reminder', async () => {
const created = await executeTool('CreateReminder', { body: 'private', delay_minutes: 10 }, ctx());
const id = created!.output.match(/id=([^)]+)/)?.[1]!;
expect((await executeTool('ListReminders', {}, ctx({ ownerId: 'u2' })))?.output).toContain('ありません');
expect((await executeTool('CancelReminder', { id }, ctx({ ownerId: 'u2' })))?.isError).toBe(true);
expect((await executeTool('ListReminders', {}, ctx()))?.output).toContain(id);
});
it('AddCalendarEvent happy path: row created with createdBy=agent + sourceTaskId', async () => {
+76
View File
@@ -6,6 +6,14 @@ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
const MAX_TITLE_LEN = 200;
const MAX_DESC_LEN = 4000;
const MAX_REMINDER_BODY_LEN = 4000;
const ISO_WITH_TIMEZONE_PATTERN = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?(?:Z|[+-](?:(?:0\d|1[0-3]):[0-5]\d|14:00))$/;
function isValidIsoWithTimezone(value: string): boolean {
if (!ISO_WITH_TIMEZONE_PATTERN.test(value) || Number.isNaN(Date.parse(value))) return false;
const [year, month, day] = value.slice(0, 10).split('-').map(Number);
return day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
}
let _repo: Repository | null = null;
@@ -51,9 +59,29 @@ const LIST_CALENDAR_EVENTS_DEF: ToolDef = {
},
};
const CREATE_REMINDER_DEF: ToolDef = {
type: 'function',
function: {
name: 'CreateReminder',
description: '指定時刻または指定分後に、実行ユーザー向けの通知を登録する。過去時刻や曖昧な時刻は登録しない。',
parameters: { type: 'object', properties: {
body: { type: 'string', description: '通知本文(必須)' },
due_at: { type: 'string', description: 'UTC ISO 8601 時刻。delay_minutes と排他' },
delay_minutes: { type: 'number', description: '現在からの分数。due_at と排他、1以上' },
title: { type: 'string', description: '任意のタイトル' }, image_url: { type: 'string', description: '任意の HTTPS 画像 URL' },
}, required: ['body'] },
},
};
const LIST_REMINDERS_DEF: ToolDef = { type: 'function', function: { name: 'ListReminders', description: '実行ユーザーが未配信かつ取消していないリマインダーを一覧する。', parameters: { type: 'object', properties: {}, required: [] } } };
const CANCEL_REMINDER_DEF: ToolDef = { type: 'function', function: { name: 'CancelReminder', description: '実行ユーザーの未配信リマインダーを ID で取消する。', parameters: { type: 'object', properties: { id: { type: 'string', description: 'ListReminders で確認した ID' } }, required: ['id'] } } };
export const TOOL_DEFS: Record<string, ToolDef> = {
AddCalendarEvent: ADD_CALENDAR_EVENT_DEF,
ListCalendarEvents: LIST_CALENDAR_EVENTS_DEF,
CreateReminder: CREATE_REMINDER_DEF,
ListReminders: LIST_REMINDERS_DEF,
CancelReminder: CANCEL_REMINDER_DEF,
};
export async function executeTool(
@@ -63,9 +91,57 @@ export async function executeTool(
): Promise<ToolResult | null> {
if (name === 'AddCalendarEvent') return executeAddCalendarEvent(input, ctx);
if (name === 'ListCalendarEvents') return executeListCalendarEvents(input, ctx);
if (name === 'CreateReminder') return executeCreateReminder(input, ctx);
if (name === 'ListReminders') return executeListReminders(ctx);
if (name === 'CancelReminder') return executeCancelReminder(input, ctx);
return null;
}
function requireReminderOwner(ctx: ToolContext): string | null {
return typeof ctx.ownerId === 'string' && ctx.ownerId.length > 0 ? ctx.ownerId : null;
}
async function executeCreateReminder(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
if (!_repo) return { output: 'Reminder repo is not initialized', isError: true };
const ownerId = requireReminderOwner(ctx);
if (!ownerId) return { output: 'リマインダーにはユーザー所有者が必要です', isError: true };
const body = input.body;
if (typeof body !== 'string' || !body.trim() || body.length > MAX_REMINDER_BODY_LEN) return { output: `body は1〜${MAX_REMINDER_BODY_LEN}文字で指定してください`, isError: true };
const title = input.title;
if (title !== undefined && (typeof title !== 'string' || title.length > MAX_TITLE_LEN)) return { output: `title は最大${MAX_TITLE_LEN}文字です`, isError: true };
const imageUrl = input.image_url;
if (imageUrl !== undefined && (typeof imageUrl !== 'string' || !imageUrl.startsWith('https://'))) return { output: 'image_url は HTTPS URL で指定してください', isError: true };
const hasDueAt = input.due_at !== undefined;
const hasDelay = input.delay_minutes !== undefined;
if (hasDueAt === hasDelay) return { output: 'due_at または delay_minutes のどちらか一方を指定してください', isError: true };
let due: Date;
if (hasDueAt) {
if (typeof input.due_at !== 'string' || !isValidIsoWithTimezone(input.due_at)) return { output: 'due_at は実在する、タイムゾーン付き ISO 8601 形式で指定してください', isError: true };
due = new Date(input.due_at);
} else {
if (typeof input.delay_minutes !== 'number' || !Number.isFinite(input.delay_minutes) || input.delay_minutes < 1) return { output: 'delay_minutes は1以上の分数で指定してください', isError: true };
due = new Date(Date.now() + input.delay_minutes * 60_000);
}
if (due.getTime() <= Date.now()) return { output: '過去の時刻には登録できません。明示的な未来時刻を指定してください', isError: true };
const reminder = _repo.createAgentReminder({ ownerId, spaceId: ctx.spaceId ?? null, title: typeof title === 'string' ? title : null, body: body.trim(), imageUrl: typeof imageUrl === 'string' ? imageUrl : null, dueAt: due.toISOString() });
return { output: `リマインダーを登録しました: ${reminder.dueAt} (id=${reminder.id})`, isError: false };
}
async function executeListReminders(ctx: ToolContext): Promise<ToolResult> {
if (!_repo) return { output: 'Reminder repo is not initialized', isError: true };
const ownerId = requireReminderOwner(ctx);
if (!ownerId) return { output: 'リマインダーにはユーザー所有者が必要です', isError: true };
const reminders = _repo.listAgentReminders(ownerId);
return { output: reminders.length ? reminders.map(r => `- ${r.dueAt} 「${r.title ?? r.body}」(id=${r.id})`).join('\n') : '未配信のリマインダーはありません', isError: false };
}
async function executeCancelReminder(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
if (!_repo) return { output: 'Reminder repo is not initialized', isError: true };
const ownerId = requireReminderOwner(ctx);
if (!ownerId || typeof input.id !== 'string' || !input.id) return { output: 'id とユーザー所有者が必要です', isError: true };
return _repo.cancelAgentReminder(input.id, ownerId) ? { output: 'リマインダーを取消しました', isError: false } : { output: '未配信のリマインダーが見つかりません', isError: true };
}
async function executeAddCalendarEvent(
input: Record<string, unknown>,
ctx: ToolContext,