fix(drone): namespace subprocess timeout to --drone-timeout — plain --timeout passes through to modules (watchdog 600s regression, live repro x2). Regression test, 879 green, CHANGELOG both fixes
This commit is contained in:
@@ -73,6 +73,25 @@ PyPI version — not the changelog header.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Plan-number memory search hits the exact plan.** Searching a plan ID
|
||||
('DPLAN-0244', 'fplan 0332' — any case, dash or space) now pins the exact
|
||||
plan as the top result at 100%, via a metadata lookup on the vector store's
|
||||
source-file field instead of embedding similarity (which treats all plan IDs
|
||||
as near-identical strings and never surfaced the target). Patrick ruling:
|
||||
searching a plan number must return that plan first. Semantic search quality
|
||||
for normal queries is unchanged. Also purged 193 junk vectors — throwaway
|
||||
probe/flaky test plans from scratchpad sessions (dv4 batch, probe_test_plan,
|
||||
throwaway_e2e_proof) that had leaked into the store. 1011 memory tests green.
|
||||
|
||||
- **drone --timeout collision: router flag swallowed module flags.** The
|
||||
DPLAN-0245 subprocess-timeout flag consumed the first `--timeout` token
|
||||
anywhere in argv, so module-level flags silently vanished — watchdog's
|
||||
`--timeout 1800` never arrived and long watches died at the 600s default
|
||||
(live repro x2). Drone's flag is now namespaced `--drone-timeout`; plain
|
||||
`--timeout` passes through untouched to the target module, with a regression
|
||||
test pinning the passthrough. Per-command overrides intact. 879 drone tests
|
||||
green, seedgo 100%.
|
||||
|
||||
- **@memory command routing eaten by the new governance module.** The
|
||||
governance module shipped in Track 2 had the wrong `handle_command`
|
||||
signature (`args: list` instead of `command: str, args: list`) and always
|
||||
|
||||
@@ -52,16 +52,16 @@ INTERACTIVE_BRANCHES = ("cli", "backup")
|
||||
|
||||
|
||||
def _extract_timeout(args: list[str]) -> tuple[list[str], int | None]:
|
||||
"""Extract --timeout N from an arg list. Returns (cleaned_args, timeout_or_None)."""
|
||||
if "--timeout" not in args:
|
||||
"""Extract --drone-timeout N from an arg list. Returns (cleaned_args, timeout_or_None)."""
|
||||
if "--drone-timeout" not in args:
|
||||
return args, None
|
||||
idx = args.index("--timeout")
|
||||
idx = args.index("--drone-timeout")
|
||||
if idx + 1 >= len(args):
|
||||
return args, None
|
||||
try:
|
||||
timeout = int(args[idx + 1])
|
||||
except ValueError:
|
||||
logger.info("--timeout value %r is not an integer, ignoring", args[idx + 1])
|
||||
logger.info("--drone-timeout value %r is not an integer, ignoring", args[idx + 1])
|
||||
return args, None
|
||||
return args[:idx] + args[idx + 2 :], timeout
|
||||
|
||||
@@ -107,7 +107,7 @@ def show_help() -> None:
|
||||
table.add_row("list", "List registered custom commands")
|
||||
table.add_row("remove <name>", "Remove a custom command")
|
||||
table.add_row("rm <path> [<path>...]", "Contained safe-delete (project + tmp)")
|
||||
table.add_row("--timeout <seconds>", "Override subprocess timeout (default 30s)")
|
||||
table.add_row("--drone-timeout <seconds>", "Override subprocess timeout (default 30s)")
|
||||
table.add_row("--help", "Show this help")
|
||||
table.add_row("--version", "Show version")
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ def execute_command(
|
||||
except subprocess.TimeoutExpired as e:
|
||||
raise CommandExecutionError(
|
||||
f"Command timed out after {timeout}s: {' '.join(full_cmd)}\n"
|
||||
f" Override with: drone @<target> <command> --timeout <seconds>"
|
||||
f" Override with: drone @<target> <command> --drone-timeout <seconds>"
|
||||
) from e
|
||||
except FileNotFoundError as e:
|
||||
raise CommandExecutionError(f"Executable not found: {executable!r}") from e
|
||||
|
||||
@@ -911,10 +911,10 @@ class TestAipassIntercept:
|
||||
|
||||
|
||||
class TestExtractTimeout:
|
||||
"""Tests for --timeout flag extraction from arg lists."""
|
||||
"""Tests for --drone-timeout flag extraction from arg lists."""
|
||||
|
||||
def test_no_flag(self) -> None:
|
||||
"""Args without --timeout pass through unchanged."""
|
||||
"""Args without --drone-timeout pass through unchanged."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
args = ["close", "FPLAN-0313"]
|
||||
@@ -923,43 +923,43 @@ class TestExtractTimeout:
|
||||
assert timeout is None
|
||||
|
||||
def test_flag_at_end(self) -> None:
|
||||
"""--timeout N at end of args is extracted."""
|
||||
"""--drone-timeout N at end of args is extracted."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
cleaned, timeout = _extract_timeout(["process-plans", "--timeout", "120"])
|
||||
cleaned, timeout = _extract_timeout(["process-plans", "--drone-timeout", "120"])
|
||||
assert cleaned == ["process-plans"]
|
||||
assert timeout == 120
|
||||
|
||||
def test_flag_at_start(self) -> None:
|
||||
"""--timeout N at start of args is extracted."""
|
||||
"""--drone-timeout N at start of args is extracted."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
cleaned, timeout = _extract_timeout(["--timeout", "90", "close", "FPLAN-0313"])
|
||||
cleaned, timeout = _extract_timeout(["--drone-timeout", "90", "close", "FPLAN-0313"])
|
||||
assert cleaned == ["close", "FPLAN-0313"]
|
||||
assert timeout == 90
|
||||
|
||||
def test_flag_in_middle(self) -> None:
|
||||
"""--timeout N in the middle of args is extracted."""
|
||||
"""--drone-timeout N in the middle of args is extracted."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
cleaned, timeout = _extract_timeout(["close", "--timeout", "60", "FPLAN-0313"])
|
||||
cleaned, timeout = _extract_timeout(["close", "--drone-timeout", "60", "FPLAN-0313"])
|
||||
assert cleaned == ["close", "FPLAN-0313"]
|
||||
assert timeout == 60
|
||||
|
||||
def test_flag_without_value(self) -> None:
|
||||
"""--timeout at end with no value returns None and leaves args."""
|
||||
"""--drone-timeout at end with no value returns None and leaves args."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
args = ["close", "--timeout"]
|
||||
args = ["close", "--drone-timeout"]
|
||||
cleaned, timeout = _extract_timeout(args)
|
||||
assert cleaned == args
|
||||
assert timeout is None
|
||||
|
||||
def test_flag_non_integer_value(self) -> None:
|
||||
"""--timeout with non-integer value returns None and leaves args."""
|
||||
"""--drone-timeout with non-integer value returns None and leaves args."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
args = ["close", "--timeout", "abc"]
|
||||
args = ["close", "--drone-timeout", "abc"]
|
||||
cleaned, timeout = _extract_timeout(args)
|
||||
assert cleaned == args
|
||||
assert timeout is None
|
||||
@@ -971,3 +971,12 @@ class TestExtractTimeout:
|
||||
cleaned, timeout = _extract_timeout([])
|
||||
assert cleaned == []
|
||||
assert timeout is None
|
||||
|
||||
def test_plain_timeout_passes_through(self) -> None:
|
||||
"""--timeout (without drone- prefix) is NOT consumed — passes to target."""
|
||||
from aipass.drone.apps.drone import _extract_timeout
|
||||
|
||||
args = ["watchdog", "agent", "@memory", "--timeout", "1800"]
|
||||
cleaned, timeout = _extract_timeout(args)
|
||||
assert cleaned == args
|
||||
assert timeout is None
|
||||
|
||||
@@ -446,11 +446,11 @@ class TestTimeoutErrorMessage:
|
||||
|
||||
def test_timeout_error_includes_override_hint(self, temp_test_dir: Path):
|
||||
"""The timeout error message mentions --timeout."""
|
||||
with pytest.raises(CommandExecutionError, match="--timeout") as exc_info:
|
||||
with pytest.raises(CommandExecutionError, match="--drone-timeout") as exc_info:
|
||||
execute_command(
|
||||
sys.executable,
|
||||
["-c", "import time; time.sleep(10)"],
|
||||
cwd=str(temp_test_dir),
|
||||
timeout=1,
|
||||
)
|
||||
assert "--timeout" in str(exc_info.value)
|
||||
assert "--drone-timeout" in str(exc_info.value)
|
||||
|
||||
Reference in New Issue
Block a user