Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ pwsh ./scripts/run-load.ps1
локального стенда получи JWT тестового пользователя и выполни:

```powershell
$env:ACTION_TOKEN = "<local-test-token>"
$env:ACTION_TOKEN = "<local-test-token с audience agent-runtime и action-service>"
$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`.
3 changes: 2 additions & 1 deletion SERVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
|---|---|
| Ответственность | acceptance-, системные, load и resilience-тесты |
| Бизнес-код | отсутствует |
| Основной инструмент | k6 |
| Основной инструмент | k6; contract-first сценарии |
| Chaos | отдельный ручной запуск, выключен по умолчанию |
| Первый acceptance-путь | Agent Runtime 2.1 → Action Service → Temporal → Calendar MCP |

5 changes: 4 additions & 1 deletion scripts/check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 прошли."

13 changes: 0 additions & 13 deletions scripts/run-calendar.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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" `
Expand All @@ -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 "Сценарий создания встречи не прошёл." }
41 changes: 25 additions & 16 deletions tests/calendar-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
});
Expand All @@ -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 &&
Expand Down Expand Up @@ -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}`);
Expand Down