feat: Logcat Console export tools implementation
- Update logcat console milestone documentation - Implement export tools archive for m-logcat-console task - Update console page features - Update console page and widget tests
This commit is contained in:
parent
8c0bb0a6b0
commit
b9f3adaadf
7 changed files with 792 additions and 23 deletions
|
|
@ -35,18 +35,18 @@
|
|||
|
||||
설치 후 바로 디버깅할 수 있는 기본 로그 화면을 제공한다.
|
||||
|
||||
- [ ] [stream] 선택 device의 `adb logcat` stream을 시작하고 화면에 append한다.
|
||||
- [ ] [lifecycle] 화면 이탈 또는 device 변경 시 logcat process를 정리한다. 검증: stream subscription/process cleanup unit 또는 integration test 후보를 마련한다.
|
||||
- [x] [stream] 선택 device의 `adb logcat` stream을 시작하고 화면에 append한다.
|
||||
- [x] [lifecycle] 화면 이탈 또는 device 변경 시 logcat process를 정리한다. 검증: stream subscription/process cleanup unit 또는 integration test 후보를 마련한다.
|
||||
- [x] [level-color] info/debug/warn/error를 구분해 읽기 쉽게 표시한다.
|
||||
- [ ] [auto-scroll] 새 로그가 들어올 때 자동 스크롤하되 pause 상태에서는 사용자의 위치를 유지한다.
|
||||
- [x] [auto-scroll] 새 로그가 들어올 때 자동 스크롤하되 pause 상태에서는 사용자의 위치를 유지한다.
|
||||
|
||||
### Epic: [filter] 필터와 작업 도구
|
||||
|
||||
반복 디버깅 중 필요한 로그만 빠르게 볼 수 있게 한다.
|
||||
|
||||
- [ ] [query-filter] package/tag/text query 필터를 제공한다.
|
||||
- [ ] [level-filter] ALL/DEBUG/ERROR 같은 level filter를 제공한다.
|
||||
- [ ] [clear-pause] clear와 pause/resume 버튼을 제공한다.
|
||||
- [x] [query-filter] package/tag/text query 필터를 제공한다.
|
||||
- [x] [level-filter] ALL/DEBUG/ERROR 같은 level filter를 제공한다.
|
||||
- [x] [clear-pause] clear와 pause/resume 버튼을 제공한다.
|
||||
- [ ] [export] 현재 로그를 파일 저장 또는 clipboard 복사할 수 있는 방향을 정리한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
|
@ -73,6 +73,8 @@
|
|||
- 후속 작업: 설치 세션 이력과 오류 리포트
|
||||
- 작업 현황:
|
||||
- `01_logcat_process`: PASS 완료. archive: `agent-task/archive/2026/06/m-logcat-console/01_logcat_process/complete.log`. `Roadmap Completion`이 없어 기능 Task 체크는 no-op.
|
||||
- `02+01_console_stream`: 활성 작업. Roadmap Targets: `stream`, `lifecycle`. 선행 `01_logcat_process` 완료 조건 충족.
|
||||
- `03+02_auto_scroll`: 활성 작업. Roadmap Targets: `auto-scroll`. 선행 `02+01_console_stream` 완료 대기.
|
||||
- `02+01_console_stream`: PASS 완료. archive: `agent-task/archive/2026/06/m-logcat-console/02+01_console_stream/complete.log`. Roadmap Completion: `stream`, `lifecycle`.
|
||||
- `03+02_auto_scroll`: PASS 완료. archive: `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`. Roadmap Completion: `auto-scroll`.
|
||||
- `filter-small`: 직접 완료. query/level/clear-pause 연결 및 widget test 추가. Evidence: remote runner `flutter analyze`, `flutter test`.
|
||||
- `04+03_export_tools`: 활성 plan. Roadmap Targets: `export`. 선행 `03+02_auto_scroll` 완료 조건 충족.
|
||||
- 확인 필요: 없음
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
<!-- task=m-logcat-console/04+03_export_tools plan=0 tag=EXPORT -->
|
||||
|
||||
# Code Review Reference - EXPORT
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-14
|
||||
task=m-logcat-console/04+03_export_tools, plan=0, tag=EXPORT
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
|
||||
- Task ids:
|
||||
- `export`: 현재 로그를 파일 저장 또는 clipboard 복사할 수 있는 방향을 정리한다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G04.md` -> `code_review_local_G04_0.log`, `PLAN-local-G04.md` -> `plan_local_G04_0.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-logcat-console/04+03_export_tools/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-logcat-console`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [EXPORT-1] Clipboard Export Action | [x] |
|
||||
| [EXPORT-2] Export Widget Tests | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `ConsolePage`에 실제 live log 표시 라인 clipboard export action과 복사 버튼을 추가한다.
|
||||
- [x] sample placeholder만 있는 상태나 표시 라인이 없는 상태에서는 export가 비활성/무동작이 되게 한다.
|
||||
- [x] `test/console_page_test.dart`에 clipboard export와 empty/no-live 상태 회귀 테스트를 추가한다.
|
||||
- [x] 원격 Mac runner에서 포맷, 공백, analyze, test 검증을 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-local-G04.md`를 `code_review_local_G04_0.log`로 아카이브한다.
|
||||
- [x] active `PLAN-local-G04.md`를 `plan_local_G04_0.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/m-logcat-console/04+03_export_tools/`를 `agent-task/archive/YYYY/MM/m-logcat-console/04+03_export_tools/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-logcat-console`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-logcat-console/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G04.md`와 `CODE_REVIEW-local-G04.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 복사 버튼 enable 조건을 `_hasLiveLogs` getter로 분리했다. plan은 "live display lines가 없으면 disabled"만 명시했는데, `widget.device != null && widget.logcatStarter != null && _displayLines.isNotEmpty` 조건이 build와 `_copyVisibleLogsToClipboard` guard에서 중복되어 가독성과 단일 진실원을 위해 getter로 추출했다. 동작은 plan과 동일하다.
|
||||
- 검증 명령은 plan의 고정 계약 그대로 실행했고 대체하지 않았다. 단, plan의 `최종 검증`은 format/diff-check/analyze/test를 개별 SSH 호출로 나열하지만, 같은 remote runner에서 동일 명령을 `&&`로 묶어 한 세션에서 실행했다(결과는 동일). 추가로 새 export 테스트만 떼어 `flutter test test/console_page_test.dart`를 한 번 더 실행해 두 신규 테스트 PASS를 명시 확인했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- export 대상은 `_logLines` 원본이 아니라 `_displayLines`(level + query filter 적용 결과)로 정했다. 사용자가 화면에서 보고 있는 라인을 그대로 복사하는 것이 가장 직관적이고, checkpoint의 "현재 visible live log lines만 복사" 계약과 일치한다.
|
||||
- no-device sample placeholder 상태에서는 `_hasLiveLogs`가 false이므로 버튼 `onPressed`가 null이 되고, 추가로 `_copyVisibleLogsToClipboard` 진입부에서 `widget.device == null || widget.logcatStarter == null`과 `lines.isEmpty`를 이중으로 guard해 sample 라인이 절대 복사되지 않게 했다.
|
||||
- 새 dependency를 추가하지 않고 Flutter SDK의 `package:flutter/services.dart` `Clipboard`만 사용했다. file save dialog, export 경로/권한 등은 plan 범위 밖이라 구현하지 않았다.
|
||||
- 복사 성공 시 `SnackBar`로 복사 줄 수를 짧게 알려 사용자 피드백을 제공한다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `Clipboard.setData`에는 현재 visible live log lines만 들어가야 한다.
|
||||
- no-device sample placeholder와 empty live log 상태에서 export가 실제 복사를 시도하지 않아야 한다.
|
||||
- file save 구현이나 새 dependency가 plan 범위 밖으로 섞이지 않아야 한다.
|
||||
- `Roadmap Targets`의 `export` Task가 PASS 완료 이벤트로 체크될 수 있게 유지되어야 한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
검증은 모두 remote Mac runner(`toki@toki-labs.com`, checkout `$HOME/docker/services/code-server/data/volume/workspace/appsok`)에서 실행했다. 이 checkout은 현재 working tree와 동일한 변경(export 편집 포함)을 공유한다.
|
||||
|
||||
### EXPORT-1 중간 검증
|
||||
```bash
|
||||
$ dart format lib/src/features/console/console_page.dart test/console_page_test.dart
|
||||
Formatted lib/src/features/console/console_page.dart
|
||||
Formatted 2 files (1 changed) in 0.02 seconds.
|
||||
```
|
||||
|
||||
### EXPORT-2 중간 검증
|
||||
```bash
|
||||
$ flutter test test/console_page_test.dart
|
||||
00:01 +7: clear button removes captured live log lines
|
||||
00:01 +8: copies currently visible live log lines to clipboard
|
||||
00:01 +9: keeps export disabled without live log lines
|
||||
00:01 +10: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ ssh ... 'cd <appsok> && dart format <files> && dart format --output=none --set-exit-if-changed <files>'
|
||||
Formatted 2 files (0 changed) in 0.02 seconds.
|
||||
FORMAT_CLEAN
|
||||
|
||||
$ ssh ... 'cd <appsok> && git diff --check -- <files>'
|
||||
DIFFCHECK_CLEAN
|
||||
(no output, exit 0)
|
||||
|
||||
$ ssh ... 'cd <appsok> && flutter analyze && flutter test'
|
||||
Analyzing appsok...
|
||||
No issues found! (ran in 1.5s)
|
||||
...
|
||||
00:06 +213: All tests passed!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
Sections and their ownership:
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` -> `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` -> `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed; do not ask the user directly during implementation; when filled, include exact decision, evidence, commands/output, why automatic follow-up cannot resolve it, and resume condition |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS이므로 `complete.log`를 작성하고 task directory를 archive로 이동한다.
|
||||
|
||||
### 리뷰 근거
|
||||
|
||||
- `ConsolePage`는 실제 live mode에서 현재 `_displayLines`만 `Clipboard.setData`로 복사하며, sample/no-live/filtered-empty 상태는 버튼 비활성 및 method guard로 복사를 막는다.
|
||||
- `test/console_page_test.dart`는 filtered visible line clipboard payload와 sample/no-live disabled case를 포함한다.
|
||||
- 원격 Mac runner에서 `dart format --output=none --set-exit-if-changed`, `git diff --check`, `flutter analyze`, `flutter test`, `flutter test test/console_page_test.dart`를 재실행해 통과를 확인했다.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Complete - m-logcat-console/04+03_export_tools
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-14
|
||||
|
||||
## 요약
|
||||
|
||||
Clipboard export 구현 리뷰 1회차에서 PASS 판정으로 종료했다.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | PASS | 현재 표시 중인 live log 라인 clipboard 복사와 sample/no-live 비활성 동작이 구현 및 검증되었다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `ConsolePage`에 현재 visible live log lines를 clipboard로 복사하는 `로그 복사` 버튼과 SnackBar 피드백을 추가했다.
|
||||
- sample placeholder, live mode empty state, filtered-empty state에서는 export가 비활성/무동작이 되도록 guard를 추가했다.
|
||||
- `test/console_page_test.dart`에 clipboard payload와 no-live disabled 회귀 테스트를 추가했고, console marker 변경에 맞춰 `test/widget_test.dart` 기대 텍스트를 갱신했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com "zsh -lc 'cd \"$HOME/docker/services/code-server/data/volume/workspace/appsok\" && dart format --output=none --set-exit-if-changed lib/src/features/console/console_page.dart test/console_page_test.dart && git diff --check -- lib/src/features/console/console_page.dart test/console_page_test.dart test/widget_test.dart'"` - PASS; `Formatted 2 files (0 changed) in 0.02 seconds.`
|
||||
- `ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com "zsh -lc 'cd \"$HOME/docker/services/code-server/data/volume/workspace/appsok\" && flutter analyze'"` - PASS; `No issues found!`
|
||||
- `ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com "zsh -lc 'cd \"$HOME/docker/services/code-server/data/volume/workspace/appsok\" && flutter test'"` - PASS; `00:06 +213: All tests passed!`
|
||||
- `ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com "zsh -lc 'cd \"$HOME/docker/services/code-server/data/volume/workspace/appsok\" && flutter test test/console_page_test.dart'"` - PASS; `00:01 +10: All tests passed!`
|
||||
|
||||
## Roadmap Completion
|
||||
|
||||
- Milestone: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
|
||||
- Completed task ids:
|
||||
- `export`: PASS; evidence=`agent-task/archive/2026/06/m-logcat-console/04+03_export_tools/plan_local_G04_0.log`, `agent-task/archive/2026/06/m-logcat-console/04+03_export_tools/code_review_local_G04_0.log`; verification=`flutter analyze`, `flutter test`, `flutter test test/console_page_test.dart` on remote Mac runner
|
||||
- Not completed task ids: 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
<!-- task=m-logcat-console/04+03_export_tools plan=0 tag=EXPORT -->
|
||||
|
||||
# Plan - EXPORT
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현이다. 코드를 수정한 뒤 검증 명령을 실행하고 실제 출력과 구현 메모를 review stub에 남긴다. 최종 판정, log rename, `complete.log`, task directory archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
`filter` Epic에서 query, level, clear/pause는 직접 구현되어 남은 Task는 `export`다. MVP 범위에서는 macOS 파일 저장 경로와 권한까지 결정하지 않고, 현재 표시 중인 실제 logcat 라인을 clipboard로 복사하는 방향이 가장 작고 검증 가능하다. 이 계획은 `export` Task를 clipboard export로 완료시키는 구현 단위다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 blocker는 active `CODE_REVIEW-local-G04.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 정당성을 확인한 뒤에만 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
|
||||
- Task ids:
|
||||
- `export`: 현재 로그를 파일 저장 또는 clipboard 복사할 수 있는 방향을 정리한다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/usable-mvp/PHASE.md`
|
||||
- `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
|
||||
- `agent-ops/rules/project/domain/device-console/rules.md`
|
||||
- `agent-ops/rules/project/domain/app-shell/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/device-console-smoke.md`
|
||||
- `lib/src/features/console/console_page.dart`
|
||||
- `lib/src/services/adb_service.dart`
|
||||
- `test/console_page_test.dart`
|
||||
- `test/widget_test.dart`
|
||||
- `pubspec.yaml`
|
||||
- `.gitignore`
|
||||
- `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- `agent-test/local/rules.md` present/read. 기본 완료 evidence는 standard remote Mac runner 기준이다.
|
||||
- matched profile: `agent-test/local/device-console-smoke.md` present/read.
|
||||
- 적용 명령: remote runner checkout에서 `flutter analyze`, `flutter test`.
|
||||
- 보조 고정 검증: `dart format --output=none --set-exit-if-changed lib/src/features/console/console_page.dart test/console_page_test.dart`, `git diff --check -- lib/src/features/console/console_page.dart test/console_page_test.dart`.
|
||||
- `<확인 필요>` 값 없음. 신규 패키지는 추가하지 않는다. Clipboard는 Flutter SDK `package:flutter/services.dart`를 사용한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- 현재 표시 로그를 clipboard로 복사하는 동작: 기존 테스트 없음. `test/console_page_test.dart`에 regression widget test를 추가한다.
|
||||
- 로그가 없거나 sample placeholder만 있는 상태에서 export가 비활성/무동작이어야 하는 동작: 기존 테스트 없음. 같은 테스트 파일에 no live log disabled case를 추가한다.
|
||||
- 파일 저장: 이번 계획 범위 제외이므로 테스트하지 않는다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 plan 파일 선택 전에 평가했다.
|
||||
- 공유 task group: `agent-task/m-logcat-console`.
|
||||
- sibling subtask: `04+03_export_tools`; predecessor `03`은 `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`로 충족된다.
|
||||
- 이 subtask 안에서는 clipboard action, UI button, widget tests가 하나의 `ConsolePage` ownership boundary 안에 있어 추가 분할하지 않는다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 포함: `ConsolePage`에 실제 logcat 표시 라인을 clipboard로 복사하는 export action과 버튼을 추가한다.
|
||||
- 포함: export widget tests를 `test/console_page_test.dart`에 추가한다.
|
||||
- 제외: macOS file save dialog, default export directory, persisted export history, log redaction policy. 이들은 platform/product 선택이 커져 별도 작업이 맞다.
|
||||
- 제외: `AdbService` process contract. export는 이미 화면에 보관된 lines만 사용한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build: `local-G04`, review: `local-G04`. 단일 UI state/action과 widget test 중심이며, 원격 `flutter analyze`/`flutter test`로 검증 가능하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `ConsolePage`에 실제 live log 표시 라인 clipboard export action과 복사 버튼을 추가한다.
|
||||
- [ ] sample placeholder만 있는 상태나 표시 라인이 없는 상태에서는 export가 비활성/무동작이 되게 한다.
|
||||
- [ ] `test/console_page_test.dart`에 clipboard export와 empty/no-live 상태 회귀 테스트를 추가한다.
|
||||
- [ ] 원격 Mac runner에서 포맷, 공백, analyze, test 검증을 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
`04+03_export_tools`는 sibling index `03` 완료 뒤에만 구현한다. `03`은 `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`에서 PASS와 `Roadmap Completion: auto-scroll`이 확인되었다.
|
||||
|
||||
### [EXPORT-1] Clipboard Export Action
|
||||
|
||||
#### 문제
|
||||
|
||||
현재 console toolbar는 pause와 clear만 제공한다. `export` Task가 요구하는 clipboard 또는 file save 방향이 UI에 없다.
|
||||
|
||||
```dart
|
||||
// lib/src/features/console/console_page.dart:260
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
tooltip: _isPaused ? '재개' : '일시정지',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPaused = !_isPaused;
|
||||
if (!_isPaused) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _scrollToBottom(),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
tooltip: '지우기',
|
||||
onPressed: () {
|
||||
setState(_logLines.clear);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`package:flutter/services.dart`를 import하고 `_copyVisibleLogsToClipboard`를 추가한다. 실제 device+starter가 있는 live mode에서 `_displayLines`가 비어 있지 않을 때만 복사 버튼을 활성화한다. 복사 텍스트는 현재 표시 라인을 `\n`으로 join한다.
|
||||
|
||||
```dart
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> _copyVisibleLogsToClipboard() async {
|
||||
if (widget.device == null || widget.logcatStarter == null) return;
|
||||
final lines = _displayLines;
|
||||
if (lines.isEmpty) return;
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: lines.join('\n')));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('로그 ${lines.length}줄 복사됨')),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
버튼은 clear 버튼 옆에 `Icons.content_copy`로 둔다. tooltip은 `로그 복사`를 사용한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `lib/src/features/console/console_page.dart`: `Clipboard` import 추가.
|
||||
- [ ] `lib/src/features/console/console_page.dart`: `_copyVisibleLogsToClipboard` 추가.
|
||||
- [ ] `lib/src/features/console/console_page.dart`: live display lines가 없으면 export button disabled.
|
||||
- [ ] `lib/src/features/console/console_page.dart`: 복사 성공 후 `SnackBar`로 짧은 결과 표시.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`EXPORT-2`에서 widget tests를 추가한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
dart format lib/src/features/console/console_page.dart test/console_page_test.dart
|
||||
```
|
||||
|
||||
Expected: files formatted without error.
|
||||
|
||||
### [EXPORT-2] Export Widget Tests
|
||||
|
||||
#### 문제
|
||||
|
||||
`console_page_test.dart`는 filter와 clear/pause를 검증하지만 clipboard export는 아직 검증하지 않는다.
|
||||
|
||||
```dart
|
||||
// test/console_page_test.dart:369
|
||||
testWidgets('clear button removes captured live log lines', (tester) async {
|
||||
final fakeProcess = _FakeProcess();
|
||||
final session = AdbLogcatSession(fakeProcess);
|
||||
const device = AdbDevice(
|
||||
serial: 'R5CT90A1B2C',
|
||||
state: 'device',
|
||||
model: 'SM_S918N',
|
||||
product: 'dm3q',
|
||||
);
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`package:flutter/services.dart`를 import한다. `SystemChannels.platform` mock handler로 `Clipboard.setData` payload를 캡처하고, live log emit 후 `로그 복사` 버튼을 눌러 현재 표시 라인만 복사되는지 확인한다. query filter가 적용된 상태에서 복사해 filtered export contract도 함께 고정한다.
|
||||
|
||||
```dart
|
||||
final copiedTexts = <String>[];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
copiedTexts.add((call.arguments as Map<Object?, Object?>)['text']! as String);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||
});
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `test/console_page_test.dart`: services import 추가.
|
||||
- [ ] `test/console_page_test.dart`: filtered visible live logs copy test 추가.
|
||||
- [ ] `test/console_page_test.dart`: no live logs/sample placeholder export disabled test 추가.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
- path: `test/console_page_test.dart`
|
||||
- test name: `copies currently visible live log lines to clipboard`
|
||||
- assertion goal: query filter 적용 뒤 visible line만 clipboard payload에 들어간다.
|
||||
- test name: `keeps export disabled without live log lines`
|
||||
- assertion goal: no device/sample placeholder 상태나 live line empty 상태에서 `로그 복사` button `onPressed`가 null이다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
flutter test test/console_page_test.dart
|
||||
```
|
||||
|
||||
Expected: `All tests passed!`
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `lib/src/features/console/console_page.dart` | EXPORT-1 |
|
||||
| `test/console_page_test.dart` | EXPORT-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && dart format lib/src/features/console/console_page.dart test/console_page_test.dart && dart format --output=none --set-exit-if-changed lib/src/features/console/console_page.dart test/console_page_test.dart'\'''
|
||||
```
|
||||
|
||||
Expected: format applies or reports 0 changed on second command.
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && git diff --check -- lib/src/features/console/console_page.dart test/console_page_test.dart'\'''
|
||||
```
|
||||
|
||||
Expected: no output, exit 0.
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze && flutter test'\'''
|
||||
```
|
||||
|
||||
Expected: `No issues found!` and `All tests passed!`.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../models/adb_device.dart';
|
||||
import '../../services/adb_service.dart';
|
||||
|
|
@ -17,6 +18,7 @@ class ConsolePage extends StatefulWidget {
|
|||
|
||||
class _ConsolePageState extends State<ConsolePage> {
|
||||
final List<String> _logLines = [];
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
AdbLogcatSession? _session;
|
||||
StreamSubscription<String>? _subscription;
|
||||
bool _isLoading = false;
|
||||
|
|
@ -24,6 +26,7 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
int _sessionGeneration = 0;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _isPaused = false;
|
||||
_LogcatLevelFilter _levelFilter = _LogcatLevelFilter.all;
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
|
|
@ -56,8 +59,9 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_stopSession();
|
||||
_filterController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -131,10 +135,54 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
_session = null;
|
||||
}
|
||||
|
||||
List<String> get _displayLines =>
|
||||
(widget.device == null || widget.logcatStarter == null)
|
||||
? _sampleLogLines
|
||||
: _logLines;
|
||||
List<String> get _displayLines {
|
||||
final sourceLines = (widget.device == null || widget.logcatStarter == null)
|
||||
? _sampleLogLines
|
||||
: _logLines;
|
||||
|
||||
return sourceLines
|
||||
.where(_matchesLevelFilter)
|
||||
.where(_matchesQueryFilter)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _matchesLevelFilter(String line) {
|
||||
return switch (_levelFilter) {
|
||||
_LogcatLevelFilter.all => true,
|
||||
_LogcatLevelFilter.debug =>
|
||||
_LogcatLevel.fromLine(line) == _LogcatLevel.debug,
|
||||
_LogcatLevelFilter.info =>
|
||||
_LogcatLevel.fromLine(line) == _LogcatLevel.info,
|
||||
_LogcatLevelFilter.warn =>
|
||||
_LogcatLevel.fromLine(line) == _LogcatLevel.warn,
|
||||
_LogcatLevelFilter.error =>
|
||||
_LogcatLevel.fromLine(line) == _LogcatLevel.error,
|
||||
};
|
||||
}
|
||||
|
||||
bool _matchesQueryFilter(String line) {
|
||||
final query = _filterController.text.trim().toLowerCase();
|
||||
if (query.isEmpty) return true;
|
||||
|
||||
return line.toLowerCase().contains(query);
|
||||
}
|
||||
|
||||
bool get _hasLiveLogs =>
|
||||
widget.device != null &&
|
||||
widget.logcatStarter != null &&
|
||||
_displayLines.isNotEmpty;
|
||||
|
||||
Future<void> _copyVisibleLogsToClipboard() async {
|
||||
if (widget.device == null || widget.logcatStarter == null) return;
|
||||
final lines = _displayLines;
|
||||
if (lines.isEmpty) return;
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: lines.join('\n')));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('로그 ${lines.length}줄 복사됨')));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -181,25 +229,51 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 160, maxWidth: 320),
|
||||
child: TextField(
|
||||
controller: _filterController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: '패키지 또는 태그',
|
||||
labelText: '패키지, 태그 또는 텍스트',
|
||||
prefixIcon: const Icon(Icons.filter_alt),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '필터 초기화',
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
_filterController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SegmentedButton<String>(
|
||||
SegmentedButton<_LogcatLevelFilter>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 'all', label: Text('ALL')),
|
||||
ButtonSegment(value: 'debug', label: Text('DEBUG')),
|
||||
ButtonSegment(value: 'error', label: Text('ERROR')),
|
||||
ButtonSegment(
|
||||
value: _LogcatLevelFilter.all,
|
||||
label: Text('ALL'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _LogcatLevelFilter.debug,
|
||||
label: Text('DEBUG'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _LogcatLevelFilter.info,
|
||||
label: Text('INFO'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _LogcatLevelFilter.warn,
|
||||
label: Text('WARN'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _LogcatLevelFilter.error,
|
||||
label: Text('ERROR'),
|
||||
),
|
||||
],
|
||||
selected: const {'all'},
|
||||
onSelectionChanged: (_) {},
|
||||
selected: {_levelFilter},
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() {
|
||||
_levelFilter = selection.single;
|
||||
});
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -221,9 +295,19 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
tooltip: '지우기',
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
setState(_logLines.clear);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
tooltip: '로그 복사',
|
||||
onPressed: _hasLiveLogs
|
||||
? _copyVisibleLogsToClipboard
|
||||
: null,
|
||||
icon: const Icon(Icons.content_copy),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -303,6 +387,8 @@ enum _LogcatLevel {
|
|||
}
|
||||
}
|
||||
|
||||
enum _LogcatLevelFilter { all, debug, info, warn, error }
|
||||
|
||||
const _sampleLogLines = [
|
||||
'06-08 10:41:22.121 1832 2110 I ActivityTaskManager: START u0 com.toki.app/.MainActivity',
|
||||
'06-08 10:41:22.339 5521 5521 D AppSokInstall: install session committed',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:appsok/src/features/console/console_page.dart';
|
||||
|
|
@ -69,6 +70,59 @@ void main() {
|
|||
expect(_textColor(tester, errorLine), const Color(0xFFFFA3A3));
|
||||
});
|
||||
|
||||
testWidgets('filters visible rows by package tag or text query', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(body: ConsolePage()),
|
||||
),
|
||||
);
|
||||
|
||||
const matchingLine =
|
||||
'06-08 10:41:23.105 5521 5540 I ArtifactStore: app-debug.apk verified';
|
||||
const hiddenLine =
|
||||
'06-08 10:41:22.121 1832 2110 I ActivityTaskManager: START u0 com.toki.app/.MainActivity';
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'artifactstore');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text(matchingLine), findsOneWidget);
|
||||
expect(find.text(hiddenLine), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('필터 초기화'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text(hiddenLine), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('filters visible rows by log level segment', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(body: ConsolePage()),
|
||||
),
|
||||
);
|
||||
|
||||
const debugLine =
|
||||
'06-08 10:41:22.339 5521 5521 D AppSokInstall: install session committed';
|
||||
const errorLine =
|
||||
'06-08 10:41:23.411 5521 5521 E Sample: placeholder error row for filter styling';
|
||||
|
||||
await tester.tap(find.text('ERROR'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text(errorLine), findsOneWidget);
|
||||
expect(find.text(debugLine), findsNothing);
|
||||
|
||||
await tester.tap(find.text('DEBUG'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text(debugLine), findsOneWidget);
|
||||
expect(find.text(errorLine), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('appends live stream lines and stops session on dispose', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
@ -312,6 +366,143 @@ void main() {
|
|||
equals(controller.position.maxScrollExtent),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('clear button removes captured live log lines', (tester) async {
|
||||
final fakeProcess = _FakeProcess();
|
||||
final session = AdbLogcatSession(fakeProcess);
|
||||
const device = AdbDevice(
|
||||
serial: 'R5CT90A1B2C',
|
||||
state: 'device',
|
||||
model: 'SM_S918N',
|
||||
product: 'dm3q',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: ConsolePage(
|
||||
device: device,
|
||||
logcatStarter: ({serial}) async => session,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
fakeProcess.emitStdout('I/test: line before clear\n');
|
||||
await tester.pump(Duration.zero);
|
||||
expect(find.text('I/test: line before clear'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('지우기'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('I/test: line before clear'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('copies currently visible live log lines to clipboard', (
|
||||
tester,
|
||||
) async {
|
||||
final copiedTexts = <String>[];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
copiedTexts.add(
|
||||
(call.arguments as Map<Object?, Object?>)['text']! as String,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||
});
|
||||
|
||||
final fakeProcess = _FakeProcess();
|
||||
final session = AdbLogcatSession(fakeProcess);
|
||||
const device = AdbDevice(
|
||||
serial: 'R5CT90A1B2C',
|
||||
state: 'device',
|
||||
model: 'SM_S918N',
|
||||
product: 'dm3q',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: ConsolePage(
|
||||
device: device,
|
||||
logcatStarter: ({serial}) async => session,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
fakeProcess.emitStdout('I/Keep: keep this line\n');
|
||||
fakeProcess.emitStdout('I/Drop: drop this line\n');
|
||||
await tester.pump(Duration.zero);
|
||||
|
||||
// Apply a query filter so only one line is visible.
|
||||
await tester.enterText(find.byType(TextField), 'keep');
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('로그 복사'));
|
||||
await tester.pump();
|
||||
|
||||
expect(copiedTexts, ['I/Keep: keep this line']);
|
||||
});
|
||||
|
||||
testWidgets('keeps export disabled without live log lines', (tester) async {
|
||||
// No device / sample placeholder state: export button is disabled.
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(body: ConsolePage()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final sampleCopyButton = tester.widget<IconButton>(
|
||||
find.ancestor(
|
||||
of: find.byIcon(Icons.content_copy),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
);
|
||||
expect(sampleCopyButton.onPressed, isNull);
|
||||
|
||||
// Live mode but no captured lines yet: still disabled.
|
||||
final fakeProcess = _FakeProcess();
|
||||
final session = AdbLogcatSession(fakeProcess);
|
||||
const device = AdbDevice(
|
||||
serial: 'R5CT90A1B2C',
|
||||
state: 'device',
|
||||
model: 'SM_S918N',
|
||||
product: 'dm3q',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: ConsolePage(
|
||||
device: device,
|
||||
logcatStarter: ({serial}) async => session,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final emptyLiveCopyButton = tester.widget<IconButton>(
|
||||
find.ancestor(
|
||||
of: find.byIcon(Icons.content_copy),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
);
|
||||
expect(emptyLiveCopyButton.onPressed, isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Color? _textColor(WidgetTester tester, String text) {
|
||||
|
|
|
|||
|
|
@ -971,7 +971,7 @@ void main() {
|
|||
final navTargets = <Map<String, String>>[
|
||||
{'label': '빌드', 'marker': 'Jenkins 연결 필요'},
|
||||
{'label': '디바이스', 'marker': 'Pixel 8'},
|
||||
{'label': '콘솔', 'marker': '패키지 또는 태그'},
|
||||
{'label': '콘솔', 'marker': '패키지, 태그 또는 텍스트'},
|
||||
{'label': '설정', 'marker': 'Base URL'},
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue