From 1f61e908b275e5b16cf8e03e822a39dbe76f79ac Mon Sep 17 00:00:00 2001 From: James Cane Date: Tue, 7 Apr 2026 08:12:49 +0100 Subject: [PATCH] fix: handle non-interactive init prompts gracefully --- mempalace/entity_detector.py | 27 ++++++++++++++++++--------- mempalace/room_detector_local.py | 21 ++++++++++++++------- tests/test_noninteractive_init.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 tests/test_noninteractive_init.py diff --git a/mempalace/entity_detector.py b/mempalace/entity_detector.py index 63a78834a6..9196004a90 100644 --- a/mempalace/entity_detector.py +++ b/mempalace/entity_detector.py @@ -714,6 +714,15 @@ def _print_entity_list(entities: list, label: str): print(f" {i + 1:2}. {e['name']:20} [{confidence_bar}] {signals_str}") +def _prompt(prompt: str, default: str = "") -> str: + """Read user input safely; fall back to a default on EOF/non-interactive stdin.""" + try: + return input(prompt).strip() + except EOFError: + print(f"\n No interactive input available — defaulting to: {default or 'accept'}") + return default + + def confirm_entities(detected: dict, yes: bool = False) -> dict: """ Interactive confirmation step. @@ -750,7 +759,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: print(" [add] Add missing people or projects") print() - choice = input(" Your choice [enter/edit/add]: ").strip().lower() + choice = _prompt(" Your choice [enter/edit/add]: ").lower() confirmed_people = [e["name"] for e in detected["people"]] confirmed_projects = [e["name"] for e in detected["projects"]] @@ -760,7 +769,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: if detected["uncertain"]: print("\n Uncertain entities — classify each:") for e in detected["uncertain"]: - ans = input(f" {e['name']} — (p)erson, (r)roject, or (s)kip? ").strip().lower() + ans = _prompt(f" {e['name']} — (p)erson, (r)roject, or (s)kip? ").lower() if ans == "p": confirmed_people.append(e["name"]) elif ans == "r": @@ -768,28 +777,28 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: # Remove wrong people print(f"\n Current people: {', '.join(confirmed_people) or '(none)'}") - remove = input( + remove = _prompt( " Numbers to REMOVE from people (comma-separated, or enter to skip): " - ).strip() + ) if remove: to_remove = {int(x.strip()) - 1 for x in remove.split(",") if x.strip().isdigit()} confirmed_people = [p for i, p in enumerate(confirmed_people) if i not in to_remove] # Remove wrong projects print(f"\n Current projects: {', '.join(confirmed_projects) or '(none)'}") - remove = input( + remove = _prompt( " Numbers to REMOVE from projects (comma-separated, or enter to skip): " - ).strip() + ) if remove: to_remove = {int(x.strip()) - 1 for x in remove.split(",") if x.strip().isdigit()} confirmed_projects = [p for i, p in enumerate(confirmed_projects) if i not in to_remove] - if choice == "add" or input("\n Add any missing? [y/N]: ").strip().lower() == "y": + if choice == "add" or _prompt("\n Add any missing? [y/N]: ").lower() == "y": while True: - name = input(" Name (or enter to stop): ").strip() + name = _prompt(" Name (or enter to stop): ") if not name: break - kind = input(f" Is '{name}' a (p)erson or (r)roject? ").strip().lower() + kind = _prompt(f" Is '{name}' a (p)erson or (r)roject? ").lower() if kind == "p": confirmed_people.append(name) elif kind == "r": diff --git a/mempalace/room_detector_local.py b/mempalace/room_detector_local.py index f927a84699..51c4eb0e1f 100644 --- a/mempalace/room_detector_local.py +++ b/mempalace/room_detector_local.py @@ -215,6 +215,15 @@ def print_proposed_structure(project_name: str, rooms: list, total_files: int, s print(f"\n{'─' * 55}") +def _prompt(prompt: str, default: str = "") -> str: + """Read user input safely; fall back to a default on EOF/non-interactive stdin.""" + try: + return input(prompt).strip() + except EOFError: + print(f"\n No interactive input available — defaulting to: {default or 'accept'}") + return default + + def get_user_approval(rooms: list) -> list: """Same approval flow as AI version.""" print(" Review the proposed rooms above.") @@ -224,7 +233,7 @@ def get_user_approval(rooms: list) -> list: print(" [add] Add a room manually") print() - choice = input(" Your choice [enter/edit/add]: ").strip().lower() + choice = _prompt(" Your choice [enter/edit/add]: ").lower() if choice in ("", "y", "yes"): return rooms @@ -233,19 +242,17 @@ def get_user_approval(rooms: list) -> list: print("\n Current rooms:") for i, room in enumerate(rooms): print(f" {i + 1}. {room['name']} — {room['description']}") - remove = input("\n Room numbers to REMOVE (comma-separated, or enter to skip): ").strip() + remove = _prompt("\n Room numbers to REMOVE (comma-separated, or enter to skip): ") if remove: to_remove = {int(x.strip()) - 1 for x in remove.split(",") if x.strip().isdigit()} rooms = [r for i, r in enumerate(rooms) if i not in to_remove] - if choice == "add" or input("\n Add any missing rooms? [y/N]: ").strip().lower() == "y": + if choice == "add" or _prompt("\n Add any missing rooms? [y/N]: ").lower() == "y": while True: - new_name = ( - input(" New room name (or enter to stop): ").strip().lower().replace(" ", "_") - ) + new_name = _prompt(" New room name (or enter to stop): ").lower().replace(" ", "_") if not new_name: break - new_desc = input(f" Description for '{new_name}': ").strip() + new_desc = _prompt(f" Description for '{new_name}': ") rooms.append({"name": new_name, "description": new_desc, "keywords": [new_name]}) print(f" Added: {new_name}") diff --git a/tests/test_noninteractive_init.py b/tests/test_noninteractive_init.py new file mode 100644 index 0000000000..0fbc0dfae2 --- /dev/null +++ b/tests/test_noninteractive_init.py @@ -0,0 +1,30 @@ +import builtins + +from mempalace.entity_detector import confirm_entities +from mempalace.room_detector_local import get_user_approval + + +def test_room_approval_defaults_to_accept_on_eof(monkeypatch): + rooms = [{"name": "src", "description": "Files from src/", "keywords": ["src"]}] + + def raise_eof(_prompt): + raise EOFError + + monkeypatch.setattr(builtins, "input", raise_eof) + assert get_user_approval(rooms) == rooms + + +def test_entity_confirmation_defaults_to_accept_on_eof(monkeypatch): + detected = { + "people": [{"name": "Alice", "confidence": 1.0, "source": "test", "signals": []}], + "projects": [{"name": "MemPalace", "confidence": 1.0, "source": "test", "signals": []}], + "uncertain": [], + } + + def raise_eof(_prompt): + raise EOFError + + monkeypatch.setattr(builtins, "input", raise_eof) + confirmed = confirm_entities(detected) + + assert confirmed == {"people": ["Alice"], "projects": ["MemPalace"]}