Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 105df790cf fix(hindsight-embed): restore __file__-relative fallback for --target installs
sysconfig.get_path("scripts") correctly fixes stock venv installs
(#1401) but doesn't cover `pip install --target` layouts where the
binary sits alongside site-packages contents. Keep the original
Path(__file__)-based lookup as a second fallback before uvx (#1240).
2026-05-04 15:24:00 +02:00
Nicolò Boschi 27cdb16d68 fix(typescript-client): skip TestAbortSignal under Deno
Deno freezes ES module namespace objects, so jest.spyOn cannot patch
sdk exports. Skip these spy-based unit tests under Deno (they're
already covered by the Jest suite).
2026-05-04 12:59:27 +02:00
Nicolò Boschi 333c385452 fix(typescript-client): add jest.spyOn/fn shim to deno_setup.ts
The TestAbortSignal tests use jest.spyOn which doesn't exist under Deno.
Add a mock implementation (matching the pattern in the AI SDK's
vitest-compat.ts) so these tests pass with deno test.
2026-05-04 12:54:15 +02:00
Nicolò Boschi 8ed8dbe795 fix(hindsight-embed): use sysconfig to find scripts dir in _find_api_command (#1401)
`Path(__file__).parent.parent` resolves to site-packages/ in stock pip
venvs, missing the actual scripts dir (<venv>/bin or <venv>/Scripts).
Use `sysconfig.get_path("scripts")` which works across pip venvs, conda,
and --target installs.
2026-05-04 12:37:03 +02:00
4 changed files with 161 additions and 34 deletions
@@ -1,6 +1,6 @@
/**
* Preload script for running Jest-style tests under Deno.
* Injects Jest-compatible globals (describe, test, beforeAll, expect)
* Injects Jest-compatible globals (describe, test, beforeAll, expect, jest)
* using Deno's standard library BDD and expect modules.
*
* Usage:
@@ -11,6 +11,91 @@
import { beforeAll, beforeEach, afterAll, afterEach, describe, it } from "jsr:@std/testing/bdd";
import { expect } from "jsr:@std/expect";
// @std/expect recognises mock functions via this well-known symbol
const MOCK_SYMBOL = Symbol.for("@MOCK");
type MockCall = {
args: unknown[];
returned?: unknown;
thrown?: unknown;
timestamp: number;
returns: boolean;
throws: boolean;
};
function createMock(impl?: (...args: unknown[]) => unknown) {
let currentImpl = impl;
const calls: MockCall[] = [];
const mockInfo = { calls };
const mockFn = function (this: unknown, ...args: unknown[]) {
const call: MockCall = {
args,
timestamp: Date.now(),
returns: false,
throws: false,
};
calls.push(call);
try {
const result = currentImpl ? currentImpl.apply(this, args) : undefined;
call.returned = result;
call.returns = true;
return result;
} catch (err) {
call.thrown = err;
call.throws = true;
throw err;
}
};
(mockFn as any)[MOCK_SYMBOL] = mockInfo;
(mockFn as any).mockResolvedValue = (val: unknown) => {
currentImpl = () => Promise.resolve(val);
return mockFn;
};
(mockFn as any).mockRejectedValue = (val: unknown) => {
currentImpl = () => Promise.reject(val);
return mockFn;
};
(mockFn as any).mockImplementation = (fn: (...args: unknown[]) => unknown) => {
currentImpl = fn;
return mockFn;
};
(mockFn as any).mockReturnValue = (val: unknown) => {
currentImpl = () => val;
return mockFn;
};
(mockFn as any).mockReset = () => {
calls.length = 0;
currentImpl = undefined;
return mockFn;
};
(mockFn as any).mockClear = () => {
calls.length = 0;
return mockFn;
};
(mockFn as any).mockRestore = () => {};
return mockFn;
}
const jest = {
fn: (impl?: (...args: unknown[]) => unknown) => createMock(impl),
spyOn: <T extends Record<string, unknown>>(obj: T, method: keyof T) => {
const original = obj[method];
const mock = createMock(
typeof original === "function" ? (original as (...args: unknown[]) => unknown) : undefined
);
const restore = () => {
obj[method] = original;
};
(mock as any).mockRestore = restore;
obj[method] = mock as unknown as T[keyof T];
return mock;
},
};
Object.assign(globalThis, {
describe,
test: it,
@@ -20,4 +105,5 @@ Object.assign(globalThis, {
afterAll,
afterEach,
expect,
jest,
});
@@ -470,7 +470,11 @@ describe("TestMission", () => {
});
});
describe("TestAbortSignal", () => {
// Skip under Deno: jest.spyOn cannot patch ES-module namespace objects whose
// properties are frozen. These unit tests are covered by the Jest suite.
const canSpyOnModules = typeof (globalThis as any).Deno === "undefined";
(canSpyOnModules ? describe : describe.skip)("TestAbortSignal", () => {
test("retain passes abort signal to SDK", async () => {
const bankId = randomBankId();
const controller = new AbortController();
@@ -10,6 +10,7 @@ import os
import platform
import re
import subprocess
import sysconfig
import time
from pathlib import Path
from typing import IO, Optional
@@ -132,12 +133,23 @@ class DaemonEmbedManager(EmbedManager):
if dev_api_path.exists() and (dev_api_path / "pyproject.toml").exists():
return ["uv", "run", "--project", str(dev_api_path), "--extra", "all", "hindsight-api"]
# Prefer a hindsight-api entry point installed alongside hindsight-embed
# (e.g. `uv pip install hindsight-all` or `--target`). Falling through
# to uvx in that case downloads a standalone Python whose ABI won't
# match the sibling site-packages' C extensions (issue #1240).
package_root = Path(__file__).parent.parent
# Prefer a hindsight-api entry point installed alongside hindsight-embed.
# Try two strategies:
#
# 1. sysconfig: resolves <venv>/bin or <venv>/Scripts for standard
# pip/venv installs (issue #1401).
# 2. __file__-relative: resolves <target>/bin or <target>/Scripts for
# `pip install --target` layouts where sysconfig still points at the
# system/venv scripts dir (issue #1240).
binary_name = "hindsight-api.exe" if platform.system() == "Windows" else "hindsight-api"
scripts_dir = Path(sysconfig.get_path("scripts"))
candidate = scripts_dir / binary_name
if candidate.exists():
return [str(candidate)]
# --target installs place binaries alongside site-packages contents
package_root = Path(__file__).parent.parent
for bin_dir in ("bin", "Scripts"):
candidate = package_root / bin_dir / binary_name
if candidate.exists():
+52 -27
View File
@@ -109,38 +109,64 @@ def test_find_ui_command_uses_npx_yes_flag_for_published_control_plane(monkeypat
]
def test_find_api_command_prefers_sibling_binary_over_uvx(tmp_path, monkeypatch):
def test_find_api_command_prefers_installed_binary_over_uvx(tmp_path, monkeypatch):
"""
When hindsight-api is installed alongside hindsight-embed (e.g. via
`uv pip install hindsight-all`), _find_api_command should invoke that
binary directly rather than shelling out to uvx. uvx downloads a
standalone Python whose ABI won't match sibling C extensions compiled
for the host Python (regression for issue #1240, NixOS asyncpg failure).
`pip install hindsight-all`), _find_api_command should invoke that
binary directly rather than shelling out to uvx. Uses sysconfig to
locate the venv's scripts directory (issue #1401, #1240).
"""
package_root = tmp_path / "site-packages" / "hindsight_embed"
package_root.mkdir(parents=True)
fake_module = package_root / "daemon_embed_manager.py"
scripts_dir = tmp_path / "bin"
scripts_dir.mkdir()
api_binary = scripts_dir / "hindsight-api"
api_binary.touch()
manager = DaemonEmbedManager()
# Point __file__ away from monorepo so dev-mode check doesn't trigger
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(tmp_path / "hindsight_embed" / "daemon_embed_manager.py"))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.sysconfig.get_path", lambda key: str(scripts_dir))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Linux")
assert manager._find_api_command() == [str(api_binary)]
def test_find_api_command_target_install_uses_file_relative_fallback(tmp_path, monkeypatch):
"""
When installed with `pip install --target`, sysconfig still points at the
system/venv scripts dir (no binary there). The __file__-relative fallback
should find the sibling binary in <target>/bin/ (issue #1240).
"""
# sysconfig points to an empty venv scripts dir (no binary)
venv_scripts = tmp_path / "venv_bin"
venv_scripts.mkdir()
# --target layout: binary sits next to site-packages contents
target_dir = tmp_path / "target"
pkg_dir = target_dir / "hindsight_embed"
pkg_dir.mkdir(parents=True)
fake_module = pkg_dir / "daemon_embed_manager.py"
fake_module.write_text("")
sibling_bin = tmp_path / "site-packages" / "bin" / "hindsight-api"
sibling_bin.parent.mkdir(parents=True)
sibling_bin = target_dir / "bin" / "hindsight-api"
sibling_bin.parent.mkdir()
sibling_bin.touch()
manager = DaemonEmbedManager()
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(fake_module))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.sysconfig.get_path", lambda key: str(venv_scripts))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Linux")
assert manager._find_api_command() == [str(sibling_bin)]
def test_find_api_command_falls_back_to_uvx_when_no_sibling_binary(tmp_path, monkeypatch):
"""Without a sibling binary or dev checkout, fall back to uvx."""
package_root = tmp_path / "site-packages" / "hindsight_embed"
package_root.mkdir(parents=True)
fake_module = package_root / "daemon_embed_manager.py"
fake_module.write_text("")
def test_find_api_command_falls_back_to_uvx_when_no_binary(tmp_path, monkeypatch):
"""Without an installed binary or dev checkout, fall back to uvx."""
scripts_dir = tmp_path / "bin"
scripts_dir.mkdir()
# No hindsight-api binary in scripts_dir
manager = DaemonEmbedManager()
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(fake_module))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(tmp_path / "hindsight_embed" / "daemon_embed_manager.py"))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.sysconfig.get_path", lambda key: str(scripts_dir))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Linux")
monkeypatch.setenv("HINDSIGHT_EMBED_API_VERSION", "1.2.3")
@@ -148,17 +174,16 @@ def test_find_api_command_falls_back_to_uvx_when_no_sibling_binary(tmp_path, mon
def test_find_api_command_windows_uses_exe_suffix(tmp_path, monkeypatch):
"""On Windows, the sibling binary has a .exe suffix."""
package_root = tmp_path / "site-packages" / "hindsight_embed"
package_root.mkdir(parents=True)
fake_module = package_root / "daemon_embed_manager.py"
fake_module.write_text("")
sibling_bin = tmp_path / "site-packages" / "Scripts" / "hindsight-api.exe"
sibling_bin.parent.mkdir(parents=True)
sibling_bin.touch()
"""On Windows, the installed binary has a .exe suffix."""
scripts_dir = tmp_path / "Scripts"
scripts_dir.mkdir()
api_binary = scripts_dir / "hindsight-api.exe"
api_binary.touch()
manager = DaemonEmbedManager()
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(fake_module))
# Point __file__ away from monorepo so dev-mode check doesn't trigger
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.__file__", str(tmp_path / "hindsight_embed" / "daemon_embed_manager.py"))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.sysconfig.get_path", lambda key: str(scripts_dir))
monkeypatch.setattr("hindsight_embed.daemon_embed_manager.platform.system", lambda: "Windows")
assert manager._find_api_command() == [str(sibling_bin)]
assert manager._find_api_command() == [str(api_binary)]