@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.12 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/adl +2 -2
- package/bin/git +16 -17
- package/bin/git-ado +25 -24
- package/bin/git-defaults +173 -0
- package/bin/health-check +8 -1
- package/bin/kimi-hooks +1 -1
- package/bin/lib/adl-package.sh +257 -0
- package/bin/lib/fleet-host.sh +8 -5
- package/bin/reconcile +482 -232
- package/bin/reconcile-host +30 -20
- package/bin/setup +20 -17
- package/bin/shell-path-policy +15 -5
- package/bin/sync-and-reconcile +62 -64
- package/bin/validate-manifest +33 -9
- package/package.json +2 -10
- package/packages/agent-hooks/README.md +14 -9
- package/packages/agent-hooks/assets/hooks.json +0 -20
- package/packages/agent-hooks/src/cli/args.ts +1 -1
- package/packages/agent-hooks/src/cli/doctor.ts +6 -58
- package/packages/agent-hooks/src/deploy/deploy.ts +12 -41
- package/packages/agent-hooks/src/deploy/doctor.ts +8 -11
- package/packages/agent-hooks/src/deploy/index.ts +14 -14
- package/packages/agent-hooks/src/deploy/json-merge.ts +7 -8
- package/packages/agent-hooks/src/deploy/legacy-lock.ts +2 -2
- package/packages/agent-hooks/src/deploy/list.ts +1 -1
- package/packages/agent-hooks/src/deploy/lock.ts +31 -12
- package/packages/agent-hooks/src/deploy/profile.ts +3 -4
- package/packages/agent-hooks/src/deploy/source.ts +1 -1
- package/packages/agent-hooks/src/gating/config.ts +10 -24
- package/packages/agent-hooks/src/gating/root.ts +3 -3
- package/packages/agent-hooks/src/hooks/briefing/handler.ts +2 -2
- package/packages/agent-hooks/src/hooks/briefing/index.ts +27 -287
- package/packages/agent-hooks/src/hooks/contract.ts +17 -17
- package/packages/agent-hooks/src/hooks/crg/background.ts +27 -0
- package/packages/agent-hooks/src/hooks/crg/index.ts +7 -6
- package/packages/agent-hooks/src/hooks/log.ts +8 -10
- package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +1 -1
- package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +1 -1
- package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +6 -5
- package/packages/agent-hooks/src/hosts/registry.ts +1 -1
- package/packages/agent-hooks/src/log/adl-dir.ts +44 -0
- package/packages/agent-hooks/src/log/writer.ts +16 -2
- package/packages/agent-hooks/src/main.ts +7 -41
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +71 -82
- package/packages/agent-hooks/src/rules/engine.ts +13 -17
- package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +3 -3
- package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +12 -14
- package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +28 -28
- package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +8 -9
- package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +56 -59
- package/packages/agent-hooks/src/rules/impl/adl-identity.ts +16 -23
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +19 -16
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +9 -12
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +5 -7
- package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +411 -0
- package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +4 -5
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +6 -7
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +77 -70
- package/packages/agent-hooks/src/rules/impl/deny-allowlist.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/git-helpers.ts +21 -23
- package/packages/agent-hooks/src/rules/impl/git-worktree.ts +14 -28
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +3 -3
- package/packages/agent-hooks/src/rules/impl/messages.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/pr-completion.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +35 -46
- package/packages/agent-hooks/src/rules/impl/tool-find.ts +34 -7
- package/packages/agent-hooks/src/rules/impl/tool-grep.ts +34 -12
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +52 -9
- package/packages/agent-hooks/src/rules/ports.ts +5 -7
- package/packages/agent-hooks/src/rules/registry.ts +4 -6
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +86 -101
- package/packages/agent-hooks/src/rules/shell/git-internals.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +24 -118
- package/packages/agent-hooks/src/rules/shell/piped-script.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/prefixes.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +11 -10
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +26 -26
- package/packages/agent-hooks/src/rules/shell/substitution.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/xargs.ts +2 -2
- package/packages/agent-hooks/src/rules/spec.ts +2 -2
- package/packages/agent-settings/README.md +287 -0
- package/packages/agent-settings/assets/GLOBAL-AGENTS.md +14 -16
- package/packages/agent-settings/assets/agents/architect.toml +2 -0
- package/packages/agent-settings/assets/agents/code-reviewer.toml +2 -0
- package/packages/agent-settings/assets/agents/executor.toml +2 -0
- package/packages/agent-settings/assets/agents/explore.toml +2 -0
- package/packages/agent-settings/assets/agents/planner.toml +2 -0
- package/packages/agent-settings/assets/mcp.json +0 -4
- package/packages/agent-settings/deploy-profile.json +139 -0
- package/packages/agent-settings/deploy-profile.schema.json +147 -0
- package/packages/agent-settings/managed/README.md +29 -30
- package/packages/agent-settings/managed/codex-requirements.toml +9 -9
- package/packages/agent-settings/package.json +20 -0
- package/packages/agent-settings/src/deploy/agent-render.ts +2 -2
- package/packages/agent-settings/src/deploy/agent-toml.ts +1 -1
- package/packages/agent-settings/src/deploy/agents-deploy.ts +17 -3
- package/packages/agent-settings/src/deploy/doctor.ts +3 -83
- package/packages/agent-settings/src/deploy/index.ts +6 -33
- package/packages/agent-settings/src/deploy/legacy-lock.ts +6 -6
- package/packages/agent-settings/src/deploy/list.ts +5 -19
- package/packages/agent-settings/src/deploy/lock.ts +8 -18
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +10 -5
- package/packages/agent-settings/src/deploy/mcp-merge.ts +22 -3
- package/packages/agent-settings/src/deploy/mcp-source.ts +1 -1
- package/packages/agent-settings/src/deploy/profile.ts +11 -35
- package/packages/agent-settings/src/deploy/retire-lean-ctx.ts +184 -0
- package/packages/agent-settings/src/deploy/settings.ts +17 -22
- package/packages/agent-settings/src/global-surface-sweep.ts +0 -2
- package/packages/agent-settings/src/kimi-hooks.ts +339 -192
- package/packages/agent-settings/src/opencode-hooks.ts +1 -1
- package/packages/agent-settings/tsconfig.json +23 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +120 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +276 -0
- package/packages/agent-skills/assets/debug-issue/SKILL.md +8 -7
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +13 -13
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +12 -11
- package/packages/agent-skills/assets/review-changes/SKILL.md +7 -7
- package/packages/agent-skills/assets/testing/references/e2e.md +37 -0
- package/packages/agent-skills/assets/testing/references/integration.md +37 -0
- package/packages/agent-skills/assets/testing/references/unit.md +32 -0
- package/packages/agent-skills/dist/deploy.d.ts +2 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +27 -1
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/doctor.d.ts +0 -2
- package/packages/agent-skills/dist/doctor.d.ts.map +1 -1
- package/packages/agent-skills/dist/doctor.js +1 -1
- package/packages/agent-skills/dist/doctor.js.map +1 -1
- package/packages/agent-skills/dist/fs-utils.d.ts +0 -1
- package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -1
- package/packages/agent-skills/dist/fs-utils.js +1 -1
- package/packages/agent-skills/dist/fs-utils.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +29 -1
- package/packages/agent-skills/src/doctor.ts +1 -1
- package/packages/agent-skills/src/fs-utils.ts +1 -1
- package/packages/azdo/README.md +10 -9
- package/packages/azdo/package.json +1 -3
- package/packages/azdo/src/azdo-pl.ts +2 -54
- package/packages/azdo/src/azdo-pr-review.ts +4 -4
- package/packages/azdo/src/azdo-repo-doctor.ts +101 -52
- package/packages/azdo/src/azdo-wit.ts +41 -2
- package/packages/azdo/src/lib/agent-identity.ts +12 -14
- package/packages/azdo/src/lib/agent-token.ts +2 -2
- package/packages/azdo/src/lib/client.ts +0 -5
- package/packages/azdo/src/lib/graph-identity.ts +4 -4
- package/packages/azdo/src/workflow-rules-matrix.csv +85 -0
- package/packages/azdo/src/workflow-rules.ts +128 -0
- package/packages/fleet/manifest.json +35 -11
- package/packages/fleet/manifest.schema.json +62 -10
- package/packages/fleet/src/default-agent-file.ts +3 -3
- package/packages/fleet/src/install-adl-package.ts +73 -15
- package/packages/fleet/src/install-embedding-model.ts +13 -13
- package/packages/fleet/src/managed-settings.ts +7 -7
- package/packages/fleet/src/repo-maintain.ts +91 -55
- package/packages/fleet/src/scratch-reap.ts +5 -5
- package/packages/fleet/src/vscode-settings.ts +5 -5
- package/packages/repo-controls/README.md +141 -43
- package/packages/repo-controls/assets/promote.ts +4 -4
- package/packages/repo-controls/assets/reconcile-guard.sh +10 -46
- package/packages/repo-controls/assets/version-bump.sh +31 -84
- package/packages/repo-controls/bunfig.toml +6 -7
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +5 -6
- package/packages/repo-controls/package/adl-compliance/conftest.py +3 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +4 -16
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +9 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/notes.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/check.py +266 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.hidden.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/data.csv +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/diagram.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/mock.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/sub/nested.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p1.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p2.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p3.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p4.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p5.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/build.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/overview.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/install.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/start.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-001-tenancy.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-002-carrier.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-003-cli.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-old.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-older.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-01.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-02.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-03.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-04.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-05.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-06.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-07.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-08.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-09.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-10.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-11.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-12.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-13.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-14.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-15.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-16.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/e.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/short/only.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/grants.yaml +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/test_check.py +345 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +18 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +35 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/check.py +91 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/CLAUDE.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/-leading-hyphen.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/double--hyphen.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/getting started.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/nested/page-here.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/trailing-hyphen-.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/user_profile.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/UI-CONTRACT.png +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/diagram.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/index.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/notes.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/reference.html +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/2026-10-experiment.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +194 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +14 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +5 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-legacy/docs/compliance/index.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-type/docs/compliance/index.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +58 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +64 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +60 -9
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-clean/docs/notes.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-no-lines/docs/notes.md +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +9 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs-okf-profiles.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs/architecture/bad-status.md +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/experiment-ok.md +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-sources.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +14 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/created-date.md +21 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/research-shaped.md +19 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/operations/runbook.md +26 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +228 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +11 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +5 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +160 -44
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/c.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/register.py +127 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +265 -29
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +75 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +194 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +56 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +256 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +40 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +108 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +2 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +23 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +76 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +58 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +161 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +35 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +108 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +281 -61
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/absolute-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/backslash-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/baseline-is-a-list.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/boolean-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/dot-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/duplicate-path.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-file.yaml +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/float-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/negative-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-over-limit-baseline.yaml +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-schema-version.yaml +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/non-string-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/not-yaml.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/null-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/parent-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/string-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/top-level-is-a-list.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/trailing-slash.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/unknown-key.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/wrong-schema-version.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/zero-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/valid.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +758 -45
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +4 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +490 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +368 -55
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +4 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +523 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +1179 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +122 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +33 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +16 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +12 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +133 -147
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +133 -147
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +52 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +23 -4
- package/packages/repo-controls/package.json +2 -9
- package/packages/repo-controls/scripts/hooks/post-merge +13 -0
- package/packages/repo-controls/scripts/hooks/pre-push +3 -3
- package/packages/repo-controls/src/azdo-push.ts +64 -36
- package/packages/repo-controls/src/azure-devops-governance.ts +7 -9
- package/packages/repo-controls/src/azure-devops-remote.ts +1 -1
- package/packages/repo-controls/src/cli.ts +29 -8
- package/packages/repo-controls/src/compliance-config.ts +410 -2
- package/packages/repo-controls/src/config.ts +133 -60
- package/packages/repo-controls/src/constants.ts +13 -27
- package/packages/repo-controls/src/coverage-ratchet.ts +4 -4
- package/packages/repo-controls/src/doctor.ts +46 -19
- package/packages/repo-controls/src/gitignore-block.ts +5 -29
- package/packages/repo-controls/src/hook-log.ts +1 -2
- package/packages/repo-controls/src/hook.ts +201 -45
- package/packages/repo-controls/src/install.ts +14 -8
- package/packages/repo-controls/src/integrity.ts +3 -3
- package/packages/repo-controls/src/lfs.ts +111 -0
- package/packages/repo-controls/src/promote.ts +8 -8
- package/packages/repo-controls/src/runtime.ts +9 -1
- package/packages/repo-controls/src/workflows-drift.ts +4 -5
- package/packages/repo-controls/src/worktree-guard.ts +3 -3
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
"""REP-002 `LCK-002` grades the CONTENT of a `Cargo.lock`, not only cargo's exit code.
|
|
2
|
+
|
|
3
|
+
The hole this pins (measured through the real check on 2026-10-01, probe
|
|
4
|
+
`.scratchpad/rep002-scope/probe_stale.py`): the gate ran `cargo metadata --locked
|
|
5
|
+
--offline --no-deps` and reported `LCK-002` only when it exited non-zero. With
|
|
6
|
+
`--no-deps` cargo never resolves, so it never reads `Cargo.lock`: a lock that was
|
|
7
|
+
`version = 4` alone, one that was not TOML, and one with `version = 99` all came
|
|
8
|
+
out CLEAN, and only a missing or empty lock was ever reported.
|
|
9
|
+
|
|
10
|
+
The validation under test is deterministic Python (`tomllib`): it reads the lock,
|
|
11
|
+
the workspace manifest, the member manifests and the path-dependency manifests the
|
|
12
|
+
manifest text names, and nothing else. No cargo, no registry, no network.
|
|
13
|
+
|
|
14
|
+
Two kinds of test, and why:
|
|
15
|
+
|
|
16
|
+
* Most tests grade through the real dispatch path (`runner.run`) with the two
|
|
17
|
+
cargo subprocess calls replaced by a recorder that exits 0 (`cargo_accepts`),
|
|
18
|
+
the same technique `test_declared_paths.py` uses for the cargo cwd pin. The
|
|
19
|
+
content validation never asks cargo anything, so these tests need no toolchain
|
|
20
|
+
and cannot depend on a machine's registry cache; the recorder also pins that
|
|
21
|
+
cargo is asked exactly the two questions it always was.
|
|
22
|
+
* A few tests (`requires_cargo`) run the REAL `cargo metadata --no-deps` against
|
|
23
|
+
hand-written manifests, so the integration with the real command is proven too.
|
|
24
|
+
|
|
25
|
+
Fixtures are hand-written text, never generated by cargo: a valid lock is TOML
|
|
26
|
+
(`version = 3`, `[[package]]` with `name`/`version`/`source`/`checksum`), and a
|
|
27
|
+
path package (a workspace member, a path dependency) has no `source`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import shutil
|
|
33
|
+
from collections.abc import Mapping
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from types import SimpleNamespace
|
|
36
|
+
from typing import NamedTuple
|
|
37
|
+
|
|
38
|
+
import pytest
|
|
39
|
+
from fixture_trees import fixture_trees
|
|
40
|
+
from helpers import corpus_files
|
|
41
|
+
|
|
42
|
+
from adl_compliance import runner
|
|
43
|
+
from adl_compliance.checks.repository.lockfile_integrity import check as module
|
|
44
|
+
from adl_compliance.core.verdict import Outcome, Verdict
|
|
45
|
+
from adl_compliance.policy.registry import load
|
|
46
|
+
|
|
47
|
+
ROW = load().by_id("REP-002")
|
|
48
|
+
|
|
49
|
+
requires_cargo = pytest.mark.skipif(shutil.which("cargo") is None, reason="cargo is not installed")
|
|
50
|
+
|
|
51
|
+
REGISTRY = "registry+https://github.com/rust-lang/crates.io-index"
|
|
52
|
+
HEADER = "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n"
|
|
53
|
+
FIX = (
|
|
54
|
+
"run: (cd backend && cargo update) to resync backend/Cargo.lock with "
|
|
55
|
+
"backend/Cargo.toml, then commit the result"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------- the fixtures
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Pkg(NamedTuple):
|
|
63
|
+
name: str
|
|
64
|
+
version: str = "1.0.0"
|
|
65
|
+
path: bool = False
|
|
66
|
+
"""A path package (workspace member, path dependency): no `source` in the lock."""
|
|
67
|
+
needs: tuple[str, ...] = ()
|
|
68
|
+
"""The strings of this package's `dependencies = [...]`, verbatim."""
|
|
69
|
+
|
|
70
|
+
def text(self) -> str:
|
|
71
|
+
lines = ["[[package]]", f'name = "{self.name}"', f'version = "{self.version}"']
|
|
72
|
+
if not self.path:
|
|
73
|
+
lines += [f'source = "{REGISTRY}"', f'checksum = "{"0" * 64}"']
|
|
74
|
+
if self.needs:
|
|
75
|
+
lines += ["dependencies = [", *(f' "{need}",' for need in self.needs), "]"]
|
|
76
|
+
return "\n".join(lines) + "\n"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def lock_text(packages: tuple[Pkg, ...], *, version: int | None = 3) -> str:
|
|
80
|
+
head = HEADER + (f"version = {version}\n" if version is not None else "")
|
|
81
|
+
return head + "\n" + "\n".join(pkg.text() for pkg in packages)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def without(packages: tuple[Pkg, ...], *names: str) -> tuple[Pkg, ...]:
|
|
85
|
+
"""What `cargo update` would NOT write if `names` were never needed: the packages are gone
|
|
86
|
+
and no remaining package lists them either."""
|
|
87
|
+
return tuple(
|
|
88
|
+
p._replace(needs=tuple(n for n in p.needs if n.partition(" ")[0] not in names))
|
|
89
|
+
for p in packages
|
|
90
|
+
if p.name not in names
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Shape(NamedTuple):
|
|
95
|
+
"""A manifest tree (paths relative to the cargo workspace) and the lock packages it needs.
|
|
96
|
+
|
|
97
|
+
A member's `needs` are the direct dependencies of its manifests, as cargo writes them."""
|
|
98
|
+
|
|
99
|
+
files: dict[str, str]
|
|
100
|
+
packages: tuple[Pkg, ...]
|
|
101
|
+
|
|
102
|
+
def lock(self, *, drop: tuple[str, ...] = (), swap: Mapping[str, Pkg] | None = None) -> str:
|
|
103
|
+
swap = swap or {}
|
|
104
|
+
kept = without(self.packages, *drop)
|
|
105
|
+
return lock_text(tuple(swap.get(p.name, p) for p in kept))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
SINGLE = Shape(
|
|
109
|
+
files={
|
|
110
|
+
"Cargo.toml": """\
|
|
111
|
+
[package]
|
|
112
|
+
name = "app"
|
|
113
|
+
version = "0.1.0"
|
|
114
|
+
edition = "2021"
|
|
115
|
+
|
|
116
|
+
[dependencies]
|
|
117
|
+
serde = "1"
|
|
118
|
+
sj = { package = "serde_json_fork", version = "1" }
|
|
119
|
+
opt = { version = "1", optional = true }
|
|
120
|
+
|
|
121
|
+
[dev-dependencies]
|
|
122
|
+
pretty_assertions = "1"
|
|
123
|
+
|
|
124
|
+
[build-dependencies]
|
|
125
|
+
cc = "1"
|
|
126
|
+
|
|
127
|
+
[target.'cfg(windows)'.dependencies]
|
|
128
|
+
winapi = "0.3"
|
|
129
|
+
|
|
130
|
+
[target.'cfg(unix)'.build-dependencies]
|
|
131
|
+
pkg-config = "0.3"
|
|
132
|
+
|
|
133
|
+
[target.'cfg(unix)'.dev-dependencies]
|
|
134
|
+
tempfile = "3"
|
|
135
|
+
"""
|
|
136
|
+
},
|
|
137
|
+
packages=(
|
|
138
|
+
Pkg(
|
|
139
|
+
"app",
|
|
140
|
+
"0.1.0",
|
|
141
|
+
path=True,
|
|
142
|
+
needs=(
|
|
143
|
+
"serde",
|
|
144
|
+
"serde_json_fork",
|
|
145
|
+
"opt",
|
|
146
|
+
"pretty_assertions",
|
|
147
|
+
"cc",
|
|
148
|
+
"winapi",
|
|
149
|
+
"pkg-config",
|
|
150
|
+
"tempfile",
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
Pkg("serde"),
|
|
154
|
+
Pkg("serde_json_fork"),
|
|
155
|
+
Pkg("opt"),
|
|
156
|
+
Pkg("pretty_assertions"),
|
|
157
|
+
Pkg("cc"),
|
|
158
|
+
Pkg("winapi", "0.3.9"),
|
|
159
|
+
Pkg("pkg-config", "0.3.30"),
|
|
160
|
+
Pkg("tempfile", "3.10.0"),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# (real package name, the key it is declared under, the section it is declared in)
|
|
165
|
+
SINGLE_DEPENDENCIES = [
|
|
166
|
+
pytest.param("serde", "serde", "[dependencies]", id="normal"),
|
|
167
|
+
pytest.param("serde_json_fork", "sj", "[dependencies]", id="renamed via package="),
|
|
168
|
+
pytest.param("opt", "opt", "[dependencies]", id="optional"),
|
|
169
|
+
pytest.param("pretty_assertions", "pretty_assertions", "[dev-dependencies]", id="dev"),
|
|
170
|
+
pytest.param("cc", "cc", "[build-dependencies]", id="build"),
|
|
171
|
+
pytest.param("winapi", "winapi", "[target.'cfg(windows)'.dependencies]", id="target normal"),
|
|
172
|
+
pytest.param(
|
|
173
|
+
"pkg-config", "pkg-config", "[target.'cfg(unix)'.build-dependencies]", id="target build"
|
|
174
|
+
),
|
|
175
|
+
pytest.param(
|
|
176
|
+
"tempfile", "tempfile", "[target.'cfg(unix)'.dev-dependencies]", id="target dev"
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
VIRTUAL = Shape(
|
|
181
|
+
files={
|
|
182
|
+
"Cargo.toml": """\
|
|
183
|
+
[workspace]
|
|
184
|
+
resolver = "2"
|
|
185
|
+
members = ["crates/*"]
|
|
186
|
+
exclude = ["crates/skipped"]
|
|
187
|
+
|
|
188
|
+
[workspace.package]
|
|
189
|
+
version = "0.3.0"
|
|
190
|
+
edition = "2021"
|
|
191
|
+
|
|
192
|
+
[workspace.dependencies]
|
|
193
|
+
serde = { version = "1", features = ["derive"] }
|
|
194
|
+
log = "0.4"
|
|
195
|
+
renamed = { package = "real_name", version = "2" }
|
|
196
|
+
core-lib = { path = "crates/core-lib" }
|
|
197
|
+
unused = "1"
|
|
198
|
+
""",
|
|
199
|
+
"crates/core-lib/Cargo.toml": """\
|
|
200
|
+
[package]
|
|
201
|
+
name = "core-lib"
|
|
202
|
+
version.workspace = true
|
|
203
|
+
edition.workspace = true
|
|
204
|
+
""",
|
|
205
|
+
"crates/web/Cargo.toml": """\
|
|
206
|
+
[package]
|
|
207
|
+
name = "web"
|
|
208
|
+
version = "0.1.0"
|
|
209
|
+
edition = "2021"
|
|
210
|
+
|
|
211
|
+
[dependencies]
|
|
212
|
+
serde.workspace = true
|
|
213
|
+
core-lib = { workspace = true }
|
|
214
|
+
renamed = { workspace = true, optional = true }
|
|
215
|
+
|
|
216
|
+
[dev-dependencies]
|
|
217
|
+
log = { workspace = true }
|
|
218
|
+
""",
|
|
219
|
+
"crates/skipped/Cargo.toml": """\
|
|
220
|
+
[package]
|
|
221
|
+
name = "skipped"
|
|
222
|
+
version = "9.9.9"
|
|
223
|
+
edition = "2021"
|
|
224
|
+
|
|
225
|
+
[dependencies]
|
|
226
|
+
only-skipped = "1"
|
|
227
|
+
""",
|
|
228
|
+
},
|
|
229
|
+
packages=(
|
|
230
|
+
Pkg("core-lib", "0.3.0", path=True),
|
|
231
|
+
Pkg("web", "0.1.0", path=True, needs=("serde", "core-lib", "real_name", "log")),
|
|
232
|
+
Pkg("serde"),
|
|
233
|
+
Pkg("log", "0.4.21"),
|
|
234
|
+
Pkg("real_name", "2.0.0"),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# A root package that is also a workspace; `helper` is a path dependency living inside the
|
|
239
|
+
# root and listed nowhere in `members`: cargo makes it a member, so ITS dependencies are locked.
|
|
240
|
+
IMPLICIT = Shape(
|
|
241
|
+
files={
|
|
242
|
+
"Cargo.toml": """\
|
|
243
|
+
[package]
|
|
244
|
+
name = "root"
|
|
245
|
+
version = "0.1.0"
|
|
246
|
+
edition = "2021"
|
|
247
|
+
|
|
248
|
+
[workspace]
|
|
249
|
+
members = ["member-a"]
|
|
250
|
+
|
|
251
|
+
[dependencies]
|
|
252
|
+
h = { path = "helper", package = "helper" }
|
|
253
|
+
""",
|
|
254
|
+
"member-a/Cargo.toml": """\
|
|
255
|
+
[package]
|
|
256
|
+
name = "member-a"
|
|
257
|
+
version = "0.2.0"
|
|
258
|
+
edition = "2021"
|
|
259
|
+
|
|
260
|
+
[dependencies]
|
|
261
|
+
shared = { path = "../../shared" }
|
|
262
|
+
""",
|
|
263
|
+
"helper/Cargo.toml": """\
|
|
264
|
+
[package]
|
|
265
|
+
name = "helper"
|
|
266
|
+
version = "0.4.0"
|
|
267
|
+
edition = "2021"
|
|
268
|
+
|
|
269
|
+
[dependencies]
|
|
270
|
+
helper-dep = "1"
|
|
271
|
+
""",
|
|
272
|
+
"../shared/Cargo.toml": """\
|
|
273
|
+
[package]
|
|
274
|
+
name = "shared"
|
|
275
|
+
version = "0.5.0"
|
|
276
|
+
edition = "2021"
|
|
277
|
+
""",
|
|
278
|
+
},
|
|
279
|
+
packages=(
|
|
280
|
+
Pkg("root", "0.1.0", path=True, needs=("helper",)),
|
|
281
|
+
Pkg("member-a", "0.2.0", path=True, needs=("shared",)),
|
|
282
|
+
Pkg("helper", "0.4.0", path=True, needs=("helper-dep",)),
|
|
283
|
+
Pkg("shared", "0.5.0", path=True),
|
|
284
|
+
Pkg("helper-dep"),
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
SHAPES = [
|
|
289
|
+
pytest.param(SINGLE, id="single package, every dependency table"),
|
|
290
|
+
pytest.param(VIRTUAL, id="virtual workspace, members glob + exclude + inheritance"),
|
|
291
|
+
pytest.param(IMPLICIT, id="root package and workspace, implicit path-dependency member"),
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str = "backend") -> None:
|
|
296
|
+
"""Write a cargo workspace under `root/where` plus the TypeScript lock the gate also wants.
|
|
297
|
+
|
|
298
|
+
A manifest with a `[package]` gets a `src/lib.rs`, so the very same files are also valid
|
|
299
|
+
input to the real `cargo metadata`. `where` is the cargo workspace directory.
|
|
300
|
+
"""
|
|
301
|
+
base = root / where
|
|
302
|
+
for relative, body in files.items():
|
|
303
|
+
target = (base / relative).resolve()
|
|
304
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
target.write_text(body, encoding="utf-8")
|
|
306
|
+
if "[package]" in body:
|
|
307
|
+
(target.parent / "src").mkdir(exist_ok=True)
|
|
308
|
+
(target.parent / "src" / "lib.rs").write_text("", encoding="utf-8")
|
|
309
|
+
lockfile = base / "Cargo.lock"
|
|
310
|
+
if isinstance(lock, bytes):
|
|
311
|
+
lockfile.write_bytes(lock)
|
|
312
|
+
else:
|
|
313
|
+
lockfile.write_text(lock, encoding="utf-8")
|
|
314
|
+
(root / "frontend").mkdir(exist_ok=True)
|
|
315
|
+
(root / "frontend" / "bun.lock").write_text("# fixture lock\n", encoding="utf-8")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def grade(
|
|
319
|
+
root: Path, *, declarations: Mapping[str, str] | None = None, trees: Mapping[str, str] | None = None
|
|
320
|
+
) -> Verdict:
|
|
321
|
+
"""The real dispatch path, whole tree, exhaustive."""
|
|
322
|
+
trees = fixture_trees() if trees is None else trees
|
|
323
|
+
files = corpus_files(root)
|
|
324
|
+
return runner.run(
|
|
325
|
+
ROW,
|
|
326
|
+
root,
|
|
327
|
+
files,
|
|
328
|
+
exhaustive=True,
|
|
329
|
+
universe=files,
|
|
330
|
+
declarations=declarations or {},
|
|
331
|
+
trees=trees,
|
|
332
|
+
declared_trees=trees,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def messages(verdict: Verdict) -> list[str]:
|
|
337
|
+
return [finding.message for finding in verdict.findings]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def stale_lock(tmp_path: Path, files: Mapping[str, str], lock: str | bytes) -> Verdict:
|
|
341
|
+
lay(tmp_path, files, lock)
|
|
342
|
+
return grade(tmp_path)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.fixture
|
|
346
|
+
def cargo_accepts(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]:
|
|
347
|
+
"""Both cargo calls succeed; returns every argv the check spawned."""
|
|
348
|
+
argvs: list[list[str]] = []
|
|
349
|
+
|
|
350
|
+
def fake_run(argv: list[str], *, cwd: Path, **_: object) -> object:
|
|
351
|
+
argvs.append(argv)
|
|
352
|
+
return SimpleNamespace(returncode=0)
|
|
353
|
+
|
|
354
|
+
monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
|
|
355
|
+
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
|
356
|
+
return argvs
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# --------------------------------------------- the three locks the previous probe measured
|
|
360
|
+
|
|
361
|
+
GARBAGE = [
|
|
362
|
+
pytest.param("version = 4\n", "backend/Cargo.lock lists no packages", id="version = 4 alone"),
|
|
363
|
+
pytest.param("this is [[ not a lockfile\n", "backend/Cargo.lock is not valid TOML", id="not TOML"),
|
|
364
|
+
pytest.param(
|
|
365
|
+
"version = 99\n", "backend/Cargo.lock version 99 is not supported", id="version = 99"
|
|
366
|
+
),
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@pytest.mark.parametrize(("body", "expected"), GARBAGE)
|
|
371
|
+
def test_a_garbage_lock_is_lck_002(
|
|
372
|
+
tmp_path: Path, cargo_accepts: list[list[str]], body: str, expected: str
|
|
373
|
+
) -> None:
|
|
374
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
375
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
376
|
+
assert {f.rule for f in verdict.findings} == {"LCK-002"}
|
|
377
|
+
assert {f.path for f in verdict.findings} == {"backend/Cargo.lock"}
|
|
378
|
+
assert any(message.startswith(expected) for message in messages(verdict)), messages(verdict)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@requires_cargo
|
|
382
|
+
@pytest.mark.parametrize(("body", "expected"), GARBAGE)
|
|
383
|
+
def test_a_garbage_lock_is_lck_002_with_the_real_cargo_command(
|
|
384
|
+
tmp_path: Path, body: str, expected: str
|
|
385
|
+
) -> None:
|
|
386
|
+
"""The very bodies the real `cargo metadata --no-deps` accepted: the gate now rejects them."""
|
|
387
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
388
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
389
|
+
assert {f.rule for f in verdict.findings} == {"LCK-002"}
|
|
390
|
+
assert any(message.startswith(expected) for message in messages(verdict)), messages(verdict)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_a_lock_that_is_not_utf8_is_lck_002(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
|
|
394
|
+
verdict = stale_lock(tmp_path, SINGLE.files, b"\xff\xfe\x00 not text \x80")
|
|
395
|
+
assert messages(verdict) == ["backend/Cargo.lock is not valid UTF-8 text"]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------- the lock's structure
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@pytest.mark.parametrize("version", [3, 4, None], ids=["version = 3", "version = 4", "no version (V1/V2)"])
|
|
402
|
+
def test_a_supported_lock_version_is_clean(
|
|
403
|
+
tmp_path: Path, cargo_accepts: list[list[str]], version: int | None
|
|
404
|
+
) -> None:
|
|
405
|
+
"""cargo 0.99.0, `EncodableResolve::into_resolve`: `Some(4)`, `Some(3)` and, with no
|
|
406
|
+
`version` key at all, the legacy V1/V2 encodings are read; anything else is refused."""
|
|
407
|
+
body = lock_text(SINGLE.packages, version=version)
|
|
408
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
409
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@pytest.mark.parametrize(
|
|
413
|
+
"line",
|
|
414
|
+
["version = 1", "version = 2", "version = 5", "version = 0", "version = -3", "version = 99"],
|
|
415
|
+
)
|
|
416
|
+
def test_an_unsupported_integer_lock_version_is_reported(
|
|
417
|
+
tmp_path: Path, cargo_accepts: list[list[str]], line: str
|
|
418
|
+
) -> None:
|
|
419
|
+
number = line.split("= ")[1]
|
|
420
|
+
body = HEADER + line + "\n\n" + "\n".join(p.text() for p in SINGLE.packages)
|
|
421
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
422
|
+
assert messages(verdict) == [
|
|
423
|
+
f"backend/Cargo.lock version {number} is not supported (cargo reads lock versions 3 and 4)"
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@pytest.mark.parametrize(
|
|
428
|
+
("line", "shown"),
|
|
429
|
+
[('version = "4"', "'4'"), ("version = 4.0", "4.0"), ("version = true", "True")],
|
|
430
|
+
ids=["string", "float", "boolean"],
|
|
431
|
+
)
|
|
432
|
+
def test_a_lock_version_that_is_not_an_integer_is_reported(
|
|
433
|
+
tmp_path: Path, cargo_accepts: list[list[str]], line: str, shown: str
|
|
434
|
+
) -> None:
|
|
435
|
+
body = HEADER + line + "\n\n" + "\n".join(p.text() for p in SINGLE.packages)
|
|
436
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
437
|
+
assert messages(verdict) == [
|
|
438
|
+
f"backend/Cargo.lock version {shown} is not supported (cargo reads lock versions 3 and 4)"
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@pytest.mark.parametrize(
|
|
443
|
+
"body",
|
|
444
|
+
[
|
|
445
|
+
pytest.param("version = 3\n", id="no [[package]]"),
|
|
446
|
+
pytest.param("version = 3\npackage = []\n", id="empty package array"),
|
|
447
|
+
],
|
|
448
|
+
)
|
|
449
|
+
def test_a_lock_without_packages_is_reported(
|
|
450
|
+
tmp_path: Path, cargo_accepts: list[list[str]], body: str
|
|
451
|
+
) -> None:
|
|
452
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
453
|
+
assert messages(verdict) == [
|
|
454
|
+
"backend/Cargo.lock lists no packages ([[package]] is absent or empty)"
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def test_an_empty_package_table_is_reported(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
|
|
459
|
+
body = "version = 3\n\n[[package]]\n"
|
|
460
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
461
|
+
assert messages(verdict) == [
|
|
462
|
+
"backend/Cargo.lock has a [[package]] entry (number 1) without a string name"
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@pytest.mark.parametrize(
|
|
467
|
+
("entry", "expected"),
|
|
468
|
+
[
|
|
469
|
+
pytest.param('version = "1.0.0"\n', "without a string name", id="no name"),
|
|
470
|
+
pytest.param('name = "x"\n', "without a string version", id="no version"),
|
|
471
|
+
pytest.param('name = 7\nversion = "1.0.0"\n', "without a string name", id="name not a string"),
|
|
472
|
+
pytest.param('name = "x"\nversion = 1\n', "without a string version", id="version not a string"),
|
|
473
|
+
],
|
|
474
|
+
)
|
|
475
|
+
def test_a_package_entry_without_name_or_version_is_reported(
|
|
476
|
+
tmp_path: Path, cargo_accepts: list[list[str]], entry: str, expected: str
|
|
477
|
+
) -> None:
|
|
478
|
+
good = "\n".join(p.text() for p in SINGLE.packages)
|
|
479
|
+
body = HEADER + "version = 3\n\n" + good + "\n[[package]]\n" + entry
|
|
480
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
481
|
+
number = len(SINGLE.packages) + 1
|
|
482
|
+
assert messages(verdict) == [f"backend/Cargo.lock has a [[package]] entry (number {number}) {expected}"]
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_a_package_entry_that_is_not_a_table_is_reported(
|
|
486
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
487
|
+
) -> None:
|
|
488
|
+
verdict = stale_lock(tmp_path, SINGLE.files, "version = 3\npackage = [1]\n")
|
|
489
|
+
assert messages(verdict) == [
|
|
490
|
+
"backend/Cargo.lock has a [[package]] entry (number 1) that is not a table"
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ------------------------------------- the lock must hold every package it says it needs
|
|
495
|
+
|
|
496
|
+
# The shapes cargo writes `dependencies = [...]` in (V1 to V4): a bare name when only one
|
|
497
|
+
# version of it is locked, `name version` when several are, `name version (source)` when
|
|
498
|
+
# several sources are. A package that was removed from the lock but is still listed by a
|
|
499
|
+
# dependent is a lock cargo refuses to parse, and it passed too.
|
|
500
|
+
CLOSED = (
|
|
501
|
+
Pkg("app", "0.1.0", path=True, needs=("serde", "twice 1.0.0", f"other 2.0.0 ({REGISTRY})")),
|
|
502
|
+
Pkg("serde", needs=("transitive",)),
|
|
503
|
+
Pkg("transitive"),
|
|
504
|
+
Pkg("twice", "1.0.0"),
|
|
505
|
+
Pkg("twice", "2.0.0"),
|
|
506
|
+
Pkg("other", "2.0.0"),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_a_lock_closed_over_its_own_dependencies_is_clean(
|
|
511
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
512
|
+
) -> None:
|
|
513
|
+
manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
|
|
514
|
+
verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(CLOSED))
|
|
515
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@pytest.mark.parametrize(
|
|
519
|
+
("removed", "listed"),
|
|
520
|
+
[
|
|
521
|
+
pytest.param("transitive", "transitive", id="a transitive package removed"),
|
|
522
|
+
pytest.param("other", f"other 2.0.0 ({REGISTRY})", id="a package named with its source removed"),
|
|
523
|
+
],
|
|
524
|
+
)
|
|
525
|
+
def test_a_package_the_lock_lists_as_a_dependency_but_does_not_hold_is_reported(
|
|
526
|
+
tmp_path: Path, cargo_accepts: list[list[str]], removed: str, listed: str
|
|
527
|
+
) -> None:
|
|
528
|
+
manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
|
|
529
|
+
body = lock_text(tuple(p for p in CLOSED if p.name != removed))
|
|
530
|
+
verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, body)
|
|
531
|
+
[message] = messages(verdict)
|
|
532
|
+
assert message.startswith(f"backend/Cargo.lock lists dependency '{listed}' of package '")
|
|
533
|
+
assert message.endswith("but has no such package")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_a_dependency_naming_a_version_the_lock_does_not_hold_is_reported(
|
|
537
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
538
|
+
) -> None:
|
|
539
|
+
manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
|
|
540
|
+
body = lock_text(tuple(p for p in CLOSED if (p.name, p.version) != ("twice", "1.0.0")))
|
|
541
|
+
verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, body)
|
|
542
|
+
assert messages(verdict) == [
|
|
543
|
+
"backend/Cargo.lock lists dependency 'twice 1.0.0' of package 'app' 0.1.0 but has no such package"
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_a_missing_package_listed_by_two_dependents_is_reported_once(
|
|
548
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
549
|
+
) -> None:
|
|
550
|
+
manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "1"\n'
|
|
551
|
+
packages = (
|
|
552
|
+
Pkg("app", "0.1.0", path=True, needs=("serde", "other")),
|
|
553
|
+
Pkg("serde", needs=("ghost",)),
|
|
554
|
+
Pkg("other", needs=("ghost",)),
|
|
555
|
+
)
|
|
556
|
+
verdict = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(packages))
|
|
557
|
+
assert len(messages(verdict)) == 1, messages(verdict)
|
|
558
|
+
assert "lists dependency 'ghost'" in messages(verdict)[0]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def test_a_dependencies_entry_that_is_not_a_list_of_strings_is_reported(
|
|
562
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
563
|
+
) -> None:
|
|
564
|
+
body = HEADER + 'version = 3\n\n[[package]]\nname = "app"\nversion = "0.1.0"\ndependencies = "serde"\n'
|
|
565
|
+
verdict = stale_lock(tmp_path, SINGLE.files, body)
|
|
566
|
+
assert messages(verdict) == [
|
|
567
|
+
"backend/Cargo.lock has a [[package]] entry (number 1) whose `dependencies` is not a list of strings"
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# --------------------------------------------------------------------- good locks pass
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@pytest.mark.parametrize("shape", SHAPES)
|
|
575
|
+
def test_a_lock_that_satisfies_the_manifests_is_clean(
|
|
576
|
+
tmp_path: Path, cargo_accepts: list[list[str]], shape: Shape
|
|
577
|
+
) -> None:
|
|
578
|
+
verdict = stale_lock(tmp_path, shape.files, shape.lock())
|
|
579
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
580
|
+
assert verdict.findings == ()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@requires_cargo
|
|
584
|
+
@pytest.mark.parametrize("shape", SHAPES)
|
|
585
|
+
def test_a_lock_that_satisfies_the_manifests_is_clean_with_the_real_cargo_command(
|
|
586
|
+
tmp_path: Path, shape: Shape
|
|
587
|
+
) -> None:
|
|
588
|
+
verdict = stale_lock(tmp_path, shape.files, shape.lock())
|
|
589
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# -------------------------------------------------- a direct dependency missing is found
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
|
|
596
|
+
def test_a_direct_dependency_missing_from_the_lock_is_reported(
|
|
597
|
+
tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
|
|
598
|
+
) -> None:
|
|
599
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=(name,)))
|
|
600
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
601
|
+
assert messages(verdict) == [
|
|
602
|
+
f"backend/Cargo.lock lists no package '{name}' "
|
|
603
|
+
f"(dependency '{key}' in {section} of backend/Cargo.toml)"
|
|
604
|
+
]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@requires_cargo
|
|
608
|
+
@pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
|
|
609
|
+
def test_a_direct_dependency_missing_from_the_lock_is_reported_with_the_real_cargo_command(
|
|
610
|
+
tmp_path: Path, name: str, key: str, section: str
|
|
611
|
+
) -> None:
|
|
612
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=(name,)))
|
|
613
|
+
assert [f.rule for f in verdict.findings] == ["LCK-002"], verdict.render()
|
|
614
|
+
assert f"lists no package '{name}'" in verdict.findings[0].message
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@pytest.mark.parametrize(("name", "key", "section"), SINGLE_DEPENDENCIES)
|
|
618
|
+
def test_a_dependency_the_lock_holds_only_for_someone_else_is_reported(
|
|
619
|
+
tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
|
|
620
|
+
) -> None:
|
|
621
|
+
"""The commonest stale lock: a manifest gains a dependency that is already in the lock because
|
|
622
|
+
another package brings it (`tokio`, `serde`). The package is in the lock; the MEMBER's own entry
|
|
623
|
+
does not list it, so `cargo build --locked` would refuse the lock. Presence anywhere is not enough."""
|
|
624
|
+
app, *rest = SINGLE.packages
|
|
625
|
+
app = app._replace(needs=tuple(n for n in app.needs if n != name))
|
|
626
|
+
carrier = Pkg("carrier", needs=(name,))
|
|
627
|
+
verdict = stale_lock(tmp_path, SINGLE.files, lock_text((app, *rest, carrier)))
|
|
628
|
+
assert messages(verdict) == [
|
|
629
|
+
f"backend/Cargo.lock package 'app' 0.1.0 does not list '{name}' as a dependency "
|
|
630
|
+
f"({section} of backend/Cargo.toml declares dependency '{key}')"
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def test_a_workspace_member_whose_entry_lacks_an_inherited_dependency_is_reported(
|
|
635
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
636
|
+
) -> None:
|
|
637
|
+
web = next(p for p in VIRTUAL.packages if p.name == "web")
|
|
638
|
+
packages = tuple(
|
|
639
|
+
p._replace(needs=tuple(n for n in p.needs if n != "serde")) if p is web else p
|
|
640
|
+
for p in VIRTUAL.packages
|
|
641
|
+
)
|
|
642
|
+
packages = (*packages, Pkg("carrier", needs=("serde",)))
|
|
643
|
+
verdict = stale_lock(tmp_path, VIRTUAL.files, lock_text(packages))
|
|
644
|
+
assert messages(verdict) == [
|
|
645
|
+
"backend/Cargo.lock package 'web' 0.1.0 does not list 'serde' as a dependency "
|
|
646
|
+
"([dependencies] of backend/crates/web/Cargo.toml declares dependency 'serde')"
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def test_a_dependency_listed_with_its_version_is_found_by_name(
|
|
651
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
652
|
+
) -> None:
|
|
653
|
+
"""cargo writes `name version` when two versions of a crate are locked."""
|
|
654
|
+
app, *rest = SINGLE.packages
|
|
655
|
+
app = app._replace(needs=tuple("serde 1.0.0" if n == "serde" else n for n in app.needs))
|
|
656
|
+
packages = (app, *rest, Pkg("serde", "2.0.0"))
|
|
657
|
+
verdict = stale_lock(tmp_path, SINGLE.files, lock_text(packages))
|
|
658
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_a_package_that_dev_depends_on_itself_is_not_asked_to_list_itself(
|
|
662
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
663
|
+
) -> None:
|
|
664
|
+
manifest = (
|
|
665
|
+
'[package]\nname = "app"\nversion = "0.1.0"\n\n[features]\nextra = []\n\n'
|
|
666
|
+
'[dev-dependencies]\napp = { path = ".", features = ["extra"] }\n'
|
|
667
|
+
)
|
|
668
|
+
verdict = stale_lock(
|
|
669
|
+
tmp_path, {"Cargo.toml": manifest}, lock_text((Pkg("app", "0.1.0", path=True),))
|
|
670
|
+
)
|
|
671
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def test_a_renamed_dependency_is_judged_by_its_real_name_not_its_key(
|
|
675
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
676
|
+
) -> None:
|
|
677
|
+
"""`sj = { package = "serde_json_fork" }`: a lock listing a package called `sj` is NOT
|
|
678
|
+
the dependency, and one listing `serde_json_fork` is, with no `sj` anywhere."""
|
|
679
|
+
wrong = SINGLE.lock(drop=("serde_json_fork",)) + Pkg("sj").text()
|
|
680
|
+
verdict = stale_lock(tmp_path, SINGLE.files, wrong)
|
|
681
|
+
assert [m.split(" (")[0] for m in messages(verdict)] == [
|
|
682
|
+
"backend/Cargo.lock lists no package 'serde_json_fork'"
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def test_a_dependency_declared_as_its_own_table_is_judged(
|
|
687
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
688
|
+
) -> None:
|
|
689
|
+
manifest = (
|
|
690
|
+
'[package]\nname = "app"\nversion = "0.1.0"\n\n'
|
|
691
|
+
'[dependencies.tokio]\nversion = "1"\nfeatures = ["rt"]\n\n'
|
|
692
|
+
'[dev_dependencies]\nmockito = "1"\n\n'
|
|
693
|
+
'[build_dependencies]\nprost-build = "0.12"\n'
|
|
694
|
+
)
|
|
695
|
+
packages = (
|
|
696
|
+
Pkg("app", "0.1.0", path=True, needs=("tokio", "mockito", "prost-build")),
|
|
697
|
+
Pkg("tokio"),
|
|
698
|
+
Pkg("mockito"),
|
|
699
|
+
Pkg("prost-build"),
|
|
700
|
+
)
|
|
701
|
+
clean = stale_lock(tmp_path, {"Cargo.toml": manifest}, lock_text(packages))
|
|
702
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
703
|
+
gone = grade_without(tmp_path, packages, "tokio")
|
|
704
|
+
assert [m.split(" (")[0] for m in messages(gone)] == ["backend/Cargo.lock lists no package 'tokio'"]
|
|
705
|
+
gone = grade_without(tmp_path, packages, "mockito")
|
|
706
|
+
assert "in [dev_dependencies] of backend/Cargo.toml" in messages(gone)[0]
|
|
707
|
+
gone = grade_without(tmp_path, packages, "prost-build")
|
|
708
|
+
assert "in [build_dependencies] of backend/Cargo.toml" in messages(gone)[0]
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def grade_without(root: Path, packages: tuple[Pkg, ...], name: str) -> Verdict:
|
|
712
|
+
(root / "backend" / "Cargo.lock").write_text(lock_text(without(packages, name)), encoding="utf-8")
|
|
713
|
+
return grade(root)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def test_a_dependency_both_members_need_is_reported_once(
|
|
717
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
718
|
+
) -> None:
|
|
719
|
+
files = {
|
|
720
|
+
**VIRTUAL.files,
|
|
721
|
+
"crates/api/Cargo.toml": '[package]\nname = "api"\nversion = "0.1.0"\n\n[dependencies]\nserde.workspace = true\n',
|
|
722
|
+
}
|
|
723
|
+
packages = (*VIRTUAL.packages, Pkg("api", "0.1.0", path=True, needs=("serde",)))
|
|
724
|
+
body = lock_text(without(packages, "serde"))
|
|
725
|
+
verdict = stale_lock(tmp_path, files, body)
|
|
726
|
+
assert len(messages(verdict)) == 1, messages(verdict)
|
|
727
|
+
assert messages(verdict)[0].startswith("backend/Cargo.lock lists no package 'serde' (dependency 'serde' in [dependencies] of ")
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# ------------------------------------------------------------ a workspace member missing
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_the_root_package_missing_from_the_lock_is_reported(
|
|
734
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
735
|
+
) -> None:
|
|
736
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=("app",)))
|
|
737
|
+
assert messages(verdict) == [
|
|
738
|
+
"backend/Cargo.lock lists no path package 'app' (workspace member backend/Cargo.toml)"
|
|
739
|
+
]
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def test_a_member_whose_version_differs_is_reported(
|
|
743
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
744
|
+
) -> None:
|
|
745
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(swap={"app": Pkg("app", "0.0.9", path=True)}))
|
|
746
|
+
assert messages(verdict) == [
|
|
747
|
+
"backend/Cargo.lock lists 'app' at version 0.0.9 but workspace member "
|
|
748
|
+
"backend/Cargo.toml declares 0.1.0"
|
|
749
|
+
]
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def test_a_registry_package_of_the_same_name_does_not_satisfy_a_member(
|
|
753
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
754
|
+
) -> None:
|
|
755
|
+
"""A member is a path package; a crates.io `app 0.1.0` in the lock is somebody else's."""
|
|
756
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(swap={"app": Pkg("app", "0.1.0")}))
|
|
757
|
+
assert messages(verdict) == [
|
|
758
|
+
"backend/Cargo.lock lists no path package 'app' (workspace member backend/Cargo.toml)"
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@pytest.mark.parametrize("member", ["web", "core-lib"])
|
|
763
|
+
def test_a_virtual_workspace_member_missing_from_the_lock_is_reported(
|
|
764
|
+
tmp_path: Path, cargo_accepts: list[list[str]], member: str
|
|
765
|
+
) -> None:
|
|
766
|
+
"""One finding: `core-lib` is also a path dependency of `web`, and is reported once, as the member."""
|
|
767
|
+
verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=(member,)))
|
|
768
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
769
|
+
assert messages(verdict) == [
|
|
770
|
+
f"backend/Cargo.lock lists no path package '{member}' "
|
|
771
|
+
f"(workspace member backend/crates/{member}/Cargo.toml)"
|
|
772
|
+
]
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def test_a_member_version_inherited_from_the_workspace_is_the_one_compared(
|
|
776
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
777
|
+
) -> None:
|
|
778
|
+
"""`version.workspace = true` reads `[workspace.package].version` (0.3.0)."""
|
|
779
|
+
verdict = stale_lock(
|
|
780
|
+
tmp_path, VIRTUAL.files, VIRTUAL.lock(swap={"core-lib": Pkg("core-lib", "0.2.0", path=True)})
|
|
781
|
+
)
|
|
782
|
+
assert messages(verdict) == [
|
|
783
|
+
"backend/Cargo.lock lists 'core-lib' at version 0.2.0 but workspace member "
|
|
784
|
+
"backend/crates/core-lib/Cargo.toml declares 0.3.0"
|
|
785
|
+
]
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def test_a_member_with_no_version_is_locked_at_0_0_0(
|
|
789
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
790
|
+
) -> None:
|
|
791
|
+
files = {"Cargo.toml": '[package]\nname = "app"\nedition = "2021"\n'}
|
|
792
|
+
clean = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.0.0", path=True),)))
|
|
793
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
794
|
+
wrong = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.1.0", path=True),)))
|
|
795
|
+
assert "declares 0.0.0" in messages(wrong)[0]
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# --------------------------------------------------- members, glob, exclude, inheritance
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def test_the_excluded_member_is_not_required_and_removing_the_exclude_requires_it(
|
|
802
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
803
|
+
) -> None:
|
|
804
|
+
"""Negative control for `exclude`: `skipped` (and its `only-skipped` dependency) are in no
|
|
805
|
+
lock above and the gate is clean; take the `exclude` away and `skipped` becomes a member."""
|
|
806
|
+
clean = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock())
|
|
807
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
808
|
+
|
|
809
|
+
unexcluded = {
|
|
810
|
+
**VIRTUAL.files,
|
|
811
|
+
"Cargo.toml": VIRTUAL.files["Cargo.toml"].replace('exclude = ["crates/skipped"]\n', ""),
|
|
812
|
+
}
|
|
813
|
+
verdict = stale_lock(tmp_path, unexcluded, VIRTUAL.lock())
|
|
814
|
+
found = messages(verdict)
|
|
815
|
+
assert any("lists no path package 'skipped'" in m for m in found), found
|
|
816
|
+
assert any("lists no package 'only-skipped'" in m for m in found), found
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def test_a_workspace_dependency_no_member_uses_is_not_required(
|
|
820
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
821
|
+
) -> None:
|
|
822
|
+
"""`[workspace.dependencies] unused = "1"`: cargo locks only what a member uses."""
|
|
823
|
+
assert "unused" in VIRTUAL.files["Cargo.toml"]
|
|
824
|
+
verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock())
|
|
825
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@pytest.mark.parametrize(
|
|
829
|
+
("name", "key", "section"),
|
|
830
|
+
[
|
|
831
|
+
pytest.param("serde", "serde", "[dependencies]", id="inherited via workspace = true"),
|
|
832
|
+
pytest.param("real_name", "renamed", "[dependencies]", id="inherited, renamed, optional"),
|
|
833
|
+
pytest.param("log", "log", "[dev-dependencies]", id="inherited dev"),
|
|
834
|
+
],
|
|
835
|
+
)
|
|
836
|
+
def test_an_inherited_dependency_missing_from_the_lock_is_reported(
|
|
837
|
+
tmp_path: Path, cargo_accepts: list[list[str]], name: str, key: str, section: str
|
|
838
|
+
) -> None:
|
|
839
|
+
verdict = stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=(name,)))
|
|
840
|
+
assert messages(verdict) == [
|
|
841
|
+
f"backend/Cargo.lock lists no package '{name}' (dependency '{key}' in {section} of "
|
|
842
|
+
"backend/crates/web/Cargo.toml)"
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def test_members_listed_without_a_glob_are_judged(
|
|
847
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
848
|
+
) -> None:
|
|
849
|
+
files = {
|
|
850
|
+
**VIRTUAL.files,
|
|
851
|
+
"Cargo.toml": VIRTUAL.files["Cargo.toml"].replace(
|
|
852
|
+
'members = ["crates/*"]', 'members = ["crates/web", "crates/core-lib"]'
|
|
853
|
+
),
|
|
854
|
+
}
|
|
855
|
+
clean = stale_lock(tmp_path, files, VIRTUAL.lock())
|
|
856
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
857
|
+
gone = stale_lock(tmp_path, files, VIRTUAL.lock(drop=("web",)))
|
|
858
|
+
assert any("lists no path package 'web'" in m for m in messages(gone))
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def test_a_hidden_directory_matched_by_a_glob_is_a_member(
|
|
862
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
863
|
+
) -> None:
|
|
864
|
+
"""Rust's `glob` crate (what cargo expands `members` with) has
|
|
865
|
+
`require_literal_leading_dot = false` by default: `crates/*` matches `crates/.tools`."""
|
|
866
|
+
files = {
|
|
867
|
+
**VIRTUAL.files,
|
|
868
|
+
"crates/.tools/Cargo.toml": '[package]\nname = "tools"\nversion = "0.1.0"\n',
|
|
869
|
+
}
|
|
870
|
+
verdict = stale_lock(tmp_path, files, VIRTUAL.lock())
|
|
871
|
+
assert any("lists no path package 'tools'" in m for m in messages(verdict)), messages(verdict)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# ------------------------------------------------------------------------- path dependencies
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@pytest.mark.parametrize(
|
|
878
|
+
("missing", "expected"),
|
|
879
|
+
[
|
|
880
|
+
pytest.param("helper-dep", "lists no package 'helper-dep' (dependency 'helper-dep' in [dependencies] of backend/helper/Cargo.toml)", id="dependency of an implicit member"),
|
|
881
|
+
pytest.param("helper", "lists no path package 'helper'", id="the implicit member itself, under its real name"),
|
|
882
|
+
pytest.param("shared", "lists no path package 'shared' (dependency 'shared' in [dependencies] of backend/member-a/Cargo.toml)", id="path dependency outside the workspace root"),
|
|
883
|
+
],
|
|
884
|
+
)
|
|
885
|
+
def test_path_dependencies_are_judged_by_the_name_in_their_own_manifest(
|
|
886
|
+
tmp_path: Path, cargo_accepts: list[list[str]], missing: str, expected: str
|
|
887
|
+
) -> None:
|
|
888
|
+
"""`h = { path = "helper", package = "helper" }` and `shared = { path = "../../shared" }`:
|
|
889
|
+
the lock names the package its manifest declares. `helper` lives inside the workspace root
|
|
890
|
+
and is in no `members` list, so cargo makes it a member and locks its dependencies."""
|
|
891
|
+
verdict = stale_lock(tmp_path, IMPLICIT.files, IMPLICIT.lock(drop=(missing,)))
|
|
892
|
+
assert any(expected in m for m in messages(verdict)), messages(verdict)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def test_a_path_dependency_keyed_differently_from_its_package_is_judged_by_the_package(
|
|
896
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
897
|
+
) -> None:
|
|
898
|
+
files = {
|
|
899
|
+
"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nalias = { path = "real" }\n',
|
|
900
|
+
"real/Cargo.toml": '[package]\nname = "real-name"\nversion = "0.7.0"\n',
|
|
901
|
+
}
|
|
902
|
+
good = (Pkg("app", "0.1.0", path=True, needs=("real-name",)), Pkg("real-name", "0.7.0", path=True))
|
|
903
|
+
clean = stale_lock(tmp_path, files, lock_text(good))
|
|
904
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
905
|
+
keyed = (Pkg("app", "0.1.0", path=True, needs=("alias",)), Pkg("alias", "0.7.0", path=True))
|
|
906
|
+
wrong = stale_lock(tmp_path, files, lock_text(keyed))
|
|
907
|
+
assert [m.split(" (")[0] for m in messages(wrong)] == ["backend/Cargo.lock lists no path package 'real-name'"]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def test_a_registry_package_does_not_satisfy_an_implicit_member(
|
|
911
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
912
|
+
) -> None:
|
|
913
|
+
verdict = stale_lock(
|
|
914
|
+
tmp_path, IMPLICIT.files, IMPLICIT.lock(swap={"helper": Pkg("helper", "0.4.0")})
|
|
915
|
+
)
|
|
916
|
+
assert any("lists no path package 'helper'" in m for m in messages(verdict)), messages(verdict)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def test_a_registry_package_does_not_satisfy_a_path_dependency_outside_the_workspace(
|
|
920
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
921
|
+
) -> None:
|
|
922
|
+
"""`shared` is a path dependency and no member: only the dependency check can notice that the
|
|
923
|
+
lock holds a crates.io `shared 0.5.0` instead of the path package."""
|
|
924
|
+
verdict = stale_lock(
|
|
925
|
+
tmp_path, IMPLICIT.files, IMPLICIT.lock(swap={"shared": Pkg("shared", "0.5.0")})
|
|
926
|
+
)
|
|
927
|
+
assert messages(verdict) == [
|
|
928
|
+
"backend/Cargo.lock lists no path package 'shared' (dependency 'shared' in "
|
|
929
|
+
"[dependencies] of backend/member-a/Cargo.toml)"
|
|
930
|
+
]
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# --------------------------------------------------------- what the check cannot judge
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
UNJUDGEABLE = [
|
|
937
|
+
pytest.param(
|
|
938
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = 3\n'},
|
|
939
|
+
["'foo'", "backend/Cargo.toml", "[dependencies]"],
|
|
940
|
+
id="dependency spec that is neither a string nor a table",
|
|
941
|
+
),
|
|
942
|
+
pytest.param(
|
|
943
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { workspace = false }\n'},
|
|
944
|
+
["'foo'", "workspace = false"],
|
|
945
|
+
id="workspace = false",
|
|
946
|
+
),
|
|
947
|
+
pytest.param(
|
|
948
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { workspace = true }\n'},
|
|
949
|
+
["'foo'", "[workspace.dependencies]"],
|
|
950
|
+
id="workspace = true without a [workspace.dependencies] entry",
|
|
951
|
+
),
|
|
952
|
+
pytest.param(
|
|
953
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfoo = { path = "nowhere" }\n'},
|
|
954
|
+
["'foo'", "nowhere"],
|
|
955
|
+
id="path dependency whose manifest is not there",
|
|
956
|
+
),
|
|
957
|
+
pytest.param(
|
|
958
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n\n[target.\'cfg(unix)\'.dev-dependencies]\nbar = [1]\n'},
|
|
959
|
+
["'bar'", "[target.'cfg(unix)'.dev-dependencies]"],
|
|
960
|
+
id="target-specific dependency spec that is a list",
|
|
961
|
+
),
|
|
962
|
+
pytest.param(
|
|
963
|
+
{"Cargo.toml": 'dependencies = 3\n\n[package]\nname = "app"\nversion = "0.1.0"\n'},
|
|
964
|
+
["[dependencies]", "backend/Cargo.toml", "not a table"],
|
|
965
|
+
id="a dependency section that is not a table",
|
|
966
|
+
),
|
|
967
|
+
pytest.param(
|
|
968
|
+
{"Cargo.toml": '[workspace]\nmembers = ["crates/none-*"]\n'},
|
|
969
|
+
["crates/none-*", "matches no directory"],
|
|
970
|
+
id="members entry that matches nothing",
|
|
971
|
+
),
|
|
972
|
+
pytest.param(
|
|
973
|
+
{"Cargo.toml": '[workspace]\nmembers = ["crates/a"]\n', "crates/a/readme.txt": "no manifest here\n"},
|
|
974
|
+
["crates/a", "no Cargo.toml"],
|
|
975
|
+
id="member directory without a Cargo.toml",
|
|
976
|
+
),
|
|
977
|
+
pytest.param(
|
|
978
|
+
{"Cargo.toml": '[workspace]\nmembers = ["crates/*"]\nexclude = ["crates/g*"]\n', "crates/a/Cargo.toml": '[package]\nname = "a"\nversion = "0.1.0"\n'},
|
|
979
|
+
["exclude", "crates/g*"],
|
|
980
|
+
id="exclude entry with glob syntax",
|
|
981
|
+
),
|
|
982
|
+
pytest.param(
|
|
983
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = "0.1.0"\n[dependencies\n'},
|
|
984
|
+
["backend/Cargo.toml", "not valid TOML"],
|
|
985
|
+
id="manifest that is not TOML",
|
|
986
|
+
),
|
|
987
|
+
pytest.param(
|
|
988
|
+
{"Cargo.toml": '[profile.release]\nlto = true\n'},
|
|
989
|
+
["backend/Cargo.toml", "[package]", "[workspace]"],
|
|
990
|
+
id="manifest with neither [package] nor [workspace]",
|
|
991
|
+
),
|
|
992
|
+
pytest.param(
|
|
993
|
+
{"Cargo.toml": '[workspace]\nmembers = ["crates/a"]\n', "crates/a/Cargo.toml": '[workspace]\nmembers = []\n'},
|
|
994
|
+
["crates/a/Cargo.toml", "[package]"],
|
|
995
|
+
id="a member that is not a package",
|
|
996
|
+
),
|
|
997
|
+
pytest.param(
|
|
998
|
+
{"Cargo.toml": '[package]\nname = "app"\nversion = 5\n'},
|
|
999
|
+
["backend/Cargo.toml", "version"],
|
|
1000
|
+
id="package version that is not a string",
|
|
1001
|
+
),
|
|
1002
|
+
]
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@pytest.mark.parametrize(("files", "named"), UNJUDGEABLE)
|
|
1006
|
+
def test_what_the_manifests_text_cannot_judge_is_could_not_run_naming_it(
|
|
1007
|
+
tmp_path: Path, cargo_accepts: list[list[str]], files: dict[str, str], named: list[str]
|
|
1008
|
+
) -> None:
|
|
1009
|
+
"""Never skipped, never a pass: the lock below is fine, and the verdict is still could-not-run."""
|
|
1010
|
+
verdict = stale_lock(tmp_path, files, lock_text((Pkg("app", "0.1.0", path=True), Pkg("a", "0.1.0", path=True))))
|
|
1011
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
1012
|
+
assert verdict.findings == ()
|
|
1013
|
+
for fragment in named:
|
|
1014
|
+
assert fragment in verdict.message, (fragment, verdict.message)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def test_a_garbage_lock_is_reported_even_when_the_manifests_cannot_be_judged(
|
|
1018
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1019
|
+
) -> None:
|
|
1020
|
+
"""The lock alone is conclusive: it need not be compared with anything."""
|
|
1021
|
+
verdict = stale_lock(tmp_path, {"Cargo.toml": "[profile.release]\n"}, "version = 99\n")
|
|
1022
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
# ----------------------------------------------- the finding's wording, remedy and modes
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def test_the_finding_wording_and_remedy(tmp_path: Path, cargo_accepts: list[list[str]]) -> None:
|
|
1029
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock(drop=("serde",)))
|
|
1030
|
+
[finding] = verdict.findings
|
|
1031
|
+
assert finding.rule == "LCK-002"
|
|
1032
|
+
assert finding.path == "backend/Cargo.lock"
|
|
1033
|
+
assert finding.message == (
|
|
1034
|
+
"backend/Cargo.lock lists no package 'serde' (dependency 'serde' in [dependencies] of backend/Cargo.toml)"
|
|
1035
|
+
)
|
|
1036
|
+
assert finding.fix == FIX
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def test_the_remedy_is_the_one_a_cargo_rejection_has(
|
|
1040
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1041
|
+
) -> None:
|
|
1042
|
+
"""One remedy for every LCK-002 message, cargo's own rejection included."""
|
|
1043
|
+
monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
|
|
1044
|
+
monkeypatch.setattr(
|
|
1045
|
+
module.subprocess,
|
|
1046
|
+
"run",
|
|
1047
|
+
lambda argv, **_: SimpleNamespace(returncode=0 if argv[1] == "--version" else 101),
|
|
1048
|
+
)
|
|
1049
|
+
verdict = stale_lock(tmp_path, SINGLE.files, SINGLE.lock())
|
|
1050
|
+
[finding] = verdict.findings
|
|
1051
|
+
assert finding.message == "backend/Cargo.lock does not satisfy backend/Cargo.toml"
|
|
1052
|
+
assert finding.fix == FIX
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def test_a_cargo_rejection_is_the_only_finding_for_that_lock(
|
|
1056
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1057
|
+
) -> None:
|
|
1058
|
+
"""The early-return shape: a manifest cargo refuses is one finding. A garbage lock beside
|
|
1059
|
+
it is not reported on top (cargo is the authority on the manifest, and the lock cannot be
|
|
1060
|
+
compared with a manifest cargo did not accept)."""
|
|
1061
|
+
calls: list[list[str]] = []
|
|
1062
|
+
|
|
1063
|
+
def fake_run(argv: list[str], **_: object) -> object:
|
|
1064
|
+
calls.append(argv)
|
|
1065
|
+
return SimpleNamespace(returncode=0 if argv[1] == "--version" else 101)
|
|
1066
|
+
|
|
1067
|
+
monkeypatch.setattr(module.shutil, "which", lambda name: "/fake/cargo")
|
|
1068
|
+
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
|
1069
|
+
verdict = stale_lock(tmp_path, SINGLE.files, "this is [[ not a lockfile\n")
|
|
1070
|
+
assert messages(verdict) == ["backend/Cargo.lock does not satisfy backend/Cargo.toml"]
|
|
1071
|
+
assert [argv[:2] for argv in calls] == [["cargo", "--version"], ["cargo", "metadata"]]
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def test_the_undeclared_mode_grades_the_tree_default_lockfile(
|
|
1075
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1076
|
+
) -> None:
|
|
1077
|
+
verdict = stale_lock(tmp_path, SINGLE.files, "version = 99\n")
|
|
1078
|
+
assert {f.path for f in verdict.findings} == {"backend/Cargo.lock"}
|
|
1079
|
+
assert {f.fix for f in verdict.findings} == {FIX}
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
DECLARED = {"cargo-manifests": "nested/*/Cargo.toml", "cargo-lockfiles": "nested/ws/Cargo.lock"}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_the_declared_mode_grades_the_declared_lockfile_in_its_own_directory(
|
|
1086
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1087
|
+
) -> None:
|
|
1088
|
+
lay(tmp_path, SINGLE.files, SINGLE.lock(drop=("cc",)), where="nested/ws")
|
|
1089
|
+
verdict = grade(tmp_path, declarations=DECLARED)
|
|
1090
|
+
[finding] = verdict.findings
|
|
1091
|
+
assert finding.rule == "LCK-002"
|
|
1092
|
+
assert finding.path == "nested/ws/Cargo.lock"
|
|
1093
|
+
assert finding.message == (
|
|
1094
|
+
"nested/ws/Cargo.lock lists no package 'cc' "
|
|
1095
|
+
"(dependency 'cc' in [build-dependencies] of nested/ws/Cargo.toml)"
|
|
1096
|
+
)
|
|
1097
|
+
assert finding.fix == (
|
|
1098
|
+
"run: (cd nested/ws && cargo update) to resync nested/ws/Cargo.lock with "
|
|
1099
|
+
"nested/ws/Cargo.toml, then commit the result"
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def test_the_declared_mode_is_clean_for_a_good_lock(
|
|
1104
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1105
|
+
) -> None:
|
|
1106
|
+
lay(tmp_path, SINGLE.files, SINGLE.lock(), where="nested/ws")
|
|
1107
|
+
verdict = grade(tmp_path, declarations=DECLARED)
|
|
1108
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def test_every_declared_lockfile_is_judged_and_each_stale_one_is_reported(
|
|
1112
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1113
|
+
) -> None:
|
|
1114
|
+
lay(tmp_path, SINGLE.files, SINGLE.lock(drop=("cc",)), where="nested/a")
|
|
1115
|
+
lay(tmp_path, SINGLE.files, SINGLE.lock(), where="nested/b")
|
|
1116
|
+
lay(tmp_path, SINGLE.files, "version = 99\n", where="nested/c")
|
|
1117
|
+
declarations = {
|
|
1118
|
+
"cargo-manifests": "nested/*/Cargo.toml",
|
|
1119
|
+
"cargo-lockfiles": "nested/a/Cargo.lock,nested/b/Cargo.lock,nested/c/Cargo.lock",
|
|
1120
|
+
}
|
|
1121
|
+
verdict = grade(tmp_path, declarations=declarations)
|
|
1122
|
+
assert sorted({f.path for f in verdict.findings}) == ["nested/a/Cargo.lock", "nested/c/Cargo.lock"]
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
# --------------------------------------------- determinism: cargo is asked only what it was
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def test_cargo_is_asked_exactly_the_two_questions_it_always_was(
|
|
1129
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1130
|
+
) -> None:
|
|
1131
|
+
"""The content validation spawns nothing and never drops `--no-deps`: the verdict cannot
|
|
1132
|
+
depend on the machine's registry cache or network."""
|
|
1133
|
+
stale_lock(tmp_path, VIRTUAL.files, VIRTUAL.lock(drop=("serde",)))
|
|
1134
|
+
assert [argv[:2] for argv in cargo_accepts] == [["cargo", "--version"], ["cargo", "metadata"]]
|
|
1135
|
+
metadata = cargo_accepts[1]
|
|
1136
|
+
for flag in ("--locked", "--offline", "--no-deps"):
|
|
1137
|
+
assert flag in metadata
|
|
1138
|
+
assert metadata[metadata.index("--manifest-path") + 1] == str(tmp_path / "backend" / "Cargo.toml")
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def test_without_cargo_the_gate_is_still_could_not_run(
|
|
1142
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1143
|
+
) -> None:
|
|
1144
|
+
"""The cargo guard is kept: a stale lock does not turn into a verdict on a machine without cargo."""
|
|
1145
|
+
monkeypatch.setattr(module.shutil, "which", lambda name: None)
|
|
1146
|
+
verdict = stale_lock(tmp_path, SINGLE.files, "version = 99\n")
|
|
1147
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
1148
|
+
assert "cargo is required" in verdict.message
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def test_the_missing_lockfile_is_still_lck_001_and_returns_first(
|
|
1152
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1153
|
+
) -> None:
|
|
1154
|
+
lay(tmp_path, SINGLE.files, "version = 99\n")
|
|
1155
|
+
(tmp_path / "backend" / "Cargo.lock").unlink()
|
|
1156
|
+
verdict = grade(tmp_path)
|
|
1157
|
+
[finding] = verdict.findings
|
|
1158
|
+
assert (finding.rule, finding.message) == ("LCK-001", "missing backend/Cargo.lock")
|
|
1159
|
+
assert cargo_accepts == []
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
# ------------------------------------------------------------------- the documented limit
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def test_version_requirement_drift_is_not_detected_offline(
|
|
1166
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1167
|
+
) -> None:
|
|
1168
|
+
"""A documented LIMIT, pinned so nobody mistakes it for coverage: the manifest asks for
|
|
1169
|
+
`serde = "99"` and the lock holds `serde 1.0.0`. Presence, structure and membership are
|
|
1170
|
+
checked offline; whether a locked VERSION still satisfies a manifest REQUIREMENT is
|
|
1171
|
+
`cargo metadata` without `--no-deps`, which needs the registry. When that changes on
|
|
1172
|
+
purpose, this test changes with it."""
|
|
1173
|
+
manifest = '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nserde = "99"\n'
|
|
1174
|
+
verdict = stale_lock(
|
|
1175
|
+
tmp_path,
|
|
1176
|
+
{"Cargo.toml": manifest},
|
|
1177
|
+
lock_text((Pkg("app", "0.1.0", path=True, needs=("serde",)), Pkg("serde", "1.0.0"))),
|
|
1178
|
+
)
|
|
1179
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|