diff --git a/modules/hook-context-intelligence/pyproject.toml b/modules/hook-context-intelligence/pyproject.toml index 79e6974..164af89 100644 --- a/modules/hook-context-intelligence/pyproject.toml +++ b/modules/hook-context-intelligence/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "httpx>=0.28.1", "idna>=3.15", "pathspec>=0.12,<2", - "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@v0.1.1", + "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main", ] [project.entry-points."amplifier.modules"] @@ -31,7 +31,7 @@ allow-direct-references = true [dependency-groups] dev = [ - "amplifier-core>=1.4.1", + "amplifier-core>=1.6.0", "pytest>=9.0.3", "pytest-asyncio>=0.24", "pyyaml>=6.0", @@ -39,13 +39,6 @@ dev = [ "ruff>=0.4", ] -[tool.uv.sources] -amplifier-core = { git = "https://github.com/microsoft/amplifier-core", rev = "v1.4.1" } -# Note: the bundle dependency is declared as a PEP 508 direct git reference in -# [project.dependencies] above (survives `uv pip install --no-sources`). It is -# intentionally NOT given a `path = "../.."` source here, so the module installs -# identically inside the monorepo and standalone. - [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/modules/tool-blob-read/pyproject.toml b/modules/tool-blob-read/pyproject.toml index d5c9219..c6e935f 100644 --- a/modules/tool-blob-read/pyproject.toml +++ b/modules/tool-blob-read/pyproject.toml @@ -6,7 +6,7 @@ requires-python = ">=3.11" license = "MIT" dependencies = [ - "amplifier-bundle-context-intelligence", + "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main", "httpx>=0.28.1", "idna>=3.15", ] @@ -24,19 +24,19 @@ package = true [tool.hatch.build.targets.wheel] packages = ["amplifier_module_tool_blob_read"] +[tool.hatch.metadata] +# Required to build a wheel that carries a PEP 508 direct-reference (git+https) dependency. +allow-direct-references = true + [dependency-groups] dev = [ - "amplifier-core", + "amplifier-core>=1.6.0", "pytest>=9.0.3", "pytest-asyncio>=0.24", "pyright>=1.1", "ruff>=0.4", ] -[tool.uv.sources] -amplifier-bundle-context-intelligence = { path = "../.." } -amplifier-core = { git = "https://github.com/microsoft/amplifier-core", branch = "main" } - [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/modules/tool-blob-read/tests/test_tool_dependencies.py b/modules/tool-blob-read/tests/test_tool_dependencies.py new file mode 100644 index 0000000..147bf00 --- /dev/null +++ b/modules/tool-blob-read/tests/test_tool_dependencies.py @@ -0,0 +1,96 @@ +"""Test that the blob-read tool pyproject.toml declares the bundle dependency in a +form that installs standalone (outside the monorepo). + +The tool imports from the `context_intelligence` package shipped by the parent +bundle. For the tool to install standalone under the Amplifier agent's +`uv pip install --no-sources` policy, the bundle MUST be referenced as a PEP 508 +direct git reference inside [project.dependencies] (which survives --no-sources), +NOT via a [tool.uv.sources] `path = "../.."` entry (which --no-sources strips). + +This guard mirrors hook-context-intelligence/tests/test_hook_dependencies.py so +that every module references the parent bundle uniformly and cannot regress to a +uv path source (the original cause of the uv conflicting-URLs co-install failure). +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +MODULE_ROOT = Path(__file__).parent.parent +PYPROJECT = MODULE_ROOT / "pyproject.toml" + +BUNDLE = "amplifier-bundle-context-intelligence" + + +def _load_pyproject() -> dict: + return tomllib.loads(PYPROJECT.read_text()) + + +def _dep_name(dep: str) -> str: + """Extract the bare package name from a requirement string. + + Handles version specifiers (>=, ==) and PEP 508 direct references + (`name @ git+https://...`). + """ + return dep.split("@")[0].split(">=")[0].split("==")[0].strip() + + +class TestToolDependencies: + """Verify the tool declares the bundle as a standalone-installable dependency.""" + + def test_bundle_declared_as_direct_git_reference(self) -> None: + """The bundle must be a PEP 508 direct git reference in [project.dependencies]. + + A direct `name @ git+https://...` reference survives `--no-sources`, + unlike a bare name (only resolvable from PyPI) or a [tool.uv.sources] entry. + """ + data = _load_pyproject() + deps: list[str] = data["project"]["dependencies"] + bundle_deps = [d for d in deps if _dep_name(d) == BUNDLE] + assert bundle_deps, f"Expected '{BUNDLE}' in dependencies, got: {deps}" + assert "git+https://" in bundle_deps[0], ( + f"Bundle dependency must be a direct git+https reference so it survives " + f"`uv pip install --no-sources`, got: {bundle_deps[0]!r}" + ) + + def test_bundle_is_not_a_uv_path_source(self) -> None: + """The bundle must NOT be a [tool.uv.sources] path entry. + + The `path = '../..'` assumption is exactly what breaks standalone install: + --no-sources strips [tool.uv.sources], leaving an unresolvable reference. + Two modules referencing the bundle via path while others use a git URL is + what produced uv's "conflicting URLs" abort on a single co-install command. + """ + data = _load_pyproject() + sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {}) + assert BUNDLE not in sources, ( + f"'{BUNDLE}' must not be a [tool.uv.sources] entry (breaks standalone " + f"install under --no-sources); declare it as a direct git reference in " + f"[project.dependencies] instead. Got sources: {sources}" + ) + + def test_dependencies_list_has_httpx_and_bundle(self) -> None: + """Production deps must include httpx and the bundle. + + amplifier-core is NOT a production dep — it is runtime-provided by the + Amplifier CLI. + """ + data = _load_pyproject() + deps: list[str] = data["project"]["dependencies"] + assert any("httpx" in d for d in deps), f"httpx not found in {deps}" + assert any(_dep_name(d) == BUNDLE for d in deps), f"{BUNDLE} not found in {deps}" + assert not any(_dep_name(d) == "amplifier-core" for d in deps), ( + f"amplifier-core must not be a production dep (runtime-provided): {deps}" + ) + + def test_allow_direct_references_enabled(self) -> None: + """Building a wheel that carries a direct reference requires this hatch flag.""" + data = _load_pyproject() + allow = ( + data.get("tool", {}).get("hatch", {}).get("metadata", {}).get("allow-direct-references") + ) + assert allow is True, ( + "tool.hatch.metadata.allow-direct-references must be true to build a wheel " + f"carrying the direct git reference, got: {allow!r}" + ) diff --git a/modules/tool-context-intelligence-upload/pyproject.toml b/modules/tool-context-intelligence-upload/pyproject.toml index 6e3cf3a..baf3ece 100644 --- a/modules/tool-context-intelligence-upload/pyproject.toml +++ b/modules/tool-context-intelligence-upload/pyproject.toml @@ -5,7 +5,7 @@ requires-python = ">=3.11" license = "MIT" dependencies = [ - "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@v0.1.1", + "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main", "httpx>=0.28.1", "idna>=3.15", "amplifier-module-hook-context-intelligence", diff --git a/modules/tool-graph-query/pyproject.toml b/modules/tool-graph-query/pyproject.toml index 346bc50..389f9b4 100644 --- a/modules/tool-graph-query/pyproject.toml +++ b/modules/tool-graph-query/pyproject.toml @@ -6,7 +6,7 @@ requires-python = ">=3.11" license = "MIT" dependencies = [ - "amplifier-bundle-context-intelligence", + "amplifier-bundle-context-intelligence @ git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main", "httpx>=0.28.1", "idna>=3.15", ] @@ -24,19 +24,19 @@ package = true [tool.hatch.build.targets.wheel] packages = ["amplifier_module_tool_graph_query"] +[tool.hatch.metadata] +# Required to build a wheel that carries a PEP 508 direct-reference (git+https) dependency. +allow-direct-references = true + [dependency-groups] dev = [ - "amplifier-core", + "amplifier-core>=1.6.0", "pytest>=9.0.3", "pytest-asyncio>=0.24", "pyright>=1.1", "ruff>=0.4", ] -[tool.uv.sources] -amplifier-bundle-context-intelligence = { path = "../.." } -amplifier-core = { git = "https://github.com/microsoft/amplifier-core", branch = "main" } - [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/modules/tool-graph-query/tests/test_tool_dependencies.py b/modules/tool-graph-query/tests/test_tool_dependencies.py new file mode 100644 index 0000000..f8952e6 --- /dev/null +++ b/modules/tool-graph-query/tests/test_tool_dependencies.py @@ -0,0 +1,96 @@ +"""Test that the graph-query tool pyproject.toml declares the bundle dependency in a +form that installs standalone (outside the monorepo). + +The tool imports from the `context_intelligence` package shipped by the parent +bundle. For the tool to install standalone under the Amplifier agent's +`uv pip install --no-sources` policy, the bundle MUST be referenced as a PEP 508 +direct git reference inside [project.dependencies] (which survives --no-sources), +NOT via a [tool.uv.sources] `path = "../.."` entry (which --no-sources strips). + +This guard mirrors hook-context-intelligence/tests/test_hook_dependencies.py so +that every module references the parent bundle uniformly and cannot regress to a +uv path source (the original cause of the uv conflicting-URLs co-install failure). +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +MODULE_ROOT = Path(__file__).parent.parent +PYPROJECT = MODULE_ROOT / "pyproject.toml" + +BUNDLE = "amplifier-bundle-context-intelligence" + + +def _load_pyproject() -> dict: + return tomllib.loads(PYPROJECT.read_text()) + + +def _dep_name(dep: str) -> str: + """Extract the bare package name from a requirement string. + + Handles version specifiers (>=, ==) and PEP 508 direct references + (`name @ git+https://...`). + """ + return dep.split("@")[0].split(">=")[0].split("==")[0].strip() + + +class TestToolDependencies: + """Verify the tool declares the bundle as a standalone-installable dependency.""" + + def test_bundle_declared_as_direct_git_reference(self) -> None: + """The bundle must be a PEP 508 direct git reference in [project.dependencies]. + + A direct `name @ git+https://...` reference survives `--no-sources`, + unlike a bare name (only resolvable from PyPI) or a [tool.uv.sources] entry. + """ + data = _load_pyproject() + deps: list[str] = data["project"]["dependencies"] + bundle_deps = [d for d in deps if _dep_name(d) == BUNDLE] + assert bundle_deps, f"Expected '{BUNDLE}' in dependencies, got: {deps}" + assert "git+https://" in bundle_deps[0], ( + f"Bundle dependency must be a direct git+https reference so it survives " + f"`uv pip install --no-sources`, got: {bundle_deps[0]!r}" + ) + + def test_bundle_is_not_a_uv_path_source(self) -> None: + """The bundle must NOT be a [tool.uv.sources] path entry. + + The `path = '../..'` assumption is exactly what breaks standalone install: + --no-sources strips [tool.uv.sources], leaving an unresolvable reference. + Two modules referencing the bundle via path while others use a git URL is + what produced uv's "conflicting URLs" abort on a single co-install command. + """ + data = _load_pyproject() + sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {}) + assert BUNDLE not in sources, ( + f"'{BUNDLE}' must not be a [tool.uv.sources] entry (breaks standalone " + f"install under --no-sources); declare it as a direct git reference in " + f"[project.dependencies] instead. Got sources: {sources}" + ) + + def test_dependencies_list_has_httpx_and_bundle(self) -> None: + """Production deps must include httpx and the bundle. + + amplifier-core is NOT a production dep — it is runtime-provided by the + Amplifier CLI. + """ + data = _load_pyproject() + deps: list[str] = data["project"]["dependencies"] + assert any("httpx" in d for d in deps), f"httpx not found in {deps}" + assert any(_dep_name(d) == BUNDLE for d in deps), f"{BUNDLE} not found in {deps}" + assert not any(_dep_name(d) == "amplifier-core" for d in deps), ( + f"amplifier-core must not be a production dep (runtime-provided): {deps}" + ) + + def test_allow_direct_references_enabled(self) -> None: + """Building a wheel that carries a direct reference requires this hatch flag.""" + data = _load_pyproject() + allow = ( + data.get("tool", {}).get("hatch", {}).get("metadata", {}).get("allow-direct-references") + ) + assert allow is True, ( + "tool.hatch.metadata.allow-direct-references must be true to build a wheel " + f"carrying the direct git reference, got: {allow!r}" + )