mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
4ba54d8c8f
* fix(embed): prefer locally-installed hindsight-api over uvx Falling through to `uvx hindsight-api@...` when hindsight-embed is installed via `uv pip install --target` (e.g. NixOS, hindsight-all) downloads a standalone Python whose ABI doesn't match the sibling site-packages' C extensions, causing `ModuleNotFoundError: asyncpg.protocol.protocol` at daemon startup (closes #1240). Check for a sibling `hindsight-api` entry point in `bin/` (or `Scripts/hindsight-api.exe` on Windows) before falling back to uvx. * ci(embed): add Windows unit-test job for hindsight-embed Runs pytest on windows-latest to exercise the Windows code paths in hindsight-embed (msvcrt file locking, .exe binary detection in _find_api_command, netstat-based PID lookup). Skips the test.sh smoke test: the daemon uses POSIX-only subprocess.Popen(start_new_session=True) and signal.SIGTERM, so making the full lifecycle Windows-safe is a separate effort. * ci(embed): add Windows --target install test for issue #1240 Exercises the exact install layout from the issue: `uv pip install --target` hindsight-embed + hindsight-api-slim, then verify the sibling `Scripts/hindsight-api.exe` is discovered by `_find_api_command()` instead of falling back to uvx. Also runs `hindsight-embed --help` from the installed binary as a basic smoke check. Daemon startup is still out of scope (needs secrets + POSIX `start_new_session=True` fix). * feat(embed): full Windows support for daemon + smoke test Fixes every platform-specific blocker that previously forced the Windows CI job to skip the smoke test: - hindsight-api-slim/daemon.py: skip the double-fork on Windows (no fork model). The spawning embed process now drives detachment via CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS instead. - hindsight-embed/daemon_embed_manager.py: centralize detach flags in _detach_popen_kwargs(). Windows requires creationflags plus explicit stdout/stderr redirection (DETACHED_PROCESS leaves the child with no console). POSIX keeps start_new_session=True. - hindsight-embed/cli.py: reconfigure sys.stdout/stderr to UTF-8 on Windows so Rich's box-drawing / ✓ glyphs don't crash the default cp1252 codec. - hindsight-embed/profile_manager.py: seek to byte 0 before msvcrt lock/unlock. Windows's msvcrt.locking(LK_UNLCK) requires the file pointer at the start of the locked region, which wasn't true after json.dump moved the position past the data. - hindsight-embed/test.sh: detect python vs python3 so Git Bash on windows-latest (which only ships `python`) can run the smoke test. - tests: set USERPROFILE alongside HOME because Path.home() on Windows consults USERPROFILE, not HOME. - HINDSIGHT_EMBED_DAEMON_STARTUP_TIMEOUT env var: bump on Windows CI since pg0-embedded's initdb on cold runners is slow. CI: test-embed-windows now mirrors the Linux test-embed job — vertexai creds, local-ml/embedded-db extras, HF cache, full smoke test — on top of the --target install-layout check for issue #1240. * fix(api-slim): gate mlx/mlx-lm off Windows in local-ml extras mlx only ships wheels for macOS/Linux, so `uv sync --all-extras` on win_amd64 errors out with "no source distribution or wheel for the current platform". Constrain both to `sys_platform != 'win32'` so Windows resolves local-ml without the Apple Silicon pieces. * fix(embed): use Path.replace for atomic metadata write on Windows Path.rename refuses to overwrite an existing destination on Windows (WinError 183); every profile metadata update after the first one failed with FileExistsError. Path.replace is the cross-platform atomic rename added in Python 3.3 precisely for this pattern. * fix(embed): skip configure prompts when CI env vars are set do_configure previously gated non-interactive mode on `sys.stdin.isatty()`: if stdin looked interactive, it went to the prompt path regardless of env. On Windows GHA pwsh runners stdin looks like a TTY (it doesn't on Linux headless runners), so the subprocess-invoked `configure` would block on input and exit with "Configuration cancelled" — even though HINDSIGHT_API_LLM_* env vars were set. Fall through to _do_configure_from_env whenever the required CI inputs are present (API key set, or provider is ollama/vertexai). * ci(embed): build and stage hindsight Rust CLI on Windows smoke test hindsight-embed's retain/recall delegate to the Rust `hindsight` CLI. On POSIX the embed CLI auto-installs via curl|bash, but on Windows `bash` routes to WSL (not provisioned) and there's no Windows installer. Build the CLI from source with cargo and copy the .exe into ~/.local/bin, which is the first location find_cli_binary() checks. Also teach find_cli_binary to look for `hindsight.exe` (and drop the Unix-only os.access X check on Windows) so the staged binary is actually picked up. * fix(cli): update get_graph call to match regenerated client signature hindsight-clients/rust was regenerated when document_id + chunk_id query params were added to /banks/{id}/graph; progenitor orders query params alphabetically, so the call-site now needs three leading Nones (chunk_id, document_id, limit) and type_filter in the 8th slot. Building the CLI off the current openapi.json was failing with E0061 "this method takes 9 arguments but 7 arguments were supplied", blocking the Windows smoke-test cargo build. * chore(api-slim): bump pg0-embedded to 0.13.0 for Windows support 0.13.0 fixes the "IO error: invalid gzip header" crash that blocked embedded PostgreSQL startup on Windows, which was the final remaining blocker for the Windows hindsight-embed smoke test. * ci(embed): install --target outside repo for sibling-binary verify _find_api_command's first check looks for a sibling hindsight-api-slim/ dir via Path(__file__).parent.parent.parent. When the --target install dir lives inside the monorepo checkout, that branch matches and the test silently exercises the dev-mode path instead of the sibling-binary path we're trying to validate. Move the install into $RUNNER_TEMP so the dev-mode probe misses and the sibling-binary branch is actually hit.
205 lines
6.5 KiB
Bash
Executable File
205 lines
6.5 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Smoke test for hindsight-embed CLI with daemon mode
|
|
# Tests retain and recall operations via the background daemon
|
|
# Verifies daemon lifecycle, memory retention, and recall functionality
|
|
#
|
|
|
|
set -e
|
|
|
|
# Windows (Git Bash / MSYS) ships `python` but not `python3`. Let callers
|
|
# override explicitly via $PYTHON; otherwise probe for whichever is on PATH.
|
|
if [ -z "${PYTHON:-}" ]; then
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
PYTHON=python3
|
|
else
|
|
PYTHON=python
|
|
fi
|
|
fi
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
API_DIR="$(cd "$SCRIPT_DIR/../hindsight-api" && pwd)"
|
|
|
|
echo "=== Hindsight Embed Smoke Test (Daemon Mode) ==="
|
|
|
|
# Check required environment (load from config if not set)
|
|
if [ -f ~/.hindsight/config.env ]; then
|
|
source ~/.hindsight/config.env
|
|
fi
|
|
|
|
# vertexai uses GCP service account credentials instead of an API key
|
|
if [ -z "$HINDSIGHT_API_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
|
|
if [ "${HINDSIGHT_API_LLM_PROVIDER}" != "vertexai" ]; then
|
|
echo "Error: HINDSIGHT_API_LLM_API_KEY or OPENAI_API_KEY is required"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Use a unique bank ID for this test run
|
|
BANK_ID="test-$$-$(date +%s)"
|
|
echo "Using bank ID: $BANK_ID"
|
|
echo "Script dir: $SCRIPT_DIR"
|
|
echo "API dir: $API_DIR"
|
|
|
|
# Show environment info for debugging
|
|
echo ""
|
|
echo "Environment:"
|
|
echo " HINDSIGHT_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-not set}"
|
|
echo " HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-not set}"
|
|
echo " HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:+set (hidden)}"
|
|
echo " Python: $("$PYTHON" --version 2>&1)"
|
|
echo " uv: $(uv --version 2>&1)"
|
|
|
|
# Stop any existing daemon
|
|
echo ""
|
|
echo "Stopping any existing daemon..."
|
|
uv run --project "$SCRIPT_DIR" hindsight-embed daemon stop 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# Test 1: Retain (this should start the daemon)
|
|
echo ""
|
|
echo "Test 1: Retaining a memory (first call - daemon will start)..."
|
|
START_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
set +e # Temporarily disable exit on error to capture output
|
|
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory retain "$BANK_ID" "The user's favorite color is blue" 2>&1)
|
|
EXIT_CODE=$?
|
|
set -e
|
|
END_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
DURATION=$("$PYTHON" -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
|
echo "$OUTPUT"
|
|
echo "Duration: ${DURATION}s"
|
|
echo "Exit code: $EXIT_CODE"
|
|
|
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
echo "FAIL: Command exited with code $EXIT_CODE"
|
|
echo ""
|
|
echo "Checking daemon logs..."
|
|
if [ -f ~/.hindsight/daemon.log ]; then
|
|
echo "=== daemon.log ==="
|
|
tail -50 ~/.hindsight/daemon.log
|
|
else
|
|
echo "No daemon.log found"
|
|
fi
|
|
if [ -f ~/.hindsight/daemon.stderr ]; then
|
|
echo ""
|
|
echo "=== daemon.stderr ==="
|
|
cat ~/.hindsight/daemon.stderr
|
|
else
|
|
echo "No daemon.stderr found"
|
|
fi
|
|
echo ""
|
|
echo "Checking for hindsight-api..."
|
|
which hindsight-api 2>/dev/null || echo "hindsight-api not in PATH"
|
|
which uvx 2>/dev/null || echo "uvx not in PATH"
|
|
which uv 2>/dev/null || echo "uv not in PATH"
|
|
exit 1
|
|
fi
|
|
|
|
if ! echo "$OUTPUT" | grep -qi "retained"; then
|
|
echo "FAIL: Expected 'retained' in output"
|
|
exit 1
|
|
fi
|
|
echo "PASS: Memory retained successfully"
|
|
|
|
# Test 2: Recall (daemon already running - should be faster)
|
|
echo ""
|
|
echo "Test 2: Recalling memories (daemon already running)..."
|
|
START_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
set +e
|
|
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory recall "$BANK_ID" "What is the user's favorite color?" 2>&1)
|
|
EXIT_CODE=$?
|
|
set -e
|
|
END_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
DURATION=$("$PYTHON" -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
|
echo "$OUTPUT"
|
|
echo "Duration: ${DURATION}s"
|
|
echo "Exit code: $EXIT_CODE"
|
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
echo "FAIL: Command exited with code $EXIT_CODE"
|
|
exit 1
|
|
fi
|
|
if ! echo "$OUTPUT" | grep -qi "blue"; then
|
|
echo "FAIL: Expected 'blue' in recall output"
|
|
exit 1
|
|
fi
|
|
echo "PASS: Memory recalled successfully"
|
|
|
|
# Test 3: Retain with context (daemon should still be running)
|
|
echo ""
|
|
echo "Test 3: Retaining memory with context..."
|
|
START_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
set +e
|
|
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory retain "$BANK_ID" "User prefers Python over JavaScript" --context work 2>&1)
|
|
EXIT_CODE=$?
|
|
set -e
|
|
END_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
DURATION=$("$PYTHON" -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
|
echo "$OUTPUT"
|
|
echo "Duration: ${DURATION}s"
|
|
echo "Exit code: $EXIT_CODE"
|
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
echo "FAIL: Command exited with code $EXIT_CODE"
|
|
exit 1
|
|
fi
|
|
if ! echo "$OUTPUT" | grep -qi "retained"; then
|
|
echo "FAIL: Expected 'retained' in output"
|
|
exit 1
|
|
fi
|
|
echo "PASS: Memory with context retained successfully"
|
|
|
|
# Test 4: Recall with JSON output
|
|
echo ""
|
|
echo "Test 4: Recalling with JSON output..."
|
|
START_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
set +e
|
|
JSON_OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory recall "$BANK_ID" "programming preferences" -o json 2>&1)
|
|
EXIT_CODE=$?
|
|
set -e
|
|
END_TIME=$("$PYTHON" -c "import time; print(time.time())")
|
|
DURATION=$("$PYTHON" -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
|
echo "$JSON_OUTPUT"
|
|
echo "Duration: ${DURATION}s"
|
|
echo "Exit code: $EXIT_CODE"
|
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
echo "FAIL: Command exited with code $EXIT_CODE"
|
|
exit 1
|
|
fi
|
|
if ! echo "$JSON_OUTPUT" | grep -qi "python"; then
|
|
echo "FAIL: Expected 'Python' in recall output"
|
|
exit 1
|
|
fi
|
|
if ! echo "$JSON_OUTPUT" | "$PYTHON" -c "import sys, json; json.load(sys.stdin)" 2>/dev/null; then
|
|
echo "FAIL: Expected valid JSON output"
|
|
exit 1
|
|
fi
|
|
echo "PASS: Memory recalled with JSON format successfully"
|
|
|
|
# Test 5: Check daemon is running
|
|
echo ""
|
|
echo "Test 5: Verifying daemon is running..."
|
|
if curl -s http://127.0.0.1:8888/health | grep -q "healthy"; then
|
|
echo "PASS: Daemon is running and healthy"
|
|
else
|
|
echo "FAIL: Daemon is not running"
|
|
exit 1
|
|
fi
|
|
|
|
# Test 6: Daemon status command
|
|
echo ""
|
|
echo "Test 6: Testing daemon status command..."
|
|
STATUS_OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed daemon status 2>&1)
|
|
echo "$STATUS_OUTPUT"
|
|
if ! echo "$STATUS_OUTPUT" | grep -qi "running"; then
|
|
echo "FAIL: Expected 'running' in daemon status output"
|
|
exit 1
|
|
fi
|
|
echo "PASS: Daemon status command works"
|
|
|
|
# Cleanup: Stop daemon
|
|
echo ""
|
|
echo "Stopping daemon..."
|
|
uv run --project "$SCRIPT_DIR" hindsight-embed daemon stop 2>/dev/null || true
|
|
|
|
echo ""
|
|
echo "=== All tests passed! ==="
|