План: вложения из Jira по запросу, агент v14 (#43)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7778b6dbbc
commit
ceadd9c071
92
docs/superpowers/plans/2026-10-22-attachment-fetch-agent.md
Normal file
92
docs/superpowers/plans/2026-10-22-attachment-fetch-agent.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Вложения из Jira по запросу (агент v14) — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** агент v14 в `refresh` присылает список вложений задач (без содержимого) и выполняет новую команду
|
||||
`fetch_attachment`: скачивает файл вложения из Jira и передаёт хабу `POST /agent/files`.
|
||||
|
||||
**Architecture:** список — поле `attachment` в `search` и `Executor.attachments_of(raw)` в `to_state`; скачивание —
|
||||
`JiraClient.download` (байты, предел, без перенаправлений на другой хост) и `Executor.do_fetch_attachment`; отправка —
|
||||
`HubClient.put_file` (работает и через `FolderTransport`). Python 3.9, только stdlib.
|
||||
|
||||
**Tech Stack:** Python 3.9 (`/usr/bin/python3`), stdlib, `unittest`, фейки `tests/fake_jira.py`, `tests/fake_hub.py`.
|
||||
|
||||
**Spec:** `/Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/attach-fetch/openspec/changes/attachment-fetch/` —
|
||||
`design.md` (решения 1, 4–6, 8, 17, 18), `specs/agent/spec.md` («Чтение состояния», «Скачивание вложения»),
|
||||
`specs/commands/spec.md` («Скачивание вложения по запросу»), `specs/hub-api/spec.md` (`POST /agent/files`). Решения агента —
|
||||
`openspec/changes/agent-v14-attachment-fetch/design.md` этого репозитория (таблица `R-n`).
|
||||
|
||||
**Worktree:** `/Users/andrey.epifancev/projects/jira-bridge/jira-agent/.wt/attach-fetch`, ветка `feat/43-attachment-fetch`
|
||||
(aep/jira-agent#43). Все команды — из корня worktree.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- `AGENT_VERSION = 14`.
|
||||
- Python 3.9 stdlib; без `match`, без `X | Y` в аннотациях (`tests/test_py39.py`).
|
||||
- Предел — `MAX_ATTACHMENT = 10 * 1024 * 1024` (10 485 760 байт).
|
||||
- Адрес содержимого — `<base_url>/secure/attachment/<id>/<urllib.parse.quote(filename, safe="")>`; `content` из ответа Jira не используется.
|
||||
- Таймаут скачивания — `max(self.timeout, 120)`.
|
||||
- Элемент `attachments`: `{id, filename, size, mime_type, author, created}` (`author` — `displayName`, иначе `name`, иначе `None`).
|
||||
- Тексты ошибок (точно): «вложение #<id> удалено из <key> в Jira»; «файл больше 10 МБ (<размер>) — скачайте в Jira» (неизвестный размер — «больше 10,0 МБ»); «Jira перенаправила скачивание на <хост>»; «файл пришёл не целиком (<получено> из <размер>)». Размер — «N Б» / «N,N КБ» / «N,N МБ» по 1024, запятая.
|
||||
- Содержимое файла не попадает в лог, журнал и текст ошибок.
|
||||
- Каждый коммит — с `(#43)` в заголовке и последней строкой `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
- Зелёный — весь набор: `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .`
|
||||
- Решения вне спеки и плана — в таблицу `openspec/changes/agent-v14-attachment-fetch/design.md`; находки ревью задачи n — `T<n>.<k>`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Перенаправление 302 на тот же хост (Jira иногда так отдаёт файл) — проходит; на другой хост или схему — `failed` без запроса туда (тесты в Task 2).
|
||||
2. Вложение без `size` в Jira и содержимое больше предела — чтение обрывается, хабу ничего не уходит (тест в Task 3).
|
||||
3. Пустое вложение (0 байт) — `done`, хаб получил пустой `content` (тест в Task 3).
|
||||
4. Хаб недоступен на `POST /agent/files` — `failed` с `retryable`, как прочие обрывы связи с хабом (тест в Task 3).
|
||||
5. Кириллица и пробелы в имени — путь скачивания в URL-кодировке, фейк Jira находит файл (тест в Task 3).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Список вложений в состоянии и версия 14
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` — `AGENT_VERSION = 14`, `STATE_FIELDS` + `"attachment"`, `Executor.attachment_state(a) -> dict` (статический), `Executor.attachments_of(raw) -> List[dict]`, `to_state` добавляет `attachments`
|
||||
- Test: `tests/test_refresh.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `attachments_of(raw)` — вложения в порядке Jira, без `id` — отброшено с `log` (ключ задачи); нет поля `attachment` — `[]`.
|
||||
|
||||
- [ ] **Step 1: Тесты** — `test_refresh_attachments_in_state` (сценарий «Вложения в состоянии» из дельты `agent`: два вложения, `author = "Потапова Анна"`, `size = 240000`, `mime_type = "image/png"`; число вызовов `search` прежнее; к содержимому обращений нет — фейк считает GET `/secure/attachment/`), `test_refresh_attachment_without_id_dropped`, `test_refresh_no_attachments_empty_list`.
|
||||
- [ ] **Step 2: Прогон** — `/usr/bin/python3 -m unittest tests.test_refresh -q` → FAIL.
|
||||
- [ ] **Step 3: Реализация** по сигнатурам.
|
||||
- [ ] **Step 4: Прогон** — весь набор → PASS.
|
||||
- [ ] **Step 5: Commit** — `Агент v14: список вложений в состоянии refresh (#43)`.
|
||||
|
||||
### Task 2: Скачивание байтов из Jira и файл хабу
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` — `Http.fetch_bytes(path, limit, timeout) -> bytes` (GET, без разбора JSON; перенаправления — свой `HTTPRedirectHandler`: тот же схема+хост+порт, что у `base`, — идти; иначе `RedirectAway(host)`); `JiraClient.download(path, limit) -> bytes` (через `_retry`, счётчик вызовов); класс `RedirectAway(Exception)` с полем `host`; `TooBig(Exception)`; `HubClient.put_file(data: bytes) -> dict` (`POST /agent/files {content: base64}`)
|
||||
- Modify: `tests/fake_jira.py` — `GET /secure/attachment/<id>/<имя>` отдаёт `data` вложения (имя сверяется после раскодирования), счётчик `self.content_gets`; `self.redirect_content = None` (адрес для 302); `self.hide_size = set()` (id без `size` в JSON)
|
||||
- Modify: `tests/fake_hub.py` — `POST /agent/files` кладёт в `self.files`, отвечает `{file, size}`; `self.reject_files = None` (код ответа или `"drop"`)
|
||||
- Test: `tests/test_clients.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `JiraClient.download(path: str, limit: int) -> bytes` — читает не больше `limit + 1` байт; если прочитано больше `limit` — `TooBig`; 429/503 — повтор как у `call`; `RedirectAway` и `TooBig` наружу.
|
||||
|
||||
- [ ] **Step 1: Тесты** — `test_download_bytes` (тело как есть, `calls` +1, заголовок `Authorization` ушёл); `test_download_redirect_same_host` (302 на тот же хост → байты); `test_download_redirect_other_host` (302 на `http://127.0.0.2:<порт>/x` → `RedirectAway`, второй сервер запроса не получил); `test_download_limit` (limit=10, тело 20 байт → `TooBig`); `test_download_429_retry`; `test_put_file_http` и `test_put_file_folder` (через `FolderTransport` и `tests/fake_relay.py`).
|
||||
- [ ] **Step 2: Прогон** — `/usr/bin/python3 -m unittest tests.test_clients -q` → FAIL.
|
||||
- [ ] **Step 3: Реализация.** Обработчик перенаправлений — `urllib.request.build_opener(_SameHostRedirect(base), HTTPSHandler(context=ctx))`; `urlopen` в `Http.send` не трогать.
|
||||
- [ ] **Step 4: Прогон** — весь набор → PASS.
|
||||
- [ ] **Step 5: Commit** — `Агент: скачивание байтов из Jira без чужих перенаправлений, файл хабу (#43)`.
|
||||
|
||||
### Task 3: Команда `fetch_attachment`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent.py` — `Executor.do_fetch_attachment(p, cmd) -> dict`, `_fmt_size(n: Optional[int]) -> str`; ARCHITECTURE.md (раздел «Исполнитель», `JiraClient.download`, `HubClient.put_file`), `CLAUDE.md` (граница `Executor` знает хаб и в `fetch_attachment`; последний change)
|
||||
- Test: `tests/test_executor_attachments.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `JiraClient.download`, `RedirectAway`, `TooBig`, `HubClient.put_file` (Task 2); `self._hub(fn, *args)` для вызова хаба.
|
||||
- Produces: результат `done` — `{"attachment_id", "file", "size"}`; `file` и `size` — из ответа хаба.
|
||||
|
||||
- [ ] **Step 1: Тесты** — `test_fetch_screenshot` (сценарий «Скачивание скриншота»: имя «скрин 1.png», фейк Jira отдал файл, хаб получил те же байты, результат `done` с sha256 и размером, в журнале агента нет содержимого); `test_fetch_deleted` («вложение #31337 удалено из AURORA-1650 в Jira», содержимое не запрашивалось); `test_fetch_too_large_known` (size 25 000 000 в JSON → «файл больше 10 МБ (23,8 МБ) — скачайте в Jira», `content_gets == 0`); `test_fetch_too_large_unknown` (`hide_size`, содержимое 12 МБ → «файл больше 10 МБ (больше 10,0 МБ) — скачайте в Jira», хаб ничего не получил); `test_fetch_redirect_away` («Jira перенаправила скачивание на 127.0.0.2»); `test_fetch_truncated` (фейк отдаёт меньше байт, чем `size` → «файл пришёл не целиком (N из M)»); `test_fetch_empty` (0 байт → `done`, `size = 0`); `test_fetch_hub_down` (`reject_files = "drop"` → `failed`, `retryable`, `peer = "хабом"`); `test_fetch_hub_422` (ошибка с «хаб: »).
|
||||
- [ ] **Step 2: Прогон** — `/usr/bin/python3 -m unittest tests.test_executor_attachments -q` → FAIL.
|
||||
- [ ] **Step 3: Реализация** по контракту `commands` «Скачивание вложения по запросу» (шаги 1–5); лог — `log(" вложение #%s «%s» (%s) ← %s" % (...))`.
|
||||
- [ ] **Step 4: Прогон** — весь набор с `-W error::ResourceWarning` → PASS.
|
||||
- [ ] **Step 5: Commit** — `Агент v14: команда fetch_attachment (#43)`.
|
||||
Loading…
x
Reference in New Issue
Block a user