Repository navigation
Description
Found while writing the real-binary contract for #3224. Reproduces on unmodified main 2920043 with the unmodified binary; the #3224 branch produces byte-for-byte the same lock and deployed state at every step, so it is not caused by that change.
Setup
One local bare repo acme/mono with a subdirectory package packages/pkg-a (its own apm.yml, one skill). Commit 1 is pkg-a 1.0.0, tagged v1.0.0 and pkg-a-v1.0.0. Commit 2 is pkg-a 1.1.0, tagged pkg-a-v1.1.0 and pkg-a--v1.1.0. Consumer declares acme/mono/packages/pkg-a#^1.0.0, target copilot, --no-policy, --parallel-downloads 0, isolated environment with the URL rewritten to the local origin.
What happens
install constraint='^1.0.0' resolved_tag='pkg-a-v1.1.0' resolved_ref='pkg-a-v1.1.0' version='1.1.0' commit=<c2> deployed='# two'
install --frozen
constraint='^1.0.0' resolved_tag='pkg-a-v1.1.0' resolved_ref='pkg-a-v1.1.0' version='1.1.0' commit=<c2> deployed='# one' <-- content from 1.0.0
install constraint=None resolved_tag=None resolved_ref='^1.0.0' version='1.1.0' commit=<c2> deployed='# two' <-- raw range as resolved_ref
update --yes constraint='^1.0.0' resolved_tag='pkg-a-v1.1.0' resolved_ref='pkg-a-v1.1.0' version='1.1.0' commit=<c2> deployed='# two'
install --frozen
... same lock ... deployed='# one'
Every command exits 0. apm audit --ci after the frozen replay reports content-integrity hash drift on the deployed skill, which is correct: the lock says 1.1.0 and the file is the 1.0.0 one.
Two defects, probably one cause
- Frozen replay deploys the wrong release. The lock is right, the file is from the other commit. Since both tags for pkg-a 1.1.0 and the root
v1.0.0tag exist, it looks like the frozen path materialises the subdirectory from a cached checkout of the wrong ref rather than fromresolved_commit. - A repeat ordinary install loses the semver provenance:
constraintandresolved_tagare dropped andresolved_refbecomes the literal string^1.0.0.updatewrites them back. This is the shape [BUG]apm installrecords a stale dependencyversioninapm.lock.yamlwhenapm_modules/holds the previous version #3246 / fix(install): capture acquired manifest versions on first install #3311 were about, from the other direction: the metadata on a replay no longer describes the snapshot that was installed.
Repro
Probe script, run from a checkout with the dev environment (it uses the repo's own test utilities). Pass the apm binary and a scratch directory:
update_probe.py
"""install -> reinstall -> update on a git-semver subdirectory dep, --no-policy; print lock fields per step.
usage: python update_probe.py <apm-binary> <scratch-root>"""
import os, sys, yaml
from pathlib import Path
sys.path.insert(0, "<checkout>") # harness from the working tree
from tests.utils.apm_lifecycle_runner import ApmLifecycleRunner
from tests.utils.isolated_apm_environment import IsolatedApmEnvironment
from tests.utils.local_git_repository import LocalGitRepositoryFactory
from tests.utils.local_package import LocalPackageFactory
from apm_cli.utils.yaml_io import dump_yaml
binary, root = Path(sys.argv[1]), Path(sys.argv[2])
iso = IsolatedApmEnvironment.create(root / "iso", base_env=dict(os.environ)); env = iso.subprocess_env()
pk = LocalPackageFactory(iso.package_root); b = pk.create("mono", targets=("copilot",))
def sub(r, v, m):
p = r / "packages" / "pkg-a"; (p/".apm"/"skills"/"s").mkdir(parents=True, exist_ok=True)
dump_yaml({"name":"pkg-a","version":v,"targets":["copilot"]}, p/"apm.yml"); (p/".apm"/"skills"/"s"/"SKILL.md").write_text(f"---\nname: s\ndescription: d\n---\n# {m}\n")
sub(b.root, "1.0.0", "one")
repos = LocalGitRepositoryFactory(iso.repository_root, env=env); repo = repos.create("mono", source_tree=b.root)
c1 = repos.commit(repo, message="1.0.0"); repos.tag(repo, "v1.0.0", c1); repos.tag(repo, "pkg-a-v1.0.0", c1)
sub(repo.worktree, "1.1.0", "two"); c2 = repos.commit(repo, message="1.1.0"); repos.tag(repo, "pkg-a-v1.1.0", c2); repos.tag(repo, "pkg-a--v1.1.0", c2)
env = repos.url_rewrite_subprocess_env(repo, "https://github.com/acme/mono.git")
proj = LocalPackageFactory(iso.work_root).create("consumer", dependencies=("acme/mono/packages/pkg-a#^1.0.0",), targets=("copilot",)).root
r = ApmLifecycleRunner((str(binary),), timeout_seconds=120, scenario_timeout_seconds=600)
print(" c1(1.0.0)", c1.sha[:7], " c2(1.1.0)", c2.sha[:7])
def lock(tag):
d = yaml.safe_load(open(proj/"apm.lock.yaml"))["dependencies"][0]
marker = (proj/'.agents/skills/s/SKILL.md').read_text().strip().splitlines()[-1]
print(f" [{tag:15}] constraint={d.get('constraint')!r:9} resolved_tag={d.get('resolved_tag')!r:15} resolved_ref={d.get('resolved_ref')!r:15} version={d.get('version')!r:8} commit={str(d.get('resolved_commit'))[:7]} deployed={marker!r}")
common = ("--target","copilot","--no-policy","--parallel-downloads","0")
for tag, args in (("install", ("install",*common)), ("frozen", ("install",*common,"--frozen")), ("reinstall", ("install",*common)), ("update", ("update","--yes","--target","copilot","--parallel-downloads","0")), ("frozen-again", ("install",*common,"--frozen"))):
res = r.run(args, scenario_id=tag, cwd=proj, env=env)
print(f" {tag} exit {res.returncode}" + ("" if res.returncode == 0 else " :: " + (res.stdout+res.stderr).strip().splitlines()[-1][:140]))
lock(tag)Happy to turn this into an APMLifecycle contract if it's accepted; I kept it out of #3224's PR because it isn't a policy problem.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dajiaohuang commentedon Oct 25, 2026
I reviewed current main at 2920043 without modifying or checking out the repository. The closest existing test, TestPositionalVirtualSubdirectorySemver.test_positional_git_semver_uses_real_bare_remote_and_replays_lockfile, uses a different tag arrangement; it checks the initial semver lock fields and byte-identical lock after a subsequent ordinary install, but does not run --frozen or assert the deployed file contents after replay:
apm/tests/integration/test_git_semver_install_e2e.py
Lines 381 to 487 in 2920043
PR #3381 also says it observed these exact frozen/reinstall behaviors on unmodified main and kept them out of #3224's accepted scope; that PR is therefore not a fix for this issue: #3381.
I did not rerun the provided reproduction, so this is a source/test-coverage observation, not independent runtime validation or a root-cause claim. I found no human scope-approval comment on #3382; under current GOVERNANCE.md/CONTRIBUTING.md, I have not started an implementation.