From 2d340b15e3f7e7a427fd3b413ee3be1e12b07611 Mon Sep 17 00:00:00 2001 From: DanliaQwerty20 Date: Sun, 13 Sep 2026 15:44:13 +0300 Subject: [PATCH] test: update calendar flow for agent contract 2.1 --- README.md | 6 +++++- SERVICE.md | 3 ++- scripts/check.ps1 | 5 ++++- scripts/run-calendar.ps1 | 13 ------------- tests/calendar-event.js | 41 ++++++++++++++++++++++++---------------- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index de37899..c6777f1 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,14 @@ pwsh ./scripts/run-load.ps1 локального стенда получи JWT тестового пользователя и выполни: ```powershell -$env:ACTION_TOKEN = "" +$env:ACTION_TOKEN = "" $env:CALENDAR_TEST_API_KEY = "<тот же локальный секрет, что у Calendar MCP>" pwsh ./scripts/run-calendar.ps1 ``` Скрипт принимает только локальные HTTP-адреса. Проверочный API `fake-calendar` доступен только в тестовом режиме и требует отдельный `X-Test-Key`; секрет не хранится в Git. + +Один JWT передаётся в Agent Runtime и Action Service. Оба сервиса независимо проверяют подпись, +issuer, срок и свой audience. Идентификаторы пользователя и tenant не передаются в JSON запроса: +сервисы получают их из проверенных claims `sub` и `tenant_id`. diff --git a/SERVICE.md b/SERVICE.md index a30e471..ca1b9f2 100644 --- a/SERVICE.md +++ b/SERVICE.md @@ -4,6 +4,7 @@ |---|---| | Ответственность | acceptance-, системные, load и resilience-тесты | | Бизнес-код | отсутствует | -| Основной инструмент | k6 | +| Основной инструмент | k6; contract-first сценарии | | Chaos | отдельный ручной запуск, выключен по умолчанию | +| Первый acceptance-путь | Agent Runtime 2.1 → Action Service → Temporal → Calendar MCP | diff --git a/scripts/check.ps1 b/scripts/check.ps1 index c65fbd4..f8a496f 100644 --- a/scripts/check.ps1 +++ b/scripts/check.ps1 @@ -10,8 +10,11 @@ foreach ($required in @("TARGET_URL", "thresholds", "http_req_failed", "http_req if ($script -notmatch $required) { throw "В k6-тесте нет $required." } } $calendarScript = Get-Content tests/calendar-event.js -Raw -foreach ($required in @("AWAITING_APPROVAL", "SUCCEEDED", "payloadHash", "requestKey", "result?.eventId", "http_req_failed", "CALENDAR_TEST_API_KEY", "X-Test-Key")) { +foreach ($required in @("AWAITING_APPROVAL", "SUCCEEDED", "payloadHash", "requestKey", "result?.eventId", "http_req_failed", "CALENDAR_TEST_API_KEY", "X-Test-Key", "availableConnectors", "requiresApproval", "Authorization")) { if ($calendarScript -notmatch [regex]::Escape($required)) { throw "В calendar acceptance-тесте нет $required." } } +foreach ($oldName in @("utterance", "tenant_id", "actor_id", "available_connectors", "requires_approval")) { + if ($calendarScript -match [regex]::Escape($oldName)) { throw "В calendar acceptance-тесте осталось старое поле $oldName." } +} Write-Host "Быстрые проверки test-lab прошли." diff --git a/scripts/run-calendar.ps1 b/scripts/run-calendar.ps1 index 9c8f32a..00f6d05 100644 --- a/scripts/run-calendar.ps1 +++ b/scripts/run-calendar.ps1 @@ -16,17 +16,6 @@ if (-not $env:ACTION_TOKEN) { throw "Укажи ACTION_TOKEN с JWT тестов if (-not $env:CALENDAR_TEST_API_KEY) { throw "Укажи CALENDAR_TEST_API_KEY локального Calendar MCP." } $parts = $env:ACTION_TOKEN.Split('.') if ($parts.Count -ne 3) { throw "ACTION_TOKEN не похож на JWT." } -$payload = $parts[1].Replace('-', '+').Replace('_', '/') -while ($payload.Length % 4) { $payload += '=' } -try { - $claims = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json -} -catch { - throw "Не удалось прочитать claims из ACTION_TOKEN." -} -if (-not $claims.sub -or -not $claims.tenant_id) { - throw "В ACTION_TOKEN нужны claims sub и tenant_id." -} & docker run --rm ` --volume "${PWD}/tests:/tests:ro" ` @@ -35,8 +24,6 @@ if (-not $claims.sub -or -not $claims.tenant_id) { --env "CALENDAR_TEST_URL=$CalendarTestUrl" ` --env "CALENDAR_TEST_API_KEY=$env:CALENDAR_TEST_API_KEY" ` --env "ACTION_TOKEN=$env:ACTION_TOKEN" ` - --env "TEST_TENANT_ID=$($claims.tenant_id)" ` - --env "TEST_ACTOR_ID=$($claims.sub)" ` "grafana/k6:2.2.0" run /tests/calendar-event.js if ($LASTEXITCODE -ne 0) { throw "Сценарий создания встречи не прошёл." } diff --git a/tests/calendar-event.js b/tests/calendar-event.js index 38f0ed2..714e136 100644 --- a/tests/calendar-event.js +++ b/tests/calendar-event.js @@ -5,9 +5,7 @@ const agentUrl = requiredUrl('AGENT_URL'); const actionUrl = requiredUrl('ACTION_URL'); const calendarTestUrl = requiredUrl('CALENDAR_TEST_URL'); const calendarTestKey = required('CALENDAR_TEST_API_KEY'); -const actionToken = required('ACTION_TOKEN'); -const tenantId = required('TEST_TENANT_ID'); -const actorId = required('TEST_ACTOR_ID'); +const userToken = required('ACTION_TOKEN'); const eventData = { title: 'Обсуждение проекта', startAt: '2026-09-08T12:00:00+03:00', @@ -52,8 +50,10 @@ export default function () { check(events, { 'calendar has one event': (items) => items.length === 1, 'calendar keeps the exact title': (items) => items[0]?.title === eventData.title, - 'calendar keeps the exact start': (items) => items[0]?.startAt === eventData.startAt, - 'calendar keeps the exact end': (items) => items[0]?.endAt === eventData.endAt, + 'calendar keeps the start instant and offset': (items) => + sameDate(items[0]?.startAt, eventData.startAt), + 'calendar keeps the end instant and offset': (items) => + sameDate(items[0]?.endAt, eventData.endAt), 'calendar keeps the exact time zone': (items) => items[0]?.timeZone === eventData.timeZone, 'calendar event id matches action result': (items) => items[0]?.eventId === done.result?.eventId, }); @@ -71,21 +71,22 @@ function createProposal() { const response = http.post( `${agentUrl}/api/v1/proposals`, JSON.stringify({ - utterance: `Создай встречу "${eventData.title}" с ${eventData.startAt} до ${eventData.endAt}`, + text: `Создай встречу "${eventData.title}" с ${eventData.startAt} до ${eventData.endAt}`, context: { - tenant_id: tenantId, - actor_id: actorId, - timezone: eventData.timeZone, - available_connectors: ['fake-calendar'], + timeZone: eventData.timeZone, + availableConnectors: ['fake-calendar'], }, }), - jsonHeaders(), + authHeaders(), ); expectStatus(response, 200, 'agent-runtime did not create a proposal'); const body = response.json(); if (!body.proposal || body.clarification) { fail('agent-runtime returned no complete proposal'); } + if (!body.proposal.requiresApproval) { + fail('agent-runtime proposal does not require approval'); + } check(body.proposal.payload, { 'proposal keeps the exact event data': (payload) => payload.title === eventData.title && @@ -142,19 +143,27 @@ function findEvents(requestKey) { return response.json().events; } -function jsonHeaders() { - return { headers: { 'Content-Type': 'application/json' } }; -} - function authHeaders() { return { headers: { - Authorization: `Bearer ${actionToken}`, + Authorization: `Bearer ${userToken}`, 'Content-Type': 'application/json', }, }; } +function sameDate(actual, expected) { + if (!actual || Date.parse(actual) !== Date.parse(expected)) { + return false; + } + return timeOffset(actual) === timeOffset(expected); +} + +function timeOffset(value) { + const match = value.match(/(Z|[+-]\d{2}:\d{2})$/); + return match ? match[1] : null; +} + function expectStatus(response, expected, message) { if (response.status !== expected) { fail(`${message}: expected ${expected}, got ${response.status}`);