Compare commits

Author SHA1 Message Date
gitea-agent 85de08b2c6 gitea-agent: rework PR #9
Review-ID: 41
Previous-SHA: 15d8c75308
2000-01-01 00:00:00 +00:00
code-agent 15d8c75308 code-agent: rework issue 8
Review-ID: 40
Previous-SHA: 7bfe2b62f4
Plan-SHA256: 917511183e1494310bb6a0ce480788a8bfadfef0c6f6b309f7324f212964d9fb
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
2000-01-01 00:00:00 +00:00
code-agent 7bfe2b62f4 code-agent: rework issue 8
Review-ID: 39
Previous-SHA: 925c107f9a
Plan-SHA256: 7ceaf8e0c832478852e3fde7d13d060cbb688be09c1b131deb5ccc32c85ed2ff
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
2000-01-01 00:00:00 +00:00
code-agent 925c107f9a code-agent: rework issue 8
Review-ID: 38
Previous-SHA: 1f73b4bd41
Plan-SHA256: cbefda14ef548909f419e943825b3d11d9dff9809b9bb443b06d43db183063c0
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
2000-01-01 00:00:00 +00:00
code-agent 1f73b4bd41 code-agent: implement issue 8
Approval-Correlation: a1502ebfff533b69c158f3c6c1316c01
Plan-SHA256: aae2d7573d66fb5573378443712149a4f7ba23f7f9de19e25b00f9b760d05c3e
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
Base-SHA: d0ac7fc7bc
2000-01-01 00:00:00 +00:00
genichin d0ac7fc7bc Merge pull request '이슈 #6 구현' (#7) from code-agent/issue-6-e5da4592756fb704 into main
Reviewed-on: #7
Reviewed-by: spec-agent <spec-agent@mail.jsfamily.quest>
2026-09-19 11:15:15 +00:00
code-agent d102c02bc1 code-agent: rework issue 6
Review-ID: 36
Previous-SHA: 14302a26f9
Plan-SHA256: db38eaf9d5fcf09a0a9d62f44c373eb67bfd6fcffdc40fef5f67762f0d28c477
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
2000-01-01 00:00:00 +00:00
code-agent 14302a26f9 code-agent: implement issue 6
Approval-Correlation: e5da4592756fb704256d8737d31662e5
Plan-SHA256: 0fbe8b996656fb908c513b31e30706f7521cf76eeac60778a1c28f46bfc19b46
Bundle-SHA256: 29ce1ef1917864f79938459725201e6e74d67e21b1f817ac53d7b294eca8f1a2
Base-SHA: cabd354d71
2000-01-01 00:00:00 +00:00
7 changed files with 869 additions and 118 deletions
+32 -24
View File
@@ -1,39 +1,47 @@
<!-- gitea-agent:documentation-template/data-model/v1 -->
# 데이터 모델 # 데이터 모델
## 대화 상태 ## 목적과 범위
`ChatApp`은 세션 동안 다음 목록을 메모리에 유지한다. 이 문서는 대화 세션에서 사용자 메시지와 에이전트 응답의 작성자 의미를 명시적으로 관리하는 데이터 모델을 정의한다. 메시지의 역할과 본문을 정본 데이터로 삼고, 화면 라벨과 구조적 스타일은 역할에서 파생한다. 외부 API 연동, 인증, 사용자별 기록 관리 및 영속 저장은 범위에 포함하지 않는다.
- `submitted_inputs`: `trim`된 유효 사용자 메시지 목록 ## 정본 엔터티
- `selected_responses`: 각 사용자 메시지에 대응하는 선택 응답 목록
두 목록은 같은 인덱스를 사용하며, 유효한 제출 하나마다 항목 하나씩 추가된다. 공백 입력은 어느 목록도 변경하지 않는다. 정본 메시지 엔터티는 작성자 역할과 본문을 함께 보유한다. 작성자 역할은 사용자 또는 에이전트 중 하나이며, 역할별 화면 라벨은 각각 사용자와 에이전트로 정해진다. 본문은 사용자 입력의 앞뒤 공백을 제거한 문자열 또는 고정 응답 목록에서 선택된 문자열이다.
## 고정 응답 목록 ### `message-author-semantics`
`src/chat_tui/app.py`의 `RESPONSE_MESSAGES` 상수는 다음 다섯 문자열을 순서대로 보유한다. 메시지의 작성자 의미는 화면 표현이 아니라 정본 데이터 모델에서 관리한다. 각 메시지는 작성자 역할과 본문을 필수 값으로 가지며, 역할은 사용자와 에이전트 중 하나로 제한한다.
1. `안녕하세요! 무엇을 도와드릴까요?` - 사용자 메시지는 `user` 역할과 정규화된 사용자 입력을 가진다.
2. `그것은 제가 처리하기 불가능합니다.` - 에이전트 응답은 `agent` 역할과 선택된 응답 본문을 가진다.
3. `맡겨만 주세요.` - 역할별 표시 라벨은 사용자 역할에서 `사용자`, 에이전트 역할에서 `에이전트`로 파생한다.
4. `지금 처리 중입니다.` - 화면의 `user-message`, `response-message`, `user-role`, `agent-role` 클래스와 좌우 정렬 및 테두리는 역할에서 파생되는 표현이며 역할의 정본이 아니다.
5. `작업을 완료하였습니다.` - 메시지 본문은 역할과 분리된 문자열 값이며, 사용자 입력은 앞뒤 공백을 제거한 뒤 보존한다.
- 메시지 목록은 유효한 제출마다 사용자 메시지를 먼저, 에이전트 응답을 다음에 보유한다.
- 빈 문자열 또는 공백만 포함한 입력에는 메시지 엔터티와 응답을 생성하지 않는다.
응답은 `random.choice(RESPONSE_MESSAGES)`로 선택한다. 난수 선택 자체는 테스트에서 `chat_tui.app.random.choice`를 대체할 수 있는 경계로 취급한다. 역할과 본문은 하나의 불변 메시지 값으로 취급한다. 메시지가 생성된 뒤 역할을 화면 스타일에 맞추어 변경하거나 본문과 분리하여 관리하지 않는다. 에이전트 응답은 외부 시스템 결과가 아니라 고정된 응답 집합에서 선택된 세션 값이며, 작성자 역할은 선택 결과와 무관하게 항상 에이전트로 유지한다.
## 대화 항목 이 모델은 현재 세션 메모리에서만 사용된다. 저장 위치, 직렬화 형식, 식별자 생성 규칙 및 데이터베이스 스키마는 정의하지 않는다. 따라서 기존 저장 데이터에 대한 마이그레이션이나 저장 실패에 대한 롤백은 없다. 향후 영속화를 도입할 경우 역할 값과 본문을 함께 원자적으로 저장하고 사용자 메시지와 에이전트 응답의 순서를 보존해야 한다.
유효한 입력이 제출되면 대화 영역에 다음 두 `Label`을 순서대로 추가한다. 검증은 다음 조건을 확인한다.
1. `user-message` 클래스의 사용자 메시지 - 유효한 입력의 앞뒤 공백이 제거되고 사용자 역할로 기록되는지 확인한다.
2. `response-message` 클래스의 선택 응답 - 같은 제출에서 선택된 응답이 에이전트 역할로 기록되는지 확인한다.
- 여러 제출의 메시지가 사용자와 에이전트 순서로 반복되는지 확인한다.
- 역할 라벨과 구조적 스타일이 해당 역할에서 일관되게 파생되는지 확인한다.
- 빈 입력과 공백 입력에서 응답 선택, 메시지 목록 변경 및 화면 항목 추가가 발생하지 않는지 확인한다.
- 역할을 CSS 클래스나 표시 위치에서 추론하지 않고 정본 메시지 값에서 직접 검증하는지 확인한다.
사용자 메시지는 입력값을 `strip()`한 결과이며, 선택 응답은 고정 응답 목록의 문자열이다. ## 관계와 불변조건
## 불변 조건 각 유효한 사용자 입력은 사용자 역할의 메시지 하나와 그에 대응하는 에이전트 역할의 메시지 하나를 순서대로 만든다. 역할은 CSS 클래스나 화면 위치에서 추론하지 않으며 메시지 데이터에 직접 보존한다. 빈 입력과 공백 입력은 메시지와 응답을 만들지 않는다.
- 공백 제거 결과가 빈 문자열이면 상태와 화면을 변경하지 않는다. ## 저장과 마이그레이션
- 유효한 입력마다 사용자 메시지가 응답보다 먼저 추가된다.
- `submitted_inputs`와 `selected_responses`의 길이는 항상 같다. 메시지는 애플리케이션 세션 메모리에만 보관하며 직렬화 형식이나 영속 저장소는 정의하지 않는다. 따라서 기존 데이터 마이그레이션과 롤백 절차는 필요하지 않다. 이후 저장 기능이 추가되더라도 역할과 본문을 함께 저장하고 기존 순서를 보존해야 한다.
- 응답은 고정 목록에 포함된 값이어야 한다.
- 대화 기록은 애플리케이션 세션 안에서만 유지한다. ## 검증
정상 입력에서는 역할, 본문, 생성 순서가 일치하는지 검증한다. 경계 입력에서는 빈 문자열과 공백 문자열이 상태를 변경하지 않는지 확인한다. 렌더링 검증에서는 역할에서 파생된 라벨과 구조적 스타일이 일치하는지 확인하며, 영속 저장 후 재조회와 마이그레이션 검증은 해당 없음으로 판정한다.
+48 -24
View File
@@ -1,38 +1,62 @@
<!-- gitea-agent:documentation-template/interface-contract/v1 -->
# 인터페이스 계약 # 인터페이스 계약
## 메시지 제출 이벤트 ## 목적과 범위
Textual의 `Input.Submitted` 이벤트가 `#user-input`에서 발생하면 `ChatApp.on_input_submitted`가 처리한다. 이 문서는 대화형 Textual 화면에서 사용자 메시지와 에이전트 응답을 작성자 역할, 라벨, 구조적 스타일 및 표시 순서로 구분하는 인터페이스를 정의한다. 메시지 역할과 본문은 표시 결과의 정본이며, 외부 API 연동이나 영속 저장은 범위에 포함하지 않는다.
### 입력 정규화 ## 계약
1. 이벤트의 `value`에 `strip()`을 적용한다. - 각 메시지는 작성자 역할과 본문을 함께 가져야 한다.
2. 입력 위젯의 값을 빈 문자열로 초기화한다. - 사용자 역할은 `user`, 에이전트 역할은 `agent`로 표현한다.
3. 정규화 결과가 빈 문자열이면 추가 처리를 중단한다. - 사용자 역할의 화면 라벨은 `사용자`, 에이전트 역할의 화면 라벨은 `에이전트`다.
- 사용자 메시지는 `사용자: 본문` 형식으로, 에이전트 응답은 `에이전트: 본문` 형식으로 표시한다.
- 사용자 메시지는 `chat-message user-message user-role` 클래스를 사용하고, 에이전트 응답은 `chat-message response-message agent-role` 클래스를 사용한다.
- 사용자 메시지와 에이전트 응답은 각각 왼쪽과 오른쪽 정렬 및 서로 다른 측면 테두리로 구분한다.
- 유효한 입력 하나는 사용자 메시지 다음에 에이전트 응답을 추가한다.
공백만 포함한 입력은 사용자 메시지, 응답 선택, 대화 항목을 생성하지 않는다. ### `conversation-message-rendering`
### 유효한 입력 처리 메시지 렌더링 인터페이스는 명시적인 역할을 가진 메시지 값을 화면 항목으로 변환한다.
정규화 결과가 비어 있지 않으면 다음 순서로 처리한다. - 입력: 작성자 역할과 문자열 본문을 가진 메시지
- 출력: 작성자 라벨과 본문을 결합한 라벨 위젯
- 사용자 역할: `user` 값, `사용자` 라벨, `user-message` 및 `user-role` 클래스
- 에이전트 역할: `agent` 값, `에이전트` 라벨, `response-message` 및 `agent-role` 클래스
- 표시 형식: 역할 라벨, 콜론, 공백, 본문 순서
- 구조적 표현: 사용자 항목은 왼쪽 정렬과 왼쪽 테두리, 에이전트 항목은 오른쪽 정렬과 오른쪽 테두리
- 순서: 유효한 제출마다 사용자 메시지를 먼저 표시하고 대응하는 에이전트 응답을 바로 뒤에 표시
1. 정규화된 사용자 메시지를 `submitted_inputs`에 추가한다. 역할과 본문은 하나의 메시지 의미로 취급하며, 라벨과 스타일은 역할에서 파생한다. 화면의 위치나 스타일만으로 작성자를 판정해서는 안 된다. 메시지 본문은 사용자 입력의 앞뒤 공백을 제거한 값 또는 고정 응답 목록에서 선택된 값이어야 한다.
2. `random.choice(RESPONSE_MESSAGES)`로 응답을 선택한다.
3. 선택 응답을 `selected_responses`에 추가한다.
4. 대화 영역에 사용자 메시지 `Label`을 추가한다.
5. 바로 이어서 응답 `Label`을 추가한다.
6. 대화 영역을 마지막 항목까지 스크롤한다.
사용자 메시지와 응답은 별도 대화 항목이며, 화면 표시 순서는 항상 사용자 메시지 다음 응답이다. 빈 입력이나 공백만 포함한 입력은 정상적인 렌더링 요청으로 간주하지 않는다. 이 경우 응답을 선택하지 않고 메시지 목록과 대화 영역을 변경하지 않으며, 입력창만 비운다.
## 화면 위젯 계약 호환성을 위해 기존 대화 영역과 입력창의 위젯 식별자, 사용자 및 응답 항목의 클래스, Enter 제출, 고정 응답 선택, Escape와 Ctrl+Q 종료 동작을 유지한다. 작성자 라벨과 역할 클래스는 기존 표시 항목에 추가되는 계약이며, 대화 영역 전체 레이아웃을 재설계하지 않는다.
- 대화 영역은 `#conversation` 식별자의 `VerticalScroll`이다. 검증에서는 정상 입력 두 건을 제출하여 역할, 본문, 라벨, 클래스 및 표시 순서를 함께 비교한다. 역할별 정렬과 테두리 규칙이 색상과 독립적으로 존재하는지 확인하고, 빈 입력과 공백 입력에서 응답 선택과 새 화면 항목이 발생하지 않는지 확인한다. 또한 표준 응답 선택 호출과 종료 동작이 기존 방식대로 유지되는지 확인한다.
- 입력창은 `#user-input` 식별자의 `Input`이다.
- 사용자 메시지는 `user-message` 클래스를 사용한다.
- 응답은 `response-message` 클래스를 사용한다.
- 애플리케이션 마운트 시 입력창에 포커스를 둔다.
## 종료 계약 ## 호환성
`Escape`와 `Ctrl+Q`는 기존 `quit` 바인딩을 통해 애플리케이션 종료를 요청한다. - Enter로 제출하는 기존 입력 방식을 유지한다.
- 입력은 앞뒤 공백을 제거한 뒤 처리하며, 빈 입력이나 공백만 있는 입력은 응답 선택과 화면 변경 없이 무시한다.
- 응답은 기존 고정 응답 목록에서 표준 무작위 선택 방식으로 생성한다.
- 기존 대화 영역과 입력창의 위젯 식별자 및 메시지 표시 순서를 유지한다.
- Escape와 Ctrl+Q 종료 동작을 유지한다.
- 역할 의미는 화면 위치나 CSS 클래스만으로 추론하지 않고 메시지 데이터에서 직접 제공한다.
## 예시
- 입력값이 ` 첫 번째 입력 `이면 `사용자: 첫 번째 입력`으로 표시한다.
- 선택된 응답이 `맡겨만 주세요.`이면 `에이전트: 맡겨만 주세요.`로 표시한다.
- 여러 제출은 각 제출마다 사용자 메시지와 에이전트 응답의 쌍을 만들며, 전체 순서는 제출 순서와 일치한다.
- 사용자 항목에는 왼쪽 정렬과 왼쪽 테두리를 적용하고, 에이전트 항목에는 오른쪽 정렬과 오른쪽 테두리를 적용한다.
- 빈 문자열 또는 공백 문자열을 제출하면 새 라벨과 응답이 생성되지 않는다.
## 검증
- 역할 값과 본문이 올바르게 결합되어 메모리의 메시지 목록에 보존되는지 확인한다.
- 렌더링된 라벨이 역할별 접두사와 본문을 정확히 포함하는지 확인한다.
- 역할별 CSS 클래스, 정렬 규칙 및 양쪽 테두리가 구현에 존재하는지 확인한다.
- 여러 입력에서 사용자 메시지 뒤에 에이전트 응답이 표시되는지 확인한다.
- 빈 입력과 공백 입력에서 응답 선택, 메시지 기록 및 라벨 추가가 발생하지 않는지 확인한다.
- 기존 무작위 응답 선택과 종료 동작이 변경되지 않았는지 회귀 검증한다.
+119 -32
View File
@@ -1,44 +1,131 @@
# 프로젝트 스캐폴드 요구사항 ---
{
"schema": "proofline.requirement/v1",
"id": "REQ-001",
"title": "프로젝트 스캐폴드의 메시지 작성자 구분",
"revision": "0.1",
"status": "approved",
"depends_on": [],
"applies_to": ["chat-tui-message-rendering"],
"language": "ko"
}
---
## 범위 # REQ-001 — 프로젝트 스캐폴드의 메시지 작성자 구분
Python 3.11 이상과 표준 `pyproject.toml`을 사용하는 Textual 기반 TUI 초기 스캐폴드를 제공한다. ## 1. 목적
## 기능 요구사항 대화형 Textual TUI의 프로젝트 스캐폴드와 실행 기반을 보존하고, 메시지 작성자 의미를 명시적인 데이터 역할로 관리한다. 입력 정규화, 응답 선택, 메시지 누적 및 종료와 같은 대화 상호작용의 상세 규칙은 `REQ-002`에서 관리한다.
1. `chat-tui` 콘솔 명령과 `python -m chat_tui` 실행을 지원한다. ## 2. 배경
2. Textual 화면에 사용자 입력창과 대화 영역을 표시한다.
3. 사용자가 입력을 제출하면 입력값을 `trim`한 뒤 비어 있지 않은 사용자 메시지를 제출 순서대로 대화 영역에 누적한다.
4. 유효한 사용자 메시지 제출 후 고정된 다섯 개 응답 목록에서 표준 난수 선택으로 하나의 응답을 결정한다.
5. 선택한 응답을 해당 사용자 메시지 바로 뒤에 별도의 대화 항목으로 표시한다.
6. 유효한 입력 처리 뒤 입력창을 비운다.
7. 빈 입력 또는 공백만 포함한 입력은 사용자 메시지와 응답을 대화 기록에 추가하지 않는다.
8. `Escape` 또는 `Ctrl+Q`로 애플리케이션 종료를 요청할 수 있다.
9. 입력 누적, 응답 선택과 표시, 공백 입력 무시, 입력창 초기화 및 종료 동작을 자동 테스트로 검증한다.
## 고정 응답 목록 애플리케이션은 Python 3.11 이상과 Textual 런타임을 사용하는 로컬 TUI다. 기존 메시지 표시의 작성자 의미는 `ChatMessage`와 `MessageRole`로 표현되어야 하며, 화면 라벨과 구조적 스타일은 그 역할에서 파생되어야 한다. 이번 문서는 프로젝트 구조·패키징·실행 진입점과 승인된 작성자 모델을 보존하고, 대화 상호작용 규칙을 별도 요구사항으로 분리한다.
응답은 다음 다섯 문구 중 하나를 사용한다. ## 3. 범위
- 안녕하세요! 무엇을 도와드릴까요? 포함 범위는 Python 패키지 구조, 패키징 메타데이터, Textual 실행 기반, `chat_tui.app:main` 콘솔 실행 진입점과 승인된 메시지 작성자 모델이다. 작성자 모델은 `ChatMessage`와 `MessageRole`을 통해 사용자와 에이전트의 의미를 표현하고, 렌더러가 역할에서 라벨과 구조적 스타일을 파생할 수 있도록 한다.
- 그것은 제가 처리하기 불가능합니다.
- 맡겨만 주세요.
- 지금 처리 중입니다.
- 작업을 완료하였습니다.
응답 선택에는 표준 라이브러리의 난수 선택 기능을 사용하며, 외부 서비스나 LLM은 호출하지 않는다. 제외 범위는 입력 정규화, 고정 응답 선택, 메시지 누적 순서, 빈 입력 처리, 입력창 초기화와 종료 단축키를 포함한 대화 상호작용의 세부 동작이다. 이러한 규칙과 인수 기준은 `REQ-002`에서 단일 정본으로 관리한다. 외부 API, LLM, 인증, 영속 저장소, 데이터베이스와 새 의존성도 추가하지 않는다.
## 제외 범위 ## 4. 용어와 기준
LLM 연동, 외부 API 호출, 인증, 영구 대화 기록 저장, 사용자별 응답 기록, 응답 목록 편집, 사용자 입력에 따른 맞춤형 응답 생성은 구현하지 않는다. 프로젝트 스캐폴드는 Python 패키지 구조, 선언된 런타임 의존성과 콘솔 실행 진입점의 조합을 뜻한다. 메시지 역할은 작성자 의미를 나타내는 `user` 또는 `agent` 값이며, `ChatMessage.role`과 `ChatMessage.content`가 작성자 의미의 정본이다. 사용자·에이전트 라벨과 CSS 클래스는 이 역할에서 파생되는 표시 정보다.
## 수용 기준 ## 5. 요구사항
- Python 3.11 이상 환경에서 의존성을 설치할 수 있다. ### REQ-001.01 — 프로젝트 패키지와 실행 기반
- TUI가 실행되고 입력창에 포커스가 배치된다.
- 두 번 이상 제출한 입력이 `trim`된 값으로 제출 순서대로 표시된다. 프로젝트는 Python 3.11 이상에서 현재 Textual 의존성을 사용하고, 기존 패키지 구조와 `chat_tui.app:main` 콘솔 실행 진입점을 유지해야 한다. 구현은 외부 API, LLM, 인증, 영속 저장소 또는 별도 데이터베이스를 추가하지 않아야 한다.
- 각 유효한 사용자 메시지 바로 뒤에 고정 목록에서 선택된 응답이 표시된다.
- 자동 테스트에서 난수 선택 결과를 제어하고 선택 호출 및 표시 순서를 검증할 수 있다. ### REQ-001.02 — 승인된 메시지 작성자 모델
- 빈 입력과 공백만 포함한 입력은 사용자 메시지, 응답 및 응답 선택 결과를 남기지 않는다.
- 유효한 입력 제출 뒤 입력창이 비워진다. 메시지 모델은 `ChatMessage`와 `MessageRole`을 통해 작성자 역할과 본문을 함께 표현해야 한다. 역할 값은 `user`와 `agent`이며, UI 렌더러는 역할에서 사용자·에이전트 라벨과 기존 `user-message`·`response-message` 식별자를 파생해야 한다. 역할 구분은 색상에만 의존하지 않고 역할별 CSS 식별자와 테두리 또는 정렬 차이를 포함해야 한다.
- `Escape` 및 `Ctrl+Q`가 애플리케이션 종료를 요청한다.
## 6. 제약과 불변조건
- Python 3.11 이상과 현재 Textual 기반 TUI 구조 및 선언된 의존성을 유지한다.
- `chat_tui.app:main` 콘솔 실행 진입점과 기존 패키지 위치를 유지한다.
- 메시지의 작성자 의미는 CSS 클래스나 화면 색상에서 추론하지 않고 명시적인 `MessageRole` 값에서 확인한다.
- 사용자와 에이전트의 라벨 및 구조적 스타일은 역할과 일치해야 한다.
- 입력 처리와 응답 동작의 상세 규칙은 이 문서에 복제하지 않고 `REQ-002`를 따른다.
- 외부 API, LLM, 인증, 영속 저장소, 데이터베이스 및 새 의존성은 추가하지 않는다.
## 7. 인수 기준
### REQ-001.01-AC01 — 패키지와 실행 진입점 보존
프로젝트가 Python 3.11 이상과 선언된 Textual 의존성으로 설치되고 `chat_tui.app:main` 진입점을 계속 제공하면 통과로 판정한다. 이 기준은 프로젝트 구조와 실행 기반만 검증하며 대화 상호작용의 판정은 `REQ-002`에 위임한다.
### REQ-001.02-AC01 — 역할이 작성자 표시의 정본임
`ChatMessage`가 `MessageRole`과 본문을 함께 보유하고, 사용자와 에이전트 표시의 라벨·식별자·구조적 스타일이 그 역할에서 파생되며 색상만으로 역할을 판정하지 않으면 통과로 판정한다.
## 8. 오류 및 충돌 계약
- 프로젝트 설치 또는 콘솔 진입점이 제공되지 않으면 스캐폴드 요구사항을 충족한 것으로 판정하지 않는다.
- 정의되지 않은 메시지 역할은 정상 표시하지 않으며 역할과 라벨의 매핑은 명시적인 기준을 따른다.
- 프로젝트 구조·패키징·실행 기반에 관한 충돌은 이 문서를 따른다.
- 입력·응답·누적·입력창·종료 동작에 관한 충돌은 `REQ-002`를 따른다.
## 9. 데이터 및 인터페이스 계약
- `MessageRole`은 문자열 기반 열거형이며 값은 `user` 또는 `agent`다.
- `ChatMessage.role`은 필수 역할 값이고 `ChatMessage.content`는 필수 본문 문자열이다.
- 역할에서 사용자 라벨 `사용자` 또는 에이전트 라벨 `에이전트`를 파생할 수 있어야 한다.
- 사용자 표시 항목은 `chat-message user-message user-role` 식별자를, 에이전트 표시 항목은 `chat-message response-message agent-role` 식별자를 사용한다.
- 상세 입력·응답·세션 누적 계약은 `REQ-002`에 정의한다.
## 10. 하위 호환성
- Python 3.11 이상, 현재 Textual 런타임 의존성, 패키지 위치 및 `chat_tui.app:main` 진입점을 유지한다.
- 기존 `#conversation`과 `#user-input` 식별자를 유지한다.
- 작성자 모델과 역할별 표시를 추가해도 대화 영역 전체 레이아웃을 재설계하지 않는다.
- 입력 제출, 응답 선택, 입력창 초기화와 종료 동작은 `REQ-002`의 기존 계약을 따른다.
## 11. 검증 계획
패키지 메타데이터와 콘솔 진입점이 Python 3.11 이상 및 선언된 Textual 환경에서 제공되는지 확인한다. `ChatMessage`와 `MessageRole`의 역할-본문 모델, 역할별 라벨과 식별자, 테두리 또는 정렬 기반 스타일을 검증한다. 입력 정규화, 응답 선택, 메시지 순서, 빈 입력, 입력창 초기화와 종료 단축키의 실행 검증은 `REQ-002`의 테스트 추적성 계약에서 관리한다.
### 테스트 추적성 계약
테스트 정의:
```text
Test ID: TC-001-001
Verifies:
- REQ-001.01-AC01
- REQ-001.02-AC01
Test reference: 프로젝트 패키징 검사와 Textual 애플리케이션 테스트
Automated: 패키지 진입점, 메시지 역할 모델, 역할별 라벨·식별자·구조적 스타일을 자동 검증한다.
```
실행 증거:
```text
Test ID: TC-001-001
Requirement revision: REQ-001@0.1
Commit: 실행 증거 미기록
Environment: Python 3.11 이상, 선언된 Textual 런타임 의존성
Result: 미실행
Executed at: 검증 실행 시각 미기록
```
## 12. 추적성
근거 이슈는 Gitea 이슈 #6인 메시지 표시창의 메시지 작성자 구분 요청이다. 구현 PR은 아직 없으며, 관련 설계 문서는 데이터 모델, 인터페이스 계약, 런타임 흐름 및 프로젝트 구조 문서다. 이 문서는 프로젝트 스캐폴드와 승인된 작성자 모델의 정체성을 보존하고, 대화 상호작용의 상세 규칙은 `REQ-002`의 원본 Gitea 이슈 #8 추적성으로 분리한다.
## 13. 확정된 결정
프로젝트의 Python 패키지 구조, Textual 실행 기반과 `chat_tui.app:main` 진입점을 유지한다. 메시지 작성자 의미는 `MessageRole`과 `ChatMessage`에 명시적으로 보존하고 라벨·식별자·구조적 스타일은 역할에서 파생한다. 입력과 응답의 세부 동작은 `REQ-002`에서 관리한다.
## 14. 미결 사항
없음
## 15. 승인 기록
- 상태: `approved`
- 승인 주체: `spec-agent`
- 승인 근거: 승인 댓글 923 및 디스커버리 승인 결과
문서 분리와 정리만으로 기존 승인 상태, 승인 주체 또는 승인 근거를 변경하지 않는다.
@@ -0,0 +1,478 @@
---
{
"schema": "proofline.requirement/v1",
"id": "REQ-002",
"title": "대화 입력과 고정 응답 상호작용",
"revision": "0.1",
"status": "draft",
"depends_on": [
"REQ-001"
],
"applies_to": [
"Textual 대화형 TUI",
"입력 정규화와 메시지 누적",
"고정 응답 선택과 표시",
"빈 입력 처리와 종료 동작"
],
"language": "ko"
}
---
# REQ-002 — 대화 입력과 고정 응답 상호작용
## 1. 목적
이 요구사항은 Textual 기반 대화형 TUI에서 사용자가 제출한 입력을 정규화하고, 유효한 사용자 메시지와 고정 응답을 세션 내 대화 영역에 제출 순서대로 누적 표시하는 동작을 정의한다. 입력의 양끝 공백은 제거하고, 빈 문자열이나 공백만 포함한 입력은 응답 선택과 대화 상태 변경 없이 무시한다. 유효하거나 무효인 제출이 끝나면 입력창을 비운다.
사용자 메시지와 에이전트 응답은 명시적인 역할, 라벨 및 구조적 스타일로 구분한다. 유효한 입력마다 정본으로 정의된 고정 응답 목록에서 하나를 표준 난수 방식으로 선택하며, 사용자 메시지 바로 뒤에 에이전트 응답을 표시한다. Escape와 Ctrl+Q는 동일한 애플리케이션 종료 동작을 요청한다. 프로젝트 설치와 실행 진입점은 선행 프로젝트 스캐폴드 요구사항을 따르며, 이 문서는 대화 상호작용의 의미와 검증 기준만 관리한다.
## 2. 배경
애플리케이션은 Python 3.11 이상과 Textual 런타임을 사용하는 로컬 TUI로 제공된다. 사용자는 입력창에 문자열을 작성하고 Enter로 제출한다. 시스템은 제출된 문자열의 양끝 공백을 제거한 뒤 유효성을 판단하고, 유효한 입력에 대해서만 사용자 메시지와 고정 응답을 생성하여 표시한다.
메시지와 응답은 실행 중인 세션의 메모리 상태로만 관리한다. 외부 API, LLM, 네트워크 서비스, 데이터베이스 또는 영속 저장소는 사용하지 않는다. 프로젝트 패키징과 실행 진입점은 선행 프로젝트 스캐폴드 요구사항에서 관리하며, 입력 정규화, 메시지 누적, 고정 응답 선택과 표시, 빈 입력 처리, 입력창 초기화 및 종료 동작은 이 문서에서 단일 정본으로 관리한다. 근거는 원본 Gitea 이슈 8의 요구사항 재작업 요청이다.
## 3. 범위
포함 범위는 다음과 같다.
- 입력 양끝의 공백 제거와 정규화 결과에 따른 유효 입력 판정
- 유효한 사용자 메시지와 고정 응답의 세션 내 누적 및 표시
- 정본 고정 응답 목록을 대상으로 하는 표준 난수 선택
- 사용자 메시지 뒤에 에이전트 응답을 배치하는 순서 보장
- 사용자와 에이전트의 역할, 라벨 및 구조적 스타일 구분
- 빈 문자열, 공백, 탭 또는 줄바꿈만 포함한 입력의 무시
- 유효하거나 무효인 제출 후 입력창 초기화
- Escape와 Ctrl+Q에 의한 동일한 종료 요청
- 선행 프로젝트 스캐폴드 요구사항과의 의존 관계 및 원본 이슈 8 추적성
제외 범위는 다음과 같다.
- 프로젝트 패키징, 설치 설정 및 실행 진입점 자체의 변경
- 외부 API, LLM, 네트워크 서비스, 데이터베이스 또는 영속 저장소 연동
- 사용자 인증, 권한 관리, 다중 세션, 메시지 복원 또는 대화 저장
- 고정 응답 문구, 입력 처리 순서와 기존 사용자 인터페이스 의미의 변경
- 새로운 의존성, 배포 방식 또는 실행 환경의 추가
프로젝트 구조, 패키징 메타데이터와 실행 기반은 선행 프로젝트 스캐폴드 요구사항을 정본으로 사용하며 이 문서에서 중복 정의하지 않는다.
### 포함
사용자 입력의 양끝 공백 제거와 유효성 판정, 유효 메시지와 고정 응답의 세션 내 누적 및 표시, 표준 난수 방식의 응답 선택, 사용자와 에이전트의 역할별 표시, 빈 입력 무시, 제출 후 입력창 초기화, Escape와 Ctrl+Q 종료 요청 및 선행 프로젝트 스캐폴드와의 의존 관계와 원본 이슈 추적성을 포함한다.
### 제외
프로젝트 패키징, 설치 설정 및 실행 진입점의 변경, 외부 API·LLM·네트워크 서비스·데이터베이스·영속 저장소 연동, 사용자 인증과 다중 세션 관리, 메시지 복원, 새로운 의존성·배포 방식·실행 환경 추가는 포함하지 않는다.
## 4. 용어와 기준
입력은 사용자가 입력창에서 제출한 문자열이다. 정규화는 문자열 양끝의 공백을 제거하는 처리이며 문자열 내부의 공백은 변경하지 않는다. 정규화 결과가 빈 문자열이면 유효하지 않은 입력으로 판단한다.
유효한 제출은 정규화된 사용자 메시지와 고정 응답 목록에서 선택된 에이전트 응답으로 구성된다. 사용자 메시지는 사용자 역할과 사용자 라벨을 갖고, 응답은 에이전트 역할과 에이전트 라벨을 갖는다. 역할과 본문은 세션 메시지의 정본 상태이며, 라벨과 구조적 스타일은 이 역할에서 파생한다.
고정 응답 목록은 다음 다섯 문구로 구성한다.
- 안녕하세요! 무엇을 도와드릴까요?
- 그것은 제가 처리하기 불가능합니다.
- 맡겨만 주세요.
- 지금 처리 중입니다.
- 작업을 완료하였습니다.
유효한 입력마다 목록 전체를 대상으로 표준 난수 선택을 한 번 수행한다. 선택된 응답은 대응하는 사용자 메시지 바로 뒤에 배치한다. 종료 요청은 Escape 또는 Ctrl+Q 입력으로 발생하며, 종료 과정에서 외부 상태나 영속 데이터를 생성하지 않는다.
선행 프로젝트 스캐폴드 요구사항은 프로젝트 구조, 패키징 및 실행 진입점의 정본이다. 이 문서는 해당 실행 기반에 의존하지만 그 계약을 재정의하거나 대화 기능과 패키징 기능을 하나의 요구사항 정체성으로 합치지 않는다.
## 5. 요구사항
시스템은 입력 제출 시 입력 양끝의 공백을 제거한 값을 처리 대상으로 사용해야 한다. 정규화 결과가 비어 있지 않을 때만 사용자 메시지를 세션 상태와 화면에 기록해야 하며, 빈 문자열이나 공백·탭·줄바꿈만 포함한 입력은 응답 선택과 메시지 추가 없이 무시해야 한다. 이 동작은 입력 위젯의 제출 이벤트, 대화 표시 흐름 및 관련 경계 테스트에 영향을 준다.
유효한 입력마다 정본으로 정의된 다섯 개 응답 목록 전체에서 표준 난수 방식으로 응답 하나를 선택해야 한다. 정규화된 사용자 메시지를 사용자 역할로 먼저 추가하고 선택된 응답을 에이전트 역할로 바로 뒤에 추가해야 하며, 여러 입력을 제출해도 기존 항목을 삭제하거나 재배열하지 않고 제출 순서대로 누적해야 한다. 이 동작은 세션 대화 상태, 대화 영역의 표시 순서, 응답 선택 기록 및 회귀 테스트에 영향을 준다.
유효하거나 무효인 제출이 발생하면 입력창을 빈 문자열로 초기화해야 한다. 입력창 초기화는 메시지 처리의 성공 여부와 독립적으로 수행되어야 하며, 초기화 뒤 다음 입력을 받을 수 있어야 한다. 이미 기록된 메시지와 응답은 입력창 초기화로 삭제되거나 변경되지 않아야 한다.
각 대화 메시지는 사용자 또는 에이전트 역할을 명시적으로 보유해야 한다. 사용자 메시지는 사용자 라벨과 사용자용 구조적 스타일로, 고정 응답은 에이전트 라벨과 에이전트용 구조적 스타일로 표시해야 한다. 라벨과 스타일은 본문 문자열이나 색상만으로 결정하지 않고 메시지 역할에서 일관되게 파생해야 한다.
사용자가 Escape 또는 Ctrl+Q를 입력하면 현재 Textual 애플리케이션의 동일한 종료 동작을 요청해야 한다. 종료 입력은 메시지 제출이나 응답 선택으로 해석되어서는 안 되며, 종료 과정에서 외부 API, 네트워크 서비스, 데이터베이스 또는 영속 저장소를 호출하지 않아야 한다.
대화 처리는 선행 프로젝트 스캐폴드 요구사항이 제공하는 설치 및 실행 환경에서 동작해야 한다. 프로젝트 구조, 패키징 메타데이터와 실행 진입점은 선행 요구사항에서만 관리하고, 입력 정규화와 대화 누적, 고정 응답 선택과 표시, 빈 입력 처리, 입력창 초기화 및 종료 동작은 이 문서에서만 정본으로 관리하여 두 문서 사이의 기능 규칙과 인수 기준 중복을 방지해야 한다.
### REQ-002.01 — 입력 정규화와 유효 입력 판정
시스템은 입력창에 제출된 문자열의 양끝 공백을 제거한 값을 정규화된 입력으로 사용해야 한다. 정규화 결과가 비어 있지 않으면 이를 유효한 사용자 메시지로 처리하고, 빈 문자열이나 공백·탭·줄바꿈만 포함한 입력은 유효한 메시지로 처리하지 않아야 한다. 무효 입력에서는 응답을 선택하거나 대화 항목을 추가하지 않아야 하며, 문자열 내부의 공백은 변경하지 않아야 한다.
**영향 범위**
입력 위젯의 제출 이벤트, 대화 상태와 화면 표시 흐름에 영향을 준다. 영속성은 없으며 메시지는 실행 중인 세션 메모리에만 유지한다. 후속 사용 주체는 TUI 사용자와 대화 상태를 검증하는 자동 테스트다. 마이그레이션은 필요하지 않으며, REQ-001이 제공하는 Textual 실행 환경을 전제로 한다.
### REQ-002.02 — 유효 메시지와 고정 응답을 제출 순서대로 누적한다
유효한 입력이 제출되면 시스템은 정규화된 사용자 메시지를 사용자 역할로 먼저 대화 상태와 화면에 추가해야 한다. 이어서 다음 다섯 개의 고정 응답 목록 전체를 대상으로 표준 난수 선택을 한 번 수행하고, 선택된 응답을 에이전트 역할로 사용자 메시지 바로 뒤에 추가해야 한다: 안녕하세요! 무엇을 도와드릴까요?, 그것은 제가 처리하기 불가능합니다., 맡겨만 주세요., 지금 처리 중입니다., 작업을 완료하였습니다. 여러 입력을 제출하면 기존 항목을 삭제하거나 재배열하지 않고 각 메시지 쌍을 제출 순서대로 누적해야 한다.
**영향 범위**
세션 대화 상태, 대화 영역의 메시지 본문·역할·순서와 고정 응답 선택 흐름에 영향을 준다. 영속성은 없으며 사용자, Textual 렌더링 계층과 회귀 테스트가 후속 사용 주체다. 마이그레이션은 필요하지 않고 기존 고정 응답 문구와 표준 난수 선택 방식을 유지한다.
### REQ-002.03 — 제출 후 입력창을 초기화한다
입력 제출 이벤트가 발생하면 시스템은 제출된 값의 유효성이나 처리 결과와 관계없이 입력 위젯의 값을 빈 문자열로 초기화해야 한다. 입력창 초기화는 유효한 메시지 처리와 독립적으로 수행되어야 하며, 초기화가 끝난 뒤 사용자가 다음 입력을 제출할 수 있어야 한다.
**영향 범위**
Textual 입력 위젯의 값과 연속 입력 흐름에 영향을 준다. 영속성은 없으며 TUI 사용자와 입력 이벤트 테스트가 후속 사용 주체다. 마이그레이션은 필요하지 않고 유효·무효 입력 제출 후 입력창을 비우는 기존 동작을 유지한다.
### REQ-002.04 — 메시지 역할에 따라 라벨과 구조적 스타일을 표시한다
시스템은 각 대화 메시지에 사용자 또는 에이전트의 명시적인 역할을 보유하게 해야 한다. 사용자 메시지는 사용자 라벨과 사용자용 구조적 스타일로 표시하고, 고정 응답은 에이전트 라벨과 에이전트용 구조적 스타일로 표시해야 한다. 라벨과 스타일은 본문 문자열이나 색상만으로 결정하지 않고 메시지 역할에서 일관되게 파생해야 한다.
**영향 범위**
대화 영역의 메시지 모델, 표시 문자열, 역할별 식별자와 구조적 스타일에 영향을 준다. 영속성은 없으며 사용자, 화면 검수자와 렌더링 테스트가 후속 사용 주체다. 마이그레이션은 필요하지 않고 기존 사용자·에이전트 표시 순서와 역할 구분 의미를 유지한다.
### REQ-002.05 — 종료 단축키로 동일한 애플리케이션 종료를 요청한다
사용자가 Escape 또는 Ctrl+Q를 입력하면 시스템은 현재 Textual 애플리케이션의 종료 동작을 요청해야 한다. 두 단축키는 동일한 종료 경로를 사용해야 하며, 종료 입력은 메시지 제출이나 응답 선택으로 해석되어서는 안 된다. 종료 과정에서 외부 API, 네트워크 서비스, 데이터베이스 또는 영속 저장소를 호출하지 않아야 한다.
**영향 범위**
Textual 키 바인딩과 애플리케이션 생명 주기에 영향을 준다. 영속성은 없으며 사용자, 운영자와 종료 회귀 테스트가 후속 사용 주체다. 마이그레이션은 필요하지 않고 기존 Escape 및 Ctrl+Q 종료 방식을 유지한다. REQ-001의 선행 실행 계약이 충족되어야 한다.
## 6. 제약과 불변조건
- 입력은 제출 시 문자열의 양끝 공백을 제거한 값으로 정규화하며, 문자열 내부의 공백은 변경하지 않는다.
- 정규화 결과가 빈 문자열이면 사용자 메시지와 에이전트 응답을 추가하지 않고 고정 응답 선택도 수행하지 않는다.
- 유효한 입력 하나는 정규화된 사용자 메시지와 고정 응답 하나로 구성되며, 사용자 메시지가 먼저 기록되고 응답이 바로 뒤따라야 한다.
- 여러 입력의 메시지 쌍은 기존 항목을 삭제하거나 재배열하지 않고 제출 순서대로 누적한다.
- 고정 응답은 정본으로 정의된 다섯 문구 중 표준 난수 선택으로 하나를 결정한다.
- 유효 입력과 무효 입력 모두 제출이 끝나면 입력창을 빈 문자열로 초기화한다.
- 메시지의 역할, 라벨 및 구조적 스타일은 명시적인 역할 값에서 파생하며 본문 내용이나 색상만으로 역할을 판정하지 않는다.
- 사용자 메시지와 에이전트 응답은 각각 사용자 또는 에이전트 역할과 비어 있지 않은 본문을 함께 보유한다.
- Escape와 Ctrl+Q는 동일한 종료 요청 경로를 사용하며, 종료 입력은 메시지 제출이나 응답 선택으로 처리하지 않는다.
- 메시지와 응답은 현재 실행 세션의 메모리에만 보관하고 외부 API, 네트워크 서비스, 데이터베이스 또는 영속 저장소를 사용하지 않는다.
- 프로젝트의 설치, 패키징 및 실행 기반은 선행 프로젝트 스캐폴드 요구사항을 따르며 이 문서에서 그 계약을 재정의하지 않는다.
- 새로운 외부 연동, 의존성, 배포 방식 또는 실행 환경을 추가하지 않는다.
## 7. 인수 기준
- 양끝에 공백이 포함된 유효한 문자열을 제출하면 대화 상태와 화면에는 공백이 제거된 본문만 사용자 메시지로 기록되어야 한다. 검증은 Textual 테스트 앱에서 입력값과 저장된 메시지 및 표시값을 비교하는 방식으로 수행하며, 원본 양끝 공백이 후속 처리에 사용되지 않는지 확인한다.
- 빈 문자열과 공백, 탭, 줄바꿈만 포함한 값을 제출하면 메시지 목록, 응답 선택 기록 및 대화 영역 항목 수가 변하지 않아야 한다. 각 경계 입력을 제출하고 선택 함수가 호출되지 않았는지와 부분 메시지가 남지 않았는지를 확인한다.
- 유효한 입력마다 사용자 메시지 하나와 고정 응답 하나가 생성되고 두 항목이 인접한 순서로 표시되어야 한다. 선택 결과를 고정한 뒤 세션 메시지 목록과 화면의 전체 순서를 비교하여 사용자 역할과 에이전트 역할이 뒤바뀌지 않았는지 검증한다.
- 고정 응답 선택은 정본으로 정의된 다섯 문구 전체를 대상으로 유효 입력마다 한 번 수행되어야 한다. 선택 함수의 호출 인자와 호출 횟수를 감시하고, 반환된 값이 상태와 화면에 동일하게 반영되는지 확인한다.
- 두 개 이상의 유효 입력을 순서대로 제출하면 각 사용자 메시지와 응답 쌍이 기존 대화 뒤에 누적되어야 한다. 이전 항목의 삭제, 재배열, 중복 추가가 없는지 제출 기록과 메시지 목록을 함께 비교한다.
- 유효한 입력을 제출한 뒤 입력창 값은 빈 문자열이어야 하며 이미 기록된 메시지와 응답은 변경되지 않아야 한다. 양끝 공백을 포함한 입력을 사용하여 입력 위젯과 세션 상태를 함께 확인한다.
- 무효 입력을 제출한 뒤에도 입력창은 빈 문자열이어야 하며 기존 대화 상태와 화면은 그대로 유지되어야 한다. 빈 입력과 공백 입력을 각각 제출하여 입력창 초기화와 변경 없음 조건을 함께 검증한다.
- 사용자 메시지는 사용자 라벨과 사용자용 구조적 스타일을, 에이전트 응답은 에이전트 라벨과 에이전트용 구조적 스타일을 사용해야 한다. 렌더링 문자열, 역할 값, 스타일 식별자 및 역할별 구조 규칙을 서로 대조한다.
- Escape와 Ctrl+Q는 각각 애플리케이션 종료를 정확히 한 번 요청하고 메시지 제출이나 응답 선택을 발생시키지 않아야 한다. 두 키를 별도로 입력하여 동일한 종료 경로와 상태 불변조건을 확인한다.
- 대화 기능은 선행 프로젝트 스캐폴드의 설치 및 실행 환경에서 동작해야 하며, 패키징과 실행 진입점의 상세 기준을 중복 판정하지 않는다. 설치 환경을 준비한 뒤 대화 회귀 검증을 수행한다.
- 실행하지 않은 검증은 성공 결과나 특정 커밋의 실행 증거로 기록하지 않으며, 테스트 정의와 실제 실행 결과를 별도로 관리한다.
### REQ-002.01-AC01 — 양끝 공백이 제거된 입력을 처리한다
**판정 기준**
양끝에 공백이 포함된 유효한 입력을 제출했을 때 대화 상태와 화면에 공백이 제거된 본문만 사용자 메시지로 기록되면 통과로 판정한다.
**검증**
Textual 테스트 앱에서 양끝 공백이 포함된 입력을 제출하고 제출 기록, 메시지 본문과 화면 표시값을 비교한다. 문자열 내부 공백은 그대로 유지되는지도 확인한다.
**검증 분류**
integration
**상태 불변조건**
성공 후 사용자 메시지에는 정규화된 본문만 존재해야 하며 원본 양끝 공백은 화면이나 후속 응답 처리에 사용되지 않아야 한다.
### REQ-002.01-AC02 — 빈 입력과 공백 입력을 무시한다
**판정 기준**
빈 문자열 또는 공백·탭·줄바꿈만 포함한 입력을 제출해도 응답 선택, 메시지 추가와 새 화면 항목 생성이 발생하지 않으면 통과로 판정한다.
**검증**
경계 입력을 각각 제출하고 메시지 목록, 응답 선택 기록과 대화 영역 항목 수를 비교한다. 응답 선택 함수가 호출되지 않았는지도 확인한다.
**검증 분류**
integration
**상태 불변조건**
무효 입력 처리 후 기존 대화 상태와 화면은 변경되지 않아야 하며 부분적인 사용자 메시지나 응답이 남아서는 안 된다.
### REQ-002.02-AC01 — 사용자 메시지 뒤에 고정 응답을 표시한다
**판정 기준**
유효한 입력 하나를 제출했을 때 정규화된 사용자 메시지와 고정 목록에서 선택된 응답이 인접한 순서로 표시되고 각각 사용자와 에이전트 역할을 가지면 통과로 판정한다.
**검증**
응답 선택 결과를 고정한 Textual 테스트에서 입력을 제출하고 내부 메시지 목록과 대화 영역의 순서를 확인한다. 두 항목의 본문과 역할이 서로 바뀌거나 합쳐지지 않았는지도 검증한다.
**검증 분류**
integration
**상태 불변조건**
사용자 메시지는 대응하는 응답보다 먼저 존재해야 하며 각 메시지의 역할과 본문은 독립적으로 보존되어야 한다.
### REQ-002.02-AC02 — 고정 목록에서 한 번 응답을 선택한다
**판정 기준**
유효한 입력마다 선언된 다섯 개 응답 전체를 대상으로 표준 난수 선택이 한 번 수행되고 선택 결과가 상태와 화면에 동일하게 반영되면 통과로 판정한다.
**검증**
난수 선택 함수를 감시하거나 결과를 고정하여 유효 입력을 제출하고 호출 인자와 표시된 응답을 비교한다. 무효 입력에서 선택이 발생하지 않는지도 확인한다.
**검증 분류**
unit
**상태 불변조건**
응답은 정본 고정 목록 밖의 값이 될 수 없으며, 무효 입력에서는 선택 상태가 변경되지 않아야 한다.
### REQ-002.02-AC03 — 여러 제출을 기존 순서 뒤에 누적한다
**판정 기준**
서로 다른 유효 입력을 두 번 이상 제출했을 때 각 입력과 대응 응답 쌍이 제출 순서와 동일하게 상태와 화면에 누적되면 통과로 판정한다.
**검증**
두 개 이상의 입력을 순서대로 제출하고 제출 기록, 선택 응답, 메시지 목록과 렌더링 순서를 비교한다.
**검증 분류**
integration
**상태 불변조건**
새 제출은 기존 메시지를 삭제하거나 재정렬하지 않아야 하며 유효한 입력마다 사용자 메시지 하나와 응답 하나만 추가되어야 한다.
### REQ-002.03-AC01 — 유효 입력 제출 후 입력창을 비운다
**판정 기준**
양끝 공백을 포함한 유효한 입력을 제출한 직후 입력 위젯의 값이 빈 문자열이고 정규화된 메시지와 응답은 정상적으로 보존되면 통과로 판정한다.
**검증**
입력 위젯에 유효값을 설정하고 Enter를 누른 뒤 위젯 값, 메시지 목록과 대화 영역을 확인한다.
**검증 분류**
integration
**상태 불변조건**
입력창 초기화는 이미 기록된 사용자 메시지와 응답을 삭제하거나 변경해서는 안 된다.
### REQ-002.03-AC02 — 무효 입력 제출 후에도 입력창을 비운다
**판정 기준**
빈 값 또는 공백만 포함한 값을 제출한 뒤 입력 위젯의 값이 빈 문자열이고 대화 상태와 응답 선택 상태가 변경되지 않으면 통과로 판정한다.
**검증**
빈 입력과 공백 입력을 각각 제출하고 제출 후 입력 위젯 값, 메시지 목록, 응답 선택 기록과 화면 항목 수를 비교한다.
**검증 분류**
integration
**상태 불변조건**
입력창은 비워져야 하지만 기존 대화 상태와 표시 영역은 그대로 유지되어야 하며 새 항목이 추가되어서는 안 된다.
### REQ-002.04-AC01 — 역할에 맞는 라벨을 표시한다
**판정 기준**
사용자 메시지가 사용자 라벨을 사용하고 에이전트 응답이 에이전트 라벨을 사용하며 각 라벨이 해당 메시지 역할과 일치하면 통과로 판정한다.
**검증**
유효한 입력을 제출한 뒤 대화 영역의 두 표시 항목을 조회하여 렌더링 문자열과 내부 역할을 비교한다.
**검증 분류**
integration
**상태 불변조건**
메시지 역할과 표시 라벨은 항상 일치해야 하며 한 역할의 라벨이 다른 역할에 적용되어서는 안 된다.
### REQ-002.04-AC02 — 역할별 구조적 스타일을 유지한다
**판정 기준**
사용자와 에이전트 표시 항목이 서로 구별되는 구조적 식별자와 스타일을 사용하고 역할 구분이 색상에만 의존하지 않으면 통과로 판정한다.
**검증**
대화 영역의 표시 항목과 애플리케이션 스타일 규칙을 확인하여 역할별 식별자와 테두리 또는 정렬 차이가 존재하고 각 역할에 올바르게 적용되는지 검증한다.
**검증 분류**
integration
**상태 불변조건**
메시지 역할과 구조적 스타일은 서로 불일치해서는 안 되며 본문 내용만으로 스타일이 임의 변경되어서는 안 된다.
### REQ-002.05-AC01 — Escape 입력으로 종료를 요청한다
**판정 기준**
실행 중인 애플리케이션에서 Escape를 입력했을 때 종료 동작이 한 번 요청되고 메시지 제출이나 응답 선택이 발생하지 않으면 통과로 판정한다.
**검증**
종료 동작을 감시하는 Textual 테스트에서 Escape를 입력하고 종료 요청 횟수와 메시지·응답 상태의 변경 여부를 확인한다.
**검증 분류**
end-to-end
**상태 불변조건**
Escape 입력은 새 메시지나 응답을 생성하거나 입력 제출로 처리되어서는 안 된다.
### REQ-002.05-AC02 — Ctrl+Q 입력으로 동일한 종료를 요청한다
**판정 기준**
실행 중인 애플리케이션에서 Ctrl+Q를 입력했을 때 Escape와 동일한 종료 경로가 한 번 요청되면 통과로 판정한다.
**검증**
Ctrl+Q를 입력하고 Escape 검증과 동일한 종료 요청 횟수, 호출 경로와 상태 불변조건을 비교한다.
**검증 분류**
end-to-end
**상태 불변조건**
Ctrl+Q는 Escape와 다른 메시지 처리 경로를 사용해서는 안 되며 종료 시 대화 상태와 입력 처리 기록을 변경해서는 안 된다.
## 8. 오류 및 충돌 계약
- 입력 위젯이나 제출 이벤트를 사용할 수 없는 초기화 실패는 정상적인 대화 처리로 판정하지 않으며, 부분적으로 생성된 메시지를 성공 결과로 남기지 않는다.
- 빈 문자열 또는 공백만 포함한 입력은 오류가 아니라 무시 가능한 입력으로 처리하고, 응답 선택과 메시지 추가를 수행하지 않는다.
- 응답 선택에 실패하면 해당 제출의 사용자 메시지와 에이전트 응답을 모두 완전한 쌍으로 기록하지 않으며, 앞서 성공한 세션 메시지는 변경하지 않는다.
- 고정 응답 목록이 비어 있거나 정본과 다르면 응답을 생성하지 않고 처리 실패로 식별한다.
- 메시지 렌더링에 실패하면 화면에 일부 항목만 남은 상태를 완전한 처리 성공으로 간주하지 않는다.
- 입력창 초기화에 실패하면 제출을 완전한 성공으로 판정하지 않으며, 이전 입력이 다음 제출에 재사용되지 않도록 실패 원인을 확인할 수 있어야 한다.
- 동일한 제출 이벤트를 자동으로 중복 처리하지 않으며, 단일 Textual 이벤트 루프의 처리 순서에 따라 한 번만 반영한다.
- 이미 종료된 애플리케이션에 대한 종료 요청은 새 메시지나 응답을 추가하거나 오류 상태를 정상 처리로 바꾸지 않는다.
- 외부 API, 네트워크, 파일 저장소 또는 데이터베이스 오류를 대화 처리를 위한 대체 경로로 사용하지 않는다.
- 선행 프로젝트 스캐폴드의 설치 또는 실행 조건을 충족하지 못한 상태에서는 대화 기능이 정상 제공된 것으로 판정하지 않는다.
- 프로젝트 구조와 실행 기반에 관한 충돌은 선행 스캐폴드 요구사항을 따르고, 입력·응답·표시·입력창·종료 동작에 관한 충돌은 이 문서의 대화 상호작용 계약을 따른다.
- 두 요구사항 문서는 서로의 범위를 임의로 확장하지 않으며 동일한 기능 규칙과 인수 조건을 중복 정의하지 않는다.
## 9. 데이터 및 인터페이스 계약
- 입력 인터페이스는 대화 입력 위젯에서 Enter로 제출되는 문자열 값이며, 제출 시 양끝 공백을 제거한다. 문자열 내부의 공백은 보존한다.
- 정규화된 입력이 비어 있지 않으면 사용자 메시지의 본문으로 사용하고, 비어 있으면 메시지 본문으로 저장하지 않는다.
- 고정 응답 목록은 다음 다섯 문구를 정본으로 사용한다: "안녕하세요! 무엇을 도와드릴까요?", "그것은 제가 처리하기 불가능합니다.", "맡겨만 주세요.", "지금 처리 중입니다.", "작업을 완료하였습니다.".
- 응답 선택은 정본 고정 응답 목록 전체를 표준 난수 선택 기능에 전달하여 수행하며, 유효한 입력마다 한 번만 실행한다.
- 세션 메시지는 사용자 또는 에이전트 역할과 비어 있지 않은 본문을 함께 보유한다. 유효한 제출 하나는 사용자 메시지와 대응하는 에이전트 응답의 두 항목으로 구성된다.
- 사용자 메시지는 사용자 라벨과 사용자용 구조적 스타일로, 에이전트 응답은 에이전트 라벨과 에이전트용 구조적 스타일로 표시한다. 표시 정보는 메시지 역할에서 파생한다.
- 메시지 쌍은 사용자 메시지 다음에 에이전트 응답이 오는 인접 순서를 가지며, 여러 제출의 쌍은 세션 메모리에서 제출 순서대로 유지한다.
- 무효 입력은 입력창을 빈 값으로 만들지만 메시지, 응답 선택 기록 및 대화 영역 항목을 생성하지 않는다.
- 유효하거나 무효인 제출 이후 입력창은 다시 입력 가능한 빈 상태가 된다.
- Escape와 Ctrl+Q는 별도의 데이터 값을 만들지 않고 동일한 애플리케이션 종료 동작을 요청하는 키 입력이다.
- 메시지와 선택된 응답은 현재 프로세스의 세션 메모리에만 보관하며 종료 후 재조회하거나 복구하는 인터페이스는 제공하지 않는다.
- 외부 API, LLM, 네트워크 서비스, 데이터베이스 및 영속 저장소 인터페이스는 제공하지 않는다.
- 프로젝트 패키지 위치, 패키징 메타데이터 및 실행 진입점은 선행 프로젝트 스캐폴드 요구사항을 참조하며 이 문서에서 상세 계약을 재정의하지 않는다.
## 10. 하위 호환성
- 선행 프로젝트 스캐폴드 요구사항에 정의된 설치 방법, Python 지원 조건, 패키지 이름, Textual 런타임 의존성 및 실행 진입점은 변경하지 않는다.
- 기존 Textual 화면 진입과 입력 위젯의 역할을 유지하며, 대화 기능을 분리해도 사용자는 동일한 입력 방식과 키 조작으로 대화를 시작할 수 있어야 한다.
- 입력 양끝 공백 제거, 유효 입력 판정, 메시지 누적, 고정 응답 선택과 표시, 빈 입력 무시, 입력창 초기화 및 종료 동작의 의미는 유지한다.
- 공백이 포함된 유효 입력은 정규화된 값만 대화에 기록하고, 공백만 포함한 입력은 대화 항목과 응답을 추가하지 않는다.
- 고정 응답 문구와 사용자 및 에이전트 표시 라벨은 기존 관찰 결과와 호환되도록 유지한다.
- 사용자 메시지와 에이전트 응답의 순서, 세션 내 누적 방식 및 역할 구분을 변경하지 않는다.
- Escape와 Ctrl+Q는 계속 사용할 수 있어야 하며 두 키는 동일한 종료 동작을 요청해야 한다.
- 제출 후 입력창을 비우는 동작은 유효 입력과 무효 입력 모두에서 유지한다.
- 세션 내 메모리 기반 메시지 관리는 유지하되 영속성, 외부 서비스 연동, 인증, 다중 세션 또는 메시지 복원을 새로 도입하지 않는다.
- 대화 기능을 사용하는 기존 호출자는 추가 마이그레이션 없이 동일한 입력과 키 조작으로 동일한 관찰 결과를 얻어야 한다.
- 이 문서는 대화 상호작용의 상세 규칙을 정본으로 관리하고, 선행 프로젝트 스캐폴드 요구사항의 프로젝트 구조·패키징·실행 진입점 계약을 복제하거나 변경하지 않는다.
- 테스트와 문서는 실제 애플리케이션이 제공하는 입력, 응답, 표시, 입력창 및 종료 동작과 일치해야 하며, 실행되지 않은 검증을 성공 결과로 기록하지 않는다.
- 새로운 외부 의존성, 배포 방식, 실행 환경 변경 또는 데이터 마이그레이션은 필요하지 않다.
## 11. 검증 계획
Python 3.11 이상 환경에서 프로젝트 설치와 실행 기반이 선행 조건을 충족하는지 확인한 뒤 대화 상호작용을 검증한다. 정상 입력에서는 양끝 공백이 제거된 사용자 메시지와 고정 응답이 제출 순서대로 대화 영역에 표시되는지 확인한다. 여러 입력을 연속 제출하여 각 사용자 메시지와 에이전트 응답이 인접한 쌍으로 기존 순서 뒤에 누적되는지 검증한다.
빈 문자열과 공백·탭·줄바꿈만 포함한 입력에서는 응답 선택, 메시지 누적 및 새 화면 항목 추가가 발생하지 않고 입력창만 초기화되는지 확인한다. 응답 선택은 정본으로 정의된 고정 응답 목록에서 표준 난수 방식으로 수행되는지 확인하고, 선택된 값이 에이전트 메시지로 표시되는지 검증한다. 사용자와 에이전트의 라벨 및 구조적 스타일이 메시지 역할과 일치하는지도 확인한다.
Escape와 Ctrl+Q 입력은 동일한 애플리케이션 종료 동작을 요청하며, 종료 입력이 메시지 제출이나 응답 선택으로 처리되지 않는지 검증한다. 검증 정의와 실행 결과는 별도 테스트 카탈로그와 증거 산출물에서 관리한다. 실행되지 않은 검증은 성공으로 기록하지 않으며, 현재 문서 작성 시점에는 실행 커밋과 실행 시각을 미기록 상태로 둔다.
### 테스트 추적성 계약
테스트 정의:
```text
Test ID: TC-002-001
Verifies:
- REQ-002.01-AC01
- REQ-002.01-AC02
- REQ-002.02-AC01
- REQ-002.02-AC02
- REQ-002.02-AC03
- REQ-002.03-AC01
- REQ-002.03-AC02
- REQ-002.04-AC01
- REQ-002.04-AC02
- REQ-002.05-AC01
- REQ-002.05-AC02
Test reference: tests/test_app.py 및 python -m unittest discover -s tests -v
Automated: 저장소의 기존 unittest 기반 Textual 애플리케이션 테스트로 입력 정규화, 응답 선택, 메시지 순서, 빈 입력, 역할별 표시, 입력창 초기화 및 종료 동작을 자동 검증한다.
```
실행 증거:
```text
Test ID: TC-002-001
Requirement revision: REQ-002@0.1
Commit: 검증 커밋 미기록
Environment: Python 3.11 이상 및 선언된 Textual 런타임 의존성이 설치된 환경
Result: 미실행
Executed at: 검증 실행 시각 미기록
```
## 12. 추적성
근거는 원본 Gitea 이슈 #8인 요구사항명세서 재작업 요청([Gitea 이슈 #8](https://gitea.jsfamily.quest/genichin/gitea-agent-testbed/issues/8)), 기존 프로젝트 스캐폴드 요구사항 문서, 저장소의 사용자 동작 설명, 애플리케이션 구현 및 대화 동작 테스트다. 이 문서는 프로젝트 스캐폴드와 실행 진입점을 선행 조건으로 사용하며, 그 패키징과 실행 계약을 변경하거나 다시 정의하지 않는다.
입력 양끝 공백 제거, 유효 입력 판정, 세션 내 메시지 누적, 고정 응답 선택과 표시, 빈 입력 무시, 입력창 초기화 및 종료 동작은 이 문서에서 추적한다. 프로젝트 구조, 패키징 메타데이터와 실행 진입점은 선행 요구사항 문서에서 추적하므로 이 문서에서는 상세 규칙과 인수 기준을 중복 정의하지 않는다.
두 요구사항 문서는 설치 및 실행 기반과 대화 동작의 선행 관계를 공유하되 동일한 기능 규칙이나 인수 기준을 중복 기록하지 않는다. 대화 동작의 검증 결과는 실제 실행된 검증 기록이 있을 때만 별도 증거에 연결한다. 기존 프로젝트 스캐폴드 요구사항의 제목, 적용 범위, 승인 상태와 승인 이력은 이 문서의 작성으로 변경하지 않는다.
## 13. 확정된 결정
원본 이슈 #8의 분리 범위에 따라 기존 구현과 테스트가 정의한 대화 동작을 독립된 요구사항 문서에서 관리한다. 입력은 제출 시 양끝 공백을 제거하고, 정규화 결과가 비어 있으면 대화 상태와 응답 선택을 변경하지 않는다.
유효한 입력마다 사용자 메시지를 먼저 누적하고, 정본으로 정의된 고정 응답 목록에서 하나를 표준 난수 방식으로 선택하여 에이전트 메시지로 바로 뒤에 누적한다. 여러 제출은 기존 메시지를 삭제하거나 재배열하지 않고 제출 순서대로 유지한다.
사용자와 에이전트 메시지는 명시적인 역할에서 파생되는 라벨과 구조적 스타일로 구분한다. 유효 입력과 무효 입력 모두 제출 후 입력창을 비우며, Escape와 Ctrl+Q는 동일한 애플리케이션 종료 동작을 요청한다. 외부 API, LLM, 데이터베이스, 영속 저장소, 새 의존성 및 사용자 동작 변경은 도입하지 않는다. 실행되지 않은 검증은 특정 커밋의 실행 증거나 성공 결과로 기록하지 않는다.
## 14. 미결 사항
없음
## 15. 승인 기록
상태는 draft이며 아직 최종 승인되지 않았다. 승인 주체와 승인 근거는 확정되지 않았다.
이 문서는 원본 이슈 #8의 디스커버리 결과에 따라 대화 상호작용을 독립적으로 추적하기 위해 작성되었다. 최종 승인 전에는 입력 정규화, 메시지 누적, 고정 응답 선택과 표시, 빈 입력 처리, 입력창 초기화, 종료 동작 및 선행 프로젝트 스캐폴드와의 의존 관계를 검토해야 한다. 실제로 실행되지 않은 검증은 승인 근거나 실행 증거로 기록하지 않는다.
+42 -28
View File
@@ -1,39 +1,53 @@
<!-- gitea-agent:documentation-template/runtime-flow/v1 -->
# 런타임 흐름 # 런타임 흐름
## 정상 입력 시나리오 ## 목적과 범위
1. 애플리케이션이 `#user-input`에 포커스를 둔다. 이 문서는 사용자가 입력을 제출한 시점부터 사용자 메시지와 에이전트 응답을 역할별로 생성하고 화면에 렌더링하는 실행 범위를 정의한다. 입력은 앞뒤 공백을 제거한 뒤 처리하며, 유효한 입력마다 고정 응답 목록에서 하나의 응답을 선택한다. 외부 API, LLM, 인증, 영속 저장 및 대화 기록 관리는 범위에 포함하지 않는다.
2. 사용자가 입력하고 Enter를 누른다.
3. `Input.Submitted` 이벤트에서 입력값을 `strip()`한다.
4. 입력창 값을 즉시 빈 문자열로 초기화한다.
5. `trim` 결과가 유효하면 사용자 메시지를 세션 상태에 기록한다.
6. `random.choice(RESPONSE_MESSAGES)`가 고정 응답 하나를 선택한다.
7. 선택 응답을 세션 상태에 기록한다.
8. 대화 영역에 사용자 메시지 `Label`을 추가한다.
9. 사용자 메시지 바로 뒤에 응답 `Label`을 추가한다.
10. 대화 영역을 마지막 항목으로 스크롤한다.
여러 번 제출하면 각 사용자 메시지와 응답 쌍이 제출 순서대로 누적된다. ## 흐름
## 공백 입력 시나리오 1. 입력창에서 제출 이벤트가 발생하면 입력값의 앞뒤 공백을 제거하고 입력창을 즉시 비운다.
2. 정규화된 값이 비어 있으면 후속 처리를 중단한다.
3. 값이 유효하면 고정된 응답 목록에서 표준 무작위 선택으로 에이전트 응답을 결정한다.
4. 정규화된 입력을 사용자 역할의 메시지로 만들고, 선택된 응답을 에이전트 역할의 메시지로 만든다.
5. 두 메시지를 제출 순서대로 메모리 목록에 추가한다. 한 번의 제출에서는 사용자 메시지가 먼저이고 에이전트 응답이 바로 뒤따른다.
6. 각 메시지의 역할에서 화면 라벨과 구조적 스타일 클래스를 파생한다. 사용자 메시지는 사용자 라벨과 왼쪽 정렬·왼쪽 테두리를 사용하고, 에이전트 응답은 에이전트 라벨과 오른쪽 정렬·오른쪽 테두리를 사용한다.
7. 대화 영역에 두 항목을 순서대로 마운트하고 마지막 항목이 보이도록 스크롤한다.
8. 종료 키 입력은 기존 종료 동작을 요청하며, 메시지 제출 흐름과 독립적으로 처리한다.
1. 사용자가 공백만 포함한 값을 제출한다. ### `message-submission-and-rendering`
2. 입력값을 `strip()`한다.
3. 결과가 빈 문자열임을 확인한다.
4. 입력창은 비우되 상태, 난수 선택 결과 및 대화 영역은 변경하지 않는다.
## 종료 시나리오 입력창의 제출 이벤트는 실행 흐름의 시작점이다. 이벤트 값을 앞뒤 공백 제거로 정규화하고 입력창을 비운 다음, 정규화된 결과가 비어 있는지 판정한다. 빈 결과는 정상적으로 무시하며 응답 선택이나 상태 변경을 수행하지 않는다.
- `Escape`를 누르면 기존 종료 바인딩이 실행된다. 유효한 입력이면 고정된 응답 목록을 표준 무작위 선택 함수에 전달하여 에이전트 응답을 하나 결정한다. 이후 정규화된 입력과 선택된 응답을 각각 사용자 역할과 에이전트 역할의 불변 메시지로 만든다. 제출 입력과 선택 응답의 기존 세션 기록도 함께 갱신하되, 응답 선택은 유효한 입력마다 한 번만 수행한다.
- `Ctrl+Q`를 누르면 기존 종료 바인딩이 실행된다.
- 두 동작 모두 `quit` 액션을 통해 애플리케이션 종료를 요청한다.
## 검증 시나리오 생성된 메시지는 사용자 메시지, 에이전트 응답 순서로 메시지 목록에 연속 추가한다. 여러 번 제출한 경우에도 각 제출의 두 항목이 제출 순서에 따라 누적되며, 이전 항목의 내용과 역할은 변경하지 않는다. 메시지의 역할과 본문은 화면 표현보다 우선하는 정본 값이다.
자동 테스트는 결정 가능한 `random.choice` 결과를 사용해 다음을 확인한다. 렌더링 단계에서는 역할에서 표시 라벨과 구조적 스타일을 파생한다. 사용자 메시지는 `사용자: 본문` 형식과 사용자용 메시지 클래스를 사용하고, 에이전트 응답은 `에이전트: 본문` 형식과 응답용 메시지 클래스를 사용한다. 사용자 항목은 왼쪽 정렬과 왼쪽 테두리, 에이전트 항목은 오른쪽 정렬과 오른쪽 테두리를 적용하여 색상에만 의존하지 않고 구분한다.
- 입력 `trim` 결과가 사용자 메시지로 저장되고 표시되는지 각 항목은 대화 영역에 생성 순서대로 추가하고, 두 항목을 모두 추가한 뒤 대화 영역의 끝으로 스크롤한다. 따라서 유효한 제출 하나의 화면 결과는 사용자 메시지가 먼저이고 대응하는 에이전트 응답이 바로 뒤따르는 형태가 된다. 종료 키 처리는 이 흐름과 별도로 애플리케이션 종료를 요청하며, 입력·응답 상태를 새로 만들지 않는다.
- 사용자 메시지 다음에 응답이 표시되는지
- 공백 입력에서 `random.choice`가 호출되지 않는지 검증은 정규화된 입력과 응답 기록, 역할과 본문을 포함한 메시지 목록, 화면 라벨, 역할별 구조적 스타일 및 표시 순서를 함께 비교하여 수행한다. 빈 입력과 공백 입력에서는 응답 선택과 모든 메시지·화면 변경이 발생하지 않는지 확인하고, 여러 입력에서는 기존 항목을 보존한 채 사용자와 에이전트 쌍이 올바른 순서로 누적되는지 확인한다.
- 입력창이 제출 후 비워지는지
- 기존 종료 동작이 유지되는지 ## 실패와 복구
- 빈 문자열이나 공백·탭·개행만 포함한 입력은 입력창을 비운 뒤 무시한다.
- 무시된 입력에서는 응답 선택, 제출 기록 추가, 메시지 생성 및 화면 항목 추가가 발생하지 않는다.
- 응답 선택이나 메시지 기록 전에 입력 유효성을 확인하므로 부분적인 사용자 메시지 또는 에이전트 응답이 남지 않는다.
- 단일 이벤트 루프에서 제출 이벤트 순서를 처리하며, 처리된 제출의 표시 순서를 자동으로 재배열하지 않는다.
- 렌더링 실패가 영속 데이터나 외부 시스템 상태를 변경하지 않도록 별도 저장·보상 절차를 두지 않는다.
- 역할 값은 명시적인 역할 모델에서 관리하며 화면 위치나 스타일 클래스만으로 작성자를 추론하지 않는다.
## 관찰 가능성
실행 중 확인할 수 있는 상태는 정규화된 제출 입력 목록, 선택된 응답 목록, 역할과 본문을 함께 가진 메시지 목록 및 대화 영역의 표시 항목이다. 각 유효한 제출에 대해 입력과 응답이 같은 순서로 기록되며, 메시지 목록에는 역할이 명시된다. 화면 텍스트에는 사용자 또는 에이전트 라벨이 포함되고, 구조적 스타일은 역할별 정렬과 테두리로 확인할 수 있다. 민감정보, 외부 호출 식별자 및 영속 저장 상태는 생성하지 않는다.
## 검증
- 정상 입력 하나와 여러 개를 제출하여 입력 정규화, 응답 선택, 역할 부여, 라벨 표시 및 전체 순서를 확인한다.
- 응답 선택을 고정하여 동일한 입력 흐름을 결정적으로 검증하고, 표준 응답 선택 호출이 기존 목록을 대상으로 한 번 수행되는지 확인한다.
- 빈 입력과 공백 입력을 제출하여 선택 함수가 호출되지 않고 메시지 목록과 화면 항목이 변경되지 않는지 확인한다.
- 사용자와 에이전트 항목의 라벨, 역할 클래스, 좌우 정렬 및 양쪽 테두리 규칙을 확인한다.
- 종료 키 입력이 애플리케이션 종료 요청으로 연결되는지 회귀 검증한다.
- 입력창이 모든 제출 뒤 비워지고, 유효한 제출의 사용자 메시지 뒤에 해당 에이전트 응답이 즉시 표시되는지 확인한다.
+65 -3
View File
@@ -1,5 +1,7 @@
"""사용자 입력과 고정 무작위 응답을 표시하는 Textual 애플리케이션.""" """사용자 입력과 고정 무작위 응답을 작성자 역할과 함께 표시하는 Textual 애플리케이션."""
from dataclasses import dataclass
from enum import Enum
import random import random
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
@@ -16,6 +18,29 @@ RESPONSE_MESSAGES = (
) )
class MessageRole(str, Enum):
"""대화 메시지를 작성한 주체의 명시적인 역할."""
USER = "user"
AGENT = "agent"
@property
def label(self) -> str:
"""화면에 표시할 작성자 라벨을 반환한다."""
return {
MessageRole.USER: "사용자",
MessageRole.AGENT: "에이전트",
}[self]
@dataclass(frozen=True)
class ChatMessage:
"""작성자 역할과 본문을 함께 보유하는 대화 메시지."""
role: MessageRole
content: str
class ChatApp(App[None]): class ChatApp(App[None]):
"""사용자 메시지 뒤에 고정 목록에서 선택한 응답을 표시하는 TUI.""" """사용자 메시지 뒤에 고정 목록에서 선택한 응답을 표시하는 TUI."""
@@ -30,6 +55,23 @@ class ChatApp(App[None]):
padding: 1; padding: 1;
} }
#conversation .chat-message {
width: 80%;
margin: 1 0;
padding: 1 2;
border: round $surface;
}
#conversation .user-message {
align: left middle;
border-left: thick $success;
}
#conversation .response-message {
align: right middle;
border-right: thick $warning;
}
#user-input { #user-input {
margin: 1 0; margin: 1 0;
} }
@@ -44,6 +86,7 @@ class ChatApp(App[None]):
super().__init__() super().__init__()
self.submitted_inputs: list[str] = [] self.submitted_inputs: list[str] = []
self.selected_responses: list[str] = [] self.selected_responses: list[str] = []
self.messages: list[ChatMessage] = []
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=False) yield Header(show_clock=False)
@@ -68,11 +111,30 @@ class ChatApp(App[None]):
self.submitted_inputs.append(value) self.submitted_inputs.append(value)
self.selected_responses.append(response) self.selected_responses.append(response)
user_message = ChatMessage(MessageRole.USER, value)
agent_message = ChatMessage(MessageRole.AGENT, response)
self.messages.extend((user_message, agent_message))
conversation = self.query_one("#conversation", VerticalScroll) conversation = self.query_one("#conversation", VerticalScroll)
conversation.mount(Label(value, classes="user-message")) self._mount_message(conversation, user_message)
conversation.mount(Label(response, classes="response-message")) self._mount_message(conversation, agent_message)
conversation.scroll_end(animate=False) conversation.scroll_end(animate=False)
@staticmethod
def _mount_message(
conversation: VerticalScroll,
message: ChatMessage,
) -> None:
"""메시지 역할에서 라벨과 구조적 스타일을 일관되게 파생한다."""
style_class = {
MessageRole.USER: "user-message",
MessageRole.AGENT: "response-message",
}[message.role]
text = f"{message.role.label}: {message.content}"
conversation.mount(
Label(text, classes=f"chat-message {style_class} {message.role.value}-role")
)
def main() -> None: def main() -> None:
"""콘솔 스크립트의 실행 진입점.""" """콘솔 스크립트의 실행 진입점."""
+85 -7
View File
@@ -1,9 +1,14 @@
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from textual.widgets import Input, Label from textual.widgets import Input
from chat_tui.app import RESPONSE_MESSAGES, ChatApp from chat_tui.app import (
RESPONSE_MESSAGES,
ChatApp,
ChatMessage,
MessageRole,
)
class ChatAppTest(unittest.IsolatedAsyncioTestCase): class ChatAppTest(unittest.IsolatedAsyncioTestCase):
@@ -28,21 +33,91 @@ class ChatAppTest(unittest.IsolatedAsyncioTestCase):
app.selected_responses, app.selected_responses,
[RESPONSE_MESSAGES[2], RESPONSE_MESSAGES[2]], [RESPONSE_MESSAGES[2], RESPONSE_MESSAGES[2]],
) )
self.assertEqual(
app.messages,
[
ChatMessage(MessageRole.USER, "첫 번째 입력"),
ChatMessage(MessageRole.AGENT, RESPONSE_MESSAGES[2]),
ChatMessage(MessageRole.USER, "두 번째 입력"),
ChatMessage(MessageRole.AGENT, RESPONSE_MESSAGES[2]),
],
)
displayed = [ displayed = [
label.renderable str(label.renderable)
for label in app.query("#conversation Label") for label in app.query("#conversation Label")
] ]
self.assertEqual( self.assertEqual(
displayed, displayed,
[ [
"첫 번째 입력", "사용자: 첫 번째 입력",
RESPONSE_MESSAGES[2], f"에이전트: {RESPONSE_MESSAGES[2]}",
"두 번째 입력", "사용자: 두 번째 입력",
RESPONSE_MESSAGES[2], f"에이전트: {RESPONSE_MESSAGES[2]}",
], ],
) )
self.assertEqual(input_widget.value, "") self.assertEqual(input_widget.value, "")
async def test_messages_have_explicit_roles_labels_and_structural_styles(
self,
) -> None:
app = ChatApp()
with patch("chat_tui.app.random.choice", return_value=RESPONSE_MESSAGES[0]):
async with app.run_test() as pilot:
input_widget = app.query_one("#user-input", Input)
input_widget.value = "역할 확인"
await pilot.press("enter")
await pilot.pause()
labels = list(app.query("#conversation Label"))
self.assertEqual(len(labels), 2)
self.assertEqual(
labels[0].classes,
frozenset({"chat-message", "user-message", "user-role"}),
)
self.assertEqual(
labels[1].classes,
frozenset({"chat-message", "response-message", "agent-role"}),
)
self.assertEqual(
str(labels[0].renderable),
"사용자: 역할 확인",
)
self.assertEqual(
str(labels[1].renderable),
f"에이전트: {RESPONSE_MESSAGES[0]}",
)
self.assertIn("border-left", app.CSS)
self.assertIn("border-right", app.CSS)
self.assertIn("align: left middle", app.CSS)
self.assertIn("align: right middle", app.CSS)
async def test_rendering_styles_are_derived_from_message_roles(self) -> None:
app = ChatApp()
async with app.run_test() as pilot:
conversation = app.query_one("#conversation")
app._mount_message(
conversation,
ChatMessage(MessageRole.USER, "사용자 본문"),
)
app._mount_message(
conversation,
ChatMessage(MessageRole.AGENT, "에이전트 본문"),
)
await pilot.pause()
labels = list(app.query("#conversation Label"))
self.assertEqual(len(labels), 2)
self.assertEqual(
labels[0].classes,
frozenset({"chat-message", "user-message", "user-role"}),
)
self.assertEqual(
labels[1].classes,
frozenset({"chat-message", "response-message", "agent-role"}),
)
self.assertEqual(str(labels[0].renderable), "사용자: 사용자 본문")
self.assertEqual(str(labels[1].renderable), "에이전트: 에이전트 본문")
async def test_response_selection_uses_standard_random_choice(self) -> None: async def test_response_selection_uses_standard_random_choice(self) -> None:
app = ChatApp() app = ChatApp()
with patch( with patch(
@@ -68,7 +143,9 @@ class ChatAppTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(app.submitted_inputs, []) self.assertEqual(app.submitted_inputs, [])
self.assertEqual(app.selected_responses, []) self.assertEqual(app.selected_responses, [])
self.assertEqual(app.messages, [])
self.assertEqual(len(app.query("#conversation Label")), 0) self.assertEqual(len(app.query("#conversation Label")), 0)
self.assertEqual(input_widget.value, "")
choice_mock.assert_not_called() choice_mock.assert_not_called()
async def test_empty_input_is_not_added(self) -> None: async def test_empty_input_is_not_added(self) -> None:
@@ -79,6 +156,7 @@ class ChatAppTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(app.submitted_inputs, []) self.assertEqual(app.submitted_inputs, [])
self.assertEqual(app.selected_responses, []) self.assertEqual(app.selected_responses, [])
self.assertEqual(app.messages, [])
self.assertEqual(len(app.query("#conversation Label")), 0) self.assertEqual(len(app.query("#conversation Label")), 0)
async def test_escape_requests_application_exit(self) -> None: async def test_escape_requests_application_exit(self) -> None: