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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ Pub/Sub 신호는 재조회 계기이며, 내구성 있는 메시지 큐로 사
아래는 GCP 접근 권한 없이 로컬 PostgreSQL·Valkey와 Local Filesystem 저장소로 핵심 기능을 확인하는 경로입니다.
명령은 저장소 루트에서 실행합니다.

### 통합 명령으로 시작·검증·종료

macOS·Linux에서 아래 사전 준비 도구와 Python 3.9 이상, Bash·curl·lsof·ps를 준비한 뒤 실행합니다.

```bash
./scripts/demo.py start
./scripts/demo.py verify
./scripts/demo.py stop
```

`start`는 없는 환경 파일만 만들고 PostgreSQL·Valkey·BGE-M3·백엔드·프런트엔드를 연결합니다.
Web은 `http://localhost:3000`이며, `verify`는 비밀번호를 입력받아 프런트엔드 API 프록시로 예제 15건의
인덱싱·기대 검색 3개·원본 해시 15개를 확인합니다. 반복 실행은 이전 원장과 문서 ID도 대조합니다.
준비 상태는 `./scripts/demo.py status`로 다시 확인합니다.

기본 범위 `search`는 Ollama가 필요 없습니다. 실제 생성 답변·인용까지 검증하려면 Native Ollama와 지정
모델을 먼저 준비하고 `start --scope rag`를 사용합니다. Native Ollama는 이 명령이 자동 종료하지 않습니다.
기존 개인 환경 파일은 덮어쓰지 않으며, 원격 OpenSQL·GCS나 지원하지 않는 설정이면 자동 변경 없이 거부합니다.
`stop`은 이번 실행이 시작한 자원만 중지하고 DB·원본·모델 캐시를 보존합니다. 다른 작업공간의 고정 이름
컨테이너가 있으면 별도 Docker 환경을 사용하세요. 상세 경계는 [통합 명령 설계](docs/design/local-demo-lifecycle.md)를 참고하세요.

아래 개별 실행 절차도 계속 사용할 수 있습니다.

### 1. 사전 준비

- Git
Expand Down
75 changes: 75 additions & 0 deletions docs/design/local-demo-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 로컬 시연 통합 명령

## 목적과 경계

`scripts/demo.py`가 기존 Compose·서비스 스크립트·Gradle·npm·예제 검증기를 연결한다.
업무 API나 인덱싱 구현을 새로 만들지 않는다. macOS·Linux의 POSIX 프로세스 그룹을 사용하며 Windows는 지원하지 않는다.
기본 범위는 search이고, Native Ollama가 이미 준비된 경우에만 start의 `--scope rag`를 선택한다.

## 사용자 명령

```bash
./scripts/demo.py start
./scripts/demo.py verify
./scripts/demo.py status
./scripts/demo.py stop
```

Python 3.9 이상, Java 17, Node.js 22.13 이상, Docker Compose, Git·Bash·curl·lsof·ps가 필요하다.
처음에는 없는 환경 파일만 예제에서 만들고 JWT 서명 키를 무작위 생성한다. 기존 파일은 수정하지 않는다.
node_modules가 없으면 npm ci를 연결하고, 이미 있으면 npm ls로 확인한다. 맞지 않는 기존 의존성은 자동 교체하지 않는다.

## 기동과 검증

```text
사용자 start
→ 명령 잠금 획득: 다른 start/verify/stop과 동시 변경 거부
→ LocalConfig: local·파일 저장소·로컬 DB·고정 API/Web 포트 확인
→ 시작 전 컨테이너 ID와 실행 상태, Docker Engine ID 기록
→ local-services.sh start --scope search
→ 전용 프로세스 그룹에서 Gradle bootRun
└ readiness UP + API/관리 포트의 실제 수신 프로세스 대조
→ 전용 그룹에서 npm run dev
└ 로그인 HTML + 직접/프록시 departments 결과 대조
→ READY 기록

사용자 verify
→ 설정·소스 fingerprint, 서버 소유권, 컨테이너 ID, DB 재확인
→ 프런트엔드 /api/backend 프록시로 기존 예제 검증기 실행
→ 업로드·INDEXED·기대 검색·다운로드 해시 판정
→ 같은 DB 컨텍스트의 이전 원장과 문서 ID 대조
→ 매 실행을 별도 디렉터리에 기록
```

search는 Ollama를 기동·필수 확인하지 않는다. 백엔드 자체 RAG Worker의 동작을 비활성화하는 설정은 아니며,
Ollama가 없으면 생성 답변은 실패 대체 경로를 사용한다. search 검증은 생성 답변을 미실행으로 표시한다.

## 종료·취소와 보존

```text
stop 또는 start 도중 취소
→ PID + 시작 시각 + 명령 fingerprint + 프로세스 그룹 대조
→ 소유 그룹 TERM → 확인된 잔존 자식만 필요시 KILL
→ 저장된 Docker endpoint·Engine·컨테이너 ID·Compose 작업공간 대조
→ 이번 실행이 시작한 서비스만 stop
→ API·관리·Web 포트 해제 확인
→ STOPPED 또는 NEEDS_ATTENTION 기록

처음부터 실행 중이던 Compose → 재사용 후 유지
Native Ollama → 외부 관리 서버로 유지
DB 볼륨·원본·모델 캐시·이전 실행 기록 → 보존
식별자가 다르거나 소유권 불명 → 자동 종료 거부, 조치 필요 표시
```

자식 supervisor가 먼저 PID 파일을 기록해 부모 상태 저장 중단에도 회수할 수 있게 한다.
기동 중 중단된 세션은 컨테이너 전후 상태도 다시 대조한다. 같은 PID만으로 종료하지 않는다.
READY 기록이 있어도 status는 실제 준비 상태를 다시 검사하며, 검증 실패 후에는 서버를 남겨 원인을 확인할 수 있다.

## 기록과 제한

- `.demo/launcher/sessions/<id>/`: 정제된 구성요소 로그·상태·이벤트. 운영 상태 JSON은 원자 교체한다.
- `.demo/reports/<id>/`: 예제 검증의 불변 실행별 원장. 반복 검증은 이전 로그를 덮어쓰지 않는다.
- 비밀값·이메일·URL은 supervisor에서 파일 기록 전에 마스킹한다. 환경 원문은 로그에 저장하지 않는다.
- 원격 OpenSQL·GCS나 다른 작업공간 Compose는 자동 수정하지 않고 거부한다.
- 프로세스 소유권·주소 검증은 로컬 시연 오작동 방지이며 악의적인 로컬 관리자에 대한 보안 격리는 아니다.
- 실제 모델·새 Docker 환경의 E2E는 별도 실행 기록으로 확인한다. 이 설계 자체는 실행 증거가 아니다.
42 changes: 42 additions & 0 deletions docs/test-results/demo-lifecycle-20261018/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 통합 시연 명령의 생명주기 검증

## 목적

시작·검증·종료를 묶으면서 다른 작업의 서버나 개인 환경 파일을 변경하지 않는지 확인한다.
실제 HTTP 자식 프로세스와 포트로 종료 계약을 검사한다.

## 테스트 환경

- 로컬 Python 3.9.6·POSIX·lsof·ps. 최종 구현 `3a17668`, 검사 `3fd9eb7`.
- 임시 작업공간의 제어 HTTP 서버·sleep 자식은 실제 프로세스다.
- Docker 명령·일부 API 상태는 제어 대체물이다. 실제 Docker 기동·DB 연결·BGE-M3 추론은 0회다.
- 공개 서비스와 사용자 실행 자원은 사용하지 않았다.

## 테스트 방법

```bash
python3 -m unittest discover -s scripts/demo -p 'test_*.py' -v
```

전용 프로세스 그룹, 실제 listener 소유권, 자식 종료와 포트 해제, 잘못된 시작 정보의 종료 거부,
부모 상태가 누락된 PID 기록 회수, 동시 명령 거부, 부분 기동 실패 정리, 설정 변경 시 업로드 거부를 확인했다.
개인 환경 파일은 bytes와 수정 시각이 모두 유지되는지 검사했다. 원격 리다이렉트 거부와 여러 줄 비밀값 거부도 포함한다.

## 관측 결과

| 실행 ID | 목적 | 관측 | 판정·근거 |
| --- | --- | --- | --- |
| demo-runtime-contract-01 | 최초 소유권·생명주기 회귀 | 69/69, 6.904초 | 통과 · [로그](demo-runtime-contract-01.log) |
| demo-runtime-contract-02 | 관리 포트·중단 컨테이너 회수·실제 status 보완 | 73/73, 8.166초 | 통과 · [로그](demo-runtime-contract-02.log) |

HTTP 서버의 수신 프로세스와 그룹을 대조했고, 종료 후 자식 그룹과 실제 수신 포트가 사라졌다.
변경된 PID 식별정보에서는 서버가 계속 살아 있어 자동 종료를 거부함을 확인했다.

## 검증 과정에서 확인된 사항

- PID만 기록하면 재사용된 PID를 오인할 수 있어 시작 시각·명령 fingerprint·그룹을 함께 대조한다.
- 반복 verify는 별도 operation ID를 사용한다. 이전 완료 파일 때문에 다음 실행이 즉시 성공하는 것을 방지한다.
- API 성공만으로 준비를 판정하지 않고 실제 수신 그룹과 관리 포트도 검사한다.
- 원문 비밀번호·JWT·이메일을 제어 자식 출력에 넣어 로그에 남기 전에 마스킹되는지 검사했다.
- 이 검사는 실제 Gradle·프런트엔드·Compose 전체 기동이나 독립 환경의 모델 E2E를 증명하지 않는다.
- 각 검사 임시 프로세스·파일을 정리했다. Native Ollama 실제 기동·종료는 실행하지 않았다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
실행 ID: demo-runtime-contract-01
목적: 통합 실행의 소유권·잠금·실제 제어 프로세스 종료 회귀
환경: 로컬 Python 3.9.6·POSIX·lsof, 임시 HTTP 서버와 명령 대체물
합격 기준: 69개 검사 통과, 제어 서버 포트·자식 프로세스 정리, 실제 Docker 변경 없음
명령: python3 -m unittest discover -s scripts/demo -p test_*.py -v
시작 시각: 2026-10-18T14:56:51+09:00
test_changed_configuration_refuses_before_database_or_upload (test_demo_runtime.RuntimeContractTest) ... ok
test_changed_identity_never_kills_live_process (test_demo_runtime.RuntimeContractTest) ... ok
test_child_record_recovers_parent_missing_process_map (test_demo_runtime.RuntimeContractTest) ... ok
test_concurrent_command_lock_refuses_second_process (test_demo_runtime.RuntimeContractTest) ... ok
test_existing_personal_environment_files_are_preserved (test_demo_runtime.RuntimeContractTest) ... ok
test_frontend_proxy_comparison_ignores_response_timestamp (test_demo_runtime.RuntimeContractTest) ... ok
test_new_env_is_initialized_without_default_jwt (test_demo_runtime.RuntimeContractTest) ... ok
test_partial_start_failure_runs_cleanup (test_demo_runtime.RuntimeContractTest) ... ok
test_starting_component_records_actual_group_and_listener (test_demo_runtime.RuntimeContractTest) ... ok
test_stop_preserves_reused_containers (test_demo_runtime.RuntimeContractTest) ... ok
test_stop_releases_children_and_actual_server_port (test_demo_runtime.RuntimeContractTest) ... ok
test_supervisor_redacts_before_writing (test_demo_runtime.RuntimeContractTest) ... ok
test_verify_records_use_distinct_paths (test_demo_runtime.RuntimeContractTest) ... ok
test_changed_target_refuses_replay_before_upload (test_demo_verdict.DemoVerdictTest) ... ok
test_citation_outside_search_candidates_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_different_previous_document_id_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_download_hash_mismatch_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_empty_search_results_fail (test_demo_verdict.DemoVerdictTest) ... ok
test_event_redaction_happens_before_storage (test_demo_verdict.DemoVerdictTest) ... ok
test_existing_report_is_never_overwritten (test_demo_verdict.DemoVerdictTest) ... ok
test_full_search_scope_passes_without_rag_polling (test_demo_verdict.DemoVerdictTest) ... ok
test_invalid_candidate_is_not_a_valid_search (test_demo_verdict.DemoVerdictTest) ... ok
test_missing_expected_document_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_missing_query_id_is_not_a_valid_search (test_demo_verdict.DemoVerdictTest) ... ok
test_previous_document_ids_are_compared (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_failed_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_incomplete_is_not_success (test_demo_verdict.DemoVerdictTest) ... ok
test_rag_scope_requires_success_and_valid_citations (test_demo_verdict.DemoVerdictTest) ... ok
test_search_http_error_fails_after_all_documents_indexed (test_demo_verdict.DemoVerdictTest) ... ok
test_table_text_missing_fails (test_demo_verdict.DemoVerdictTest) ... ok
test_unrun_required_stages_cannot_pass (test_demo_verdict.DemoVerdictTest) ... ok
test_http_error_preserves_code_for_signup_decision (test_demo_verdict.HttpBoundaryTest) ... ok
test_oversized_original_is_rejected (test_demo_verdict.HttpBoundaryTest) ... ok
test_redirect_does_not_forward_credentials (test_demo_verdict.HttpBoundaryTest) ... ok
test_ambiguous_quotes_and_inline_comments_are_refused (test_local_config.LocalConfigTest) ... ok
test_container_from_another_checkout_is_refused (test_local_config.LocalConfigTest) ... ok
test_database_check_is_readonly_and_password_is_not_an_argument (test_local_config.LocalConfigTest) ... ok
test_defaults_match_local_quickstart (test_local_config.LocalConfigTest) ... ok
test_disabled_worker_is_refused (test_local_config.LocalConfigTest) ... ok
test_docker_context_takes_precedence_over_host (test_local_config.LocalConfigTest) ... ok
test_duplicate_key_is_refused (test_local_config.LocalConfigTest) ... ok
test_environment_overrides_file (test_local_config.LocalConfigTest) ... ok
test_foreign_container_is_refused_before_up (test_local_config.LocalConfigTest) ... ok
test_frontend_remote_or_different_api_is_refused (test_local_config.LocalConfigTest) ... ok
test_loopback_port_without_matching_container_is_refused (test_local_config.LocalConfigTest) ... ok
test_missing_host_port_is_not_ready_even_if_container_runs (test_local_config.LocalConfigTest) ... ok
test_remote_database_and_model_are_refused (test_local_config.LocalConfigTest) ... ok
test_remote_docker_endpoint_is_refused (test_local_config.LocalConfigTest) ... ok
test_remote_profile_and_storage_are_refused (test_local_config.LocalConfigTest) ... ok
test_shell_substitution_is_refused_without_execution (test_local_config.LocalConfigTest) ... ok
test_spring_or_jvm_bypass_is_refused (test_local_config.LocalConfigTest) ... ok
test_storage_outside_checkout_is_refused (test_local_config.LocalConfigTest) ... ok
test_url_credentials_query_and_non_http_are_refused (test_local_config.LocalConfigTest) ... ok
test_wrong_extension_version_fails (test_local_config.LocalConfigTest) ... ok
test_explicit_rag_without_ready_ollama_does_not_start_compose (test_local_config.ServiceScopeTest) ... ok
test_legacy_status_still_requires_ollama (test_local_config.ServiceScopeTest) ... ok
test_search_start_never_calls_ollama (test_local_config.ServiceScopeTest) ... ok
test_claimed_or_retrying_job_is_processing (test_seed_demo_corpus.ClassifyTest) ... ok
test_failed_job (test_seed_demo_corpus.ClassifyTest) ... ok
test_indexed_document (test_seed_demo_corpus.ClassifyTest) ... ok
test_new_upload_waits_for_worker (test_seed_demo_corpus.ClassifyTest) ... ok
test_embedding_hint_first_when_server_not_ready (test_seed_demo_corpus.DiagnoseTest) ... ok
test_embedding_hint_for_failed_documents (test_seed_demo_corpus.DiagnoseTest) ... ok
test_no_hint_when_everything_indexed (test_seed_demo_corpus.DiagnoseTest) ... ok
test_slow_hint_when_worker_and_server_are_up (test_seed_demo_corpus.DiagnoseTest) ... ok
test_worker_hint_when_no_job_started (test_seed_demo_corpus.DiagnoseTest) ... ok
test_different_bytes_produce_different_keys (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_same_bytes_produce_same_valid_key (test_seed_demo_corpus.IdempotencyKeyTest) ... ok
test_committed_corpus_is_complete (test_seed_demo_corpus.ManifestTest) ... ok

----------------------------------------------------------------------
Ran 69 tests in 6.809s

OK
종료 시각: 2026-10-18T14:56:58+09:00
종료 코드: 0
소요 시간: 6.904초
Loading
Loading