opencomplai-cli 0.9.0__tar.gz → 0.9.2__tar.gz
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.
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/PKG-INFO +2 -2
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/pyproject.toml +2 -2
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/__init__.py +1 -1
- opencomplai_cli-0.9.2/src/opencomplai_cli/ai_status.py +37 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/dashboard.py +6 -6
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/halt.py +1 -1
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/oversight_log.py +3 -1
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/portfolio.py +37 -1
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/qms.py +3 -3
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/github_actions.py +2 -2
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/gitlab_ci.py +2 -2
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/main.py +20 -8
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_cmd.py +38 -1
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_status.py +69 -0
- opencomplai_cli-0.9.2/tests/test_ai_status_cache.py +45 -0
- opencomplai_cli-0.9.2/tests/test_changelog_newest_block.py +82 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_changelog_unreleased.py +16 -15
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_portfolio.py +131 -10
- opencomplai_cli-0.9.2/tests/test_check_report_exit_codes.py +60 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_reports.py +70 -8
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_recipes_guide.py +78 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_cli_docs_pages.py +171 -0
- opencomplai_cli-0.9.2/tests/test_docs_closeout_guards.py +105 -0
- opencomplai_cli-0.9.2/tests/test_docs_wording_guards.py +77 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_works_alongside.py +57 -0
- opencomplai_cli-0.9.2/tests/test_encoding.py +258 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_github_action.py +102 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_golden_eu_output.py +9 -3
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_install_scripts.py +32 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oss_sync_gates.py +41 -0
- opencomplai_cli-0.9.2/tests/test_release_pins_parity.py +74 -0
- opencomplai_cli-0.9.0/tests/test_encoding.py +0 -128
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/.gitignore +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/README.md +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/_encoding.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/acceptance_gate.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/agent_inventory_payload.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/check_signing.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/__init__.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/accept.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_attest.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_runtime.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_verify_log.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/checker.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/checker_manifest.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/controls.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/deployer_pack.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/diff.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/incident.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/instructions.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/interactive_init.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/push.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/rules.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/serve.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/verify.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/verify_dossier.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/__init__.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/_check_args.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/data/checker-local.html +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/exit_codes.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/inputs.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/mapped_view.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/oidc_client.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/oversight_payload.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/publish.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/report_outputs.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/summaries_emit.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/__init__.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/conftest.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/agents/sample-manifest.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/agents/sample-scan-report.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/check_reports_artifact.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/sarif-2.1.0.subset.schema.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/trap/trap-manifest.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_eu_artifact.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_eu_stdout.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_nist_artifact.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_nist_stdout.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.junit.xml +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.sarif.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.summary.md +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_chatbot.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_chatbot.txt +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit.txt +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit_nist.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit_nist.txt +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/init_manifest.json +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/recommend_files.txt +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/report.html +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_accept_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agent_inventory_payload.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_attest.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_import_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_verify_log.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_docs_claims.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_preload.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_acceptance.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_acceptance_matrix.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_gate.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_output_dir.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_roles.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_sign_gate.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_signing.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_summaries.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_checker_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_checker_roles.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_integration_guide.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_templates.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_cli.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_connector_check_args.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_connectors.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_controls_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_controls_sync.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_dashboard_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_dead_code_nits.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_deployer_pack_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_diff_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_push.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_render.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_wiring.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_golive_wording.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_python_snippets.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_signing_claims.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_truth.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eu_ai_scan_golden.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eval_provider_base_url.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eval_seed_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_evidence_provenance.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_exit_codes.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_fria_generate.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gap_report_input.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_applicability.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_gpai.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_map_to.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_mapped_column.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_multi_target.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_nist_rmf_target.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gate_demo.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_halt_wire.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_homebrew_formula.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_incident_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_incident_log_cli.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_init_model_card.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_input_robustness.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_instructions_generate_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_iso_42001_target.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_no_direct_click.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oidc_client.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oversight_log.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oversight_payload.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_precommit_hooks_pin.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_provenance_stamp.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_public_tree_safety.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_publish.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_push_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_push_cmd_dossier.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_generate_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_generate_manifest.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_scaffold_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_readme_command_table.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_readme_quickstart_pin.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_recommend_gpai.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_recommend_qms_clauses.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_regulatory_timeline_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_rules_changelog_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_scan_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_scan_quick.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_security_claims.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_serve_docs.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_service_check.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_sort_priority.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_summaries_emit.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_validate_manifest_agents.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_validate_manifest_oversight.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_cmd.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_dossier_kind.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_kinds.py +0 -0
- {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_sbom.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opencomplai-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Opencomplai command-line interface for AI compliance: the EU AI Act, and NIST AI RMF derived from the same evidence
|
|
5
5
|
Project-URL: Homepage, https://opencomplai.com
|
|
6
6
|
Project-URL: Documentation, https://docs.opencomplai.com
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
|
-
Requires-Dist: opencomplai-core[reports]>=0.9.
|
|
21
|
+
Requires-Dist: opencomplai-core[reports]>=0.9.2
|
|
22
22
|
Requires-Dist: questionary>=2.0.0
|
|
23
23
|
Requires-Dist: rich>=13.0.0
|
|
24
24
|
Requires-Dist: typer>=0.9.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opencomplai-cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.2"
|
|
4
4
|
description = "Opencomplai command-line interface for AI compliance: the EU AI Act, and NIST AI RMF derived from the same evidence"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "AGPL-3.0-only" }
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Topic :: Software Development :: Quality Assurance",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
-
"opencomplai-core[reports]>=0.9.
|
|
20
|
+
"opencomplai-core[reports]>=0.9.2",
|
|
21
21
|
"typer>=0.9.0",
|
|
22
22
|
"rich>=13.0.0",
|
|
23
23
|
"questionary>=2.0.0",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Pure helpers for `opencomplai ai status` (stdlib only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cache_files(cache_dir: Path) -> list[tuple[str, int]]:
|
|
10
|
+
"""Regular files under cache_dir (recursive) as (relative posix path, bytes), sorted."""
|
|
11
|
+
root = Path(cache_dir)
|
|
12
|
+
if not root.is_dir():
|
|
13
|
+
return []
|
|
14
|
+
found: list[tuple[str, int]] = []
|
|
15
|
+
for dirpath, _dirs, names in os.walk(root, followlinks=False):
|
|
16
|
+
for name in names:
|
|
17
|
+
p = Path(dirpath) / name
|
|
18
|
+
try:
|
|
19
|
+
if p.is_symlink() or not p.is_file():
|
|
20
|
+
continue
|
|
21
|
+
size = p.stat().st_size
|
|
22
|
+
except OSError:
|
|
23
|
+
continue
|
|
24
|
+
found.append((p.relative_to(root).as_posix(), size))
|
|
25
|
+
return sorted(found)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def format_cache_size(total_bytes: int) -> str:
|
|
29
|
+
if total_bytes <= 0:
|
|
30
|
+
return "0 MB"
|
|
31
|
+
if total_bytes < 1024 * 1024:
|
|
32
|
+
return "<1 MB"
|
|
33
|
+
return f"{total_bytes // (1024 * 1024)} MB"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def spec_size_label(size_mb: int) -> str:
|
|
37
|
+
return "unknown" if size_mb <= 0 else f"~{size_mb} MB"
|
|
@@ -71,7 +71,7 @@ def _write_client_secret(raw_secret: str) -> None:
|
|
|
71
71
|
doesn't apply the same way.
|
|
72
72
|
"""
|
|
73
73
|
_OPENCOMPLAI_DIR.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
_CLIENT_SECRET_FILE.write_text(raw_secret)
|
|
74
|
+
_CLIENT_SECRET_FILE.write_text(raw_secret, encoding="utf-8")
|
|
75
75
|
try:
|
|
76
76
|
os.chmod(_CLIENT_SECRET_FILE, 0o600)
|
|
77
77
|
except (OSError, NotImplementedError):
|
|
@@ -81,7 +81,7 @@ def _write_client_secret(raw_secret: str) -> None:
|
|
|
81
81
|
def _read_client_secret() -> str | None:
|
|
82
82
|
if not _CLIENT_SECRET_FILE.exists():
|
|
83
83
|
return None
|
|
84
|
-
secret = _CLIENT_SECRET_FILE.read_text().strip()
|
|
84
|
+
secret = _CLIENT_SECRET_FILE.read_text(encoding="utf-8").strip()
|
|
85
85
|
return secret or None
|
|
86
86
|
|
|
87
87
|
|
|
@@ -117,14 +117,14 @@ def _load_egress_allowlist() -> dict:
|
|
|
117
117
|
if not _EGRESS_ALLOWLIST.exists():
|
|
118
118
|
return {}
|
|
119
119
|
try:
|
|
120
|
-
return json.loads(_EGRESS_ALLOWLIST.read_text())
|
|
120
|
+
return json.loads(_EGRESS_ALLOWLIST.read_text(encoding="utf-8"))
|
|
121
121
|
except Exception:
|
|
122
122
|
return {}
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def _write_egress_allowlist(data: dict) -> None:
|
|
126
126
|
_OPENCOMPLAI_DIR.mkdir(parents=True, exist_ok=True)
|
|
127
|
-
_EGRESS_ALLOWLIST.write_text(json.dumps(data, indent=2) + "\n")
|
|
127
|
+
_EGRESS_ALLOWLIST.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def _append_ledger_event(event: dict) -> None:
|
|
@@ -137,7 +137,7 @@ def _ledger_events() -> list[dict]:
|
|
|
137
137
|
if not _LEDGER_FILE.exists():
|
|
138
138
|
return []
|
|
139
139
|
events: list[dict] = []
|
|
140
|
-
for line in _LEDGER_FILE.read_text().splitlines():
|
|
140
|
+
for line in _LEDGER_FILE.read_text(encoding="utf-8").splitlines():
|
|
141
141
|
try:
|
|
142
142
|
events.append(json.loads(line))
|
|
143
143
|
except Exception:
|
|
@@ -236,7 +236,7 @@ def enroll(
|
|
|
236
236
|
"[red]Error:[/red] signing.pub not found — run `opencomplai init` first."
|
|
237
237
|
)
|
|
238
238
|
sys.exit(2)
|
|
239
|
-
public_key_pem = _SIGNING_PUB.read_text().strip()
|
|
239
|
+
public_key_pem = _SIGNING_PUB.read_text(encoding="utf-8").strip()
|
|
240
240
|
|
|
241
241
|
# --- POST /v1/admin/enroll ---
|
|
242
242
|
try:
|
|
@@ -235,7 +235,7 @@ def resume_cmd(
|
|
|
235
235
|
if token_str.startswith("@"):
|
|
236
236
|
token_path = Path(token_str[1:])
|
|
237
237
|
try:
|
|
238
|
-
token_str = token_path.read_text().strip()
|
|
238
|
+
token_str = token_path.read_text(encoding="utf-8").strip()
|
|
239
239
|
except OSError as exc:
|
|
240
240
|
main_mod.err_console.print(
|
|
241
241
|
f"[red]Error:[/red] cannot read approval token file: {exc}"
|
{opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/oversight_log.py
RENAMED
|
@@ -56,7 +56,9 @@ def declared_roles(manifest_path: Path) -> list[str]:
|
|
|
56
56
|
if not manifest_path.exists():
|
|
57
57
|
return []
|
|
58
58
|
try:
|
|
59
|
-
manifest = SystemManifest.model_validate_json(
|
|
59
|
+
manifest = SystemManifest.model_validate_json(
|
|
60
|
+
manifest_path.read_text(encoding="utf-8")
|
|
61
|
+
)
|
|
60
62
|
except (OSError, ValueError) as exc:
|
|
61
63
|
raise ValueError(f"cannot read manifest {manifest_path}: {exc}") from None
|
|
62
64
|
if manifest.human_oversight is None:
|
|
@@ -66,6 +66,11 @@ def safe_dir_name(system_id: str) -> str:
|
|
|
66
66
|
return "_" if name in ("", ".", "..") else name
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _per_system_path(path: Path, sid: str) -> Path:
|
|
70
|
+
"""`r.xml` + `alpha` -> `r.alpha.xml`, so systems never share a report file."""
|
|
71
|
+
return path.with_name(f"{path.stem}.{safe_dir_name(sid)}{path.suffix}")
|
|
72
|
+
|
|
73
|
+
|
|
69
74
|
def _read_raw(path: Path) -> object:
|
|
70
75
|
from opencomplai_cli.inputs import read_json_file
|
|
71
76
|
|
|
@@ -75,6 +80,30 @@ def _read_raw(path: Path) -> object:
|
|
|
75
80
|
return None
|
|
76
81
|
|
|
77
82
|
|
|
83
|
+
def _per_system_sample_set(path: Path | None, sid: str, sys_dir: Path) -> Path | None:
|
|
84
|
+
"""Copy of the shared sample set with `system_id` set to `sid`, written to
|
|
85
|
+
`sys_dir`. Anything unreadable or malformed returns `path` unchanged so the
|
|
86
|
+
child run prints its normal error; the shared file is never modified."""
|
|
87
|
+
if path is None:
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
raw = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
91
|
+
except (OSError, ValueError):
|
|
92
|
+
return path
|
|
93
|
+
if not isinstance(raw, dict) or not isinstance(raw.get("system_id"), str):
|
|
94
|
+
return path
|
|
95
|
+
try:
|
|
96
|
+
sys_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
copy = sys_dir / "sample-set.json"
|
|
98
|
+
copy.write_text(
|
|
99
|
+
json.dumps({**raw, "system_id": sid}, indent=2, ensure_ascii=False),
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
)
|
|
102
|
+
except OSError:
|
|
103
|
+
return path
|
|
104
|
+
return copy
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
def _system_id(path: Path) -> str:
|
|
79
108
|
raw = _read_raw(path)
|
|
80
109
|
sid = raw.get("system_id") if isinstance(raw, dict) else None
|
|
@@ -123,6 +152,13 @@ def run_portfolio(
|
|
|
123
152
|
"output_dir": sys_dir,
|
|
124
153
|
"output": _consoles()[2].human,
|
|
125
154
|
}
|
|
155
|
+
for key in ("report_junit", "sarif_output", "summary_md"):
|
|
156
|
+
if params.get(key) is not None:
|
|
157
|
+
params[key] = _per_system_path(Path(params[key]), sid)
|
|
158
|
+
if params.get("sample_set_file") is not None:
|
|
159
|
+
params["sample_set_file"] = _per_system_sample_set(
|
|
160
|
+
params["sample_set_file"], sid, sys_dir
|
|
161
|
+
)
|
|
126
162
|
try:
|
|
127
163
|
invoke(**params)
|
|
128
164
|
code = 0
|
|
@@ -159,7 +195,7 @@ def run_portfolio(
|
|
|
159
195
|
out = ctx_params["output"]
|
|
160
196
|
console = _consoles()[0]
|
|
161
197
|
if getattr(out, "value", out) == "json":
|
|
162
|
-
console.print_json(path.read_text())
|
|
198
|
+
console.print_json(path.read_text(encoding="utf-8"))
|
|
163
199
|
else:
|
|
164
200
|
console.print("\n[bold]Portfolio[/bold]")
|
|
165
201
|
for e in sorted(entries, key=lambda e: e["system_id"]):
|
|
@@ -124,7 +124,7 @@ def generate_cmd(
|
|
|
124
124
|
sys.exit(2)
|
|
125
125
|
try:
|
|
126
126
|
manifest = SystemManifest.model_validate(
|
|
127
|
-
json.loads(manifest_file.read_text())
|
|
127
|
+
json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
128
128
|
)
|
|
129
129
|
except Exception as e:
|
|
130
130
|
_main.err_console.print(f"[red]Validation error:[/red] {e}")
|
|
@@ -134,7 +134,7 @@ def generate_cmd(
|
|
|
134
134
|
if scan_report_file.exists():
|
|
135
135
|
try:
|
|
136
136
|
scan_report = CorroborationReport.model_validate(
|
|
137
|
-
json.loads(scan_report_file.read_text())
|
|
137
|
+
json.loads(scan_report_file.read_text(encoding="utf-8"))
|
|
138
138
|
)
|
|
139
139
|
except Exception as exc:
|
|
140
140
|
_main.err_console.print(
|
|
@@ -145,7 +145,7 @@ def generate_cmd(
|
|
|
145
145
|
if eval_report_file.exists():
|
|
146
146
|
try:
|
|
147
147
|
eval_report = EvalReport.model_validate(
|
|
148
|
-
json.loads(eval_report_file.read_text())
|
|
148
|
+
json.loads(eval_report_file.read_text(encoding="utf-8"))
|
|
149
149
|
)
|
|
150
150
|
except Exception as exc:
|
|
151
151
|
_main.err_console.print(
|
{opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/github_actions.py
RENAMED
|
@@ -125,7 +125,7 @@ def _gha_cmd(cmd: str, value: str) -> None:
|
|
|
125
125
|
def _set_output(name: str, value: str) -> None:
|
|
126
126
|
output_file = os.environ.get("GITHUB_OUTPUT", "")
|
|
127
127
|
if output_file:
|
|
128
|
-
with open(output_file, "a") as fh:
|
|
128
|
+
with open(output_file, "a", encoding="utf-8") as fh:
|
|
129
129
|
fh.write(f"{name}={value}\n")
|
|
130
130
|
else:
|
|
131
131
|
_gha_cmd("set-output", f"name={name}::{value}")
|
|
@@ -134,7 +134,7 @@ def _set_output(name: str, value: str) -> None:
|
|
|
134
134
|
def _append_summary(markdown: str) -> None:
|
|
135
135
|
summary_file = os.environ.get("GITHUB_STEP_SUMMARY", "")
|
|
136
136
|
if summary_file:
|
|
137
|
-
with open(summary_file, "a") as fh:
|
|
137
|
+
with open(summary_file, "a", encoding="utf-8") as fh:
|
|
138
138
|
fh.write(markdown + "\n")
|
|
139
139
|
|
|
140
140
|
|
|
@@ -216,7 +216,7 @@ def run_connector(
|
|
|
216
216
|
# JUnit XML artifact.
|
|
217
217
|
xml_content = _build_junit_xml(artifact_result, stdout)
|
|
218
218
|
try:
|
|
219
|
-
with open(junit_path, "w") as fh:
|
|
219
|
+
with open(junit_path, "w", encoding="utf-8") as fh:
|
|
220
220
|
fh.write(xml_content)
|
|
221
221
|
except OSError:
|
|
222
222
|
pass
|
|
@@ -307,7 +307,7 @@ def _write_dotenv(path: str, artifact: dict) -> None:
|
|
|
307
307
|
f"OPENCOMPLAI_CONTENT_HASH={artifact.get('content_hash', '')}",
|
|
308
308
|
]
|
|
309
309
|
try:
|
|
310
|
-
with open(path, "w") as fh:
|
|
310
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
311
311
|
fh.write("\n".join(lines) + "\n")
|
|
312
312
|
except OSError:
|
|
313
313
|
pass
|
|
@@ -5028,9 +5028,11 @@ def ai_configure_cmd(
|
|
|
5028
5028
|
else:
|
|
5029
5029
|
import questionary
|
|
5030
5030
|
|
|
5031
|
+
from opencomplai_cli.ai_status import spec_size_label
|
|
5032
|
+
|
|
5031
5033
|
choices = []
|
|
5032
5034
|
for mid, spec in MODEL_CATALOG.items():
|
|
5033
|
-
label = f"{mid:<28} [{spec.size_mb}
|
|
5035
|
+
label = f"{mid:<28} [{spec_size_label(spec.size_mb)}, {spec.license}, {spec.runtime}]"
|
|
5034
5036
|
if mid == "qwen2.5-coder-1.5b":
|
|
5035
5037
|
label += " ← recommended"
|
|
5036
5038
|
if mid == "codebert-onnx":
|
|
@@ -5068,6 +5070,12 @@ def ai_status_cmd() -> None:
|
|
|
5068
5070
|
from opencomplai_ai.config import get_active_model, get_cache_dir
|
|
5069
5071
|
from opencomplai_ai.models import MODEL_CATALOG
|
|
5070
5072
|
|
|
5073
|
+
from opencomplai_cli.ai_status import (
|
|
5074
|
+
cache_files,
|
|
5075
|
+
format_cache_size,
|
|
5076
|
+
spec_size_label,
|
|
5077
|
+
)
|
|
5078
|
+
|
|
5071
5079
|
active = get_active_model()
|
|
5072
5080
|
spec = MODEL_CATALOG.get(active)
|
|
5073
5081
|
cache_dir = get_cache_dir()
|
|
@@ -5076,7 +5084,7 @@ def ai_status_cmd() -> None:
|
|
|
5076
5084
|
console.print(f" active model : {active}")
|
|
5077
5085
|
if spec:
|
|
5078
5086
|
console.print(f" display name : {spec.display_name}")
|
|
5079
|
-
console.print(f" size :
|
|
5087
|
+
console.print(f" size : {spec_size_label(spec.size_mb)}")
|
|
5080
5088
|
console.print(f" runtime : {spec.runtime}")
|
|
5081
5089
|
console.print(f" license : {spec.license}")
|
|
5082
5090
|
deep_status = (
|
|
@@ -5088,17 +5096,21 @@ def ai_status_cmd() -> None:
|
|
|
5088
5096
|
console.print(f" llama-cpp : {deep_status}")
|
|
5089
5097
|
console.print(f" cache dir : {cache_dir}")
|
|
5090
5098
|
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5099
|
+
files = cache_files(cache_dir)
|
|
5100
|
+
if files:
|
|
5101
|
+
from rich.markup import escape
|
|
5102
|
+
|
|
5103
|
+
console.print(
|
|
5104
|
+
f" cache size : {format_cache_size(sum(n for _, n in files))}"
|
|
5105
|
+
)
|
|
5106
|
+
console.print(f" cached files : {', '.join(escape(p) for p, _ in files)}")
|
|
5097
5107
|
elif spec is None or spec.needs_preload:
|
|
5098
5108
|
# codebert-onnx (deterministic matcher) and saas (cloud client) have no
|
|
5099
5109
|
# local artifact, so an empty cache is not a missing download. Models
|
|
5100
5110
|
# downloaded earlier are still listed above when the cache exists.
|
|
5101
5111
|
console.print(" cache dir : (empty — model not yet downloaded)")
|
|
5112
|
+
elif cache_dir.exists():
|
|
5113
|
+
console.print(" cache size : 0 MB")
|
|
5102
5114
|
console.print()
|
|
5103
5115
|
|
|
5104
5116
|
|
|
@@ -17,7 +17,9 @@ runner = CliRunner()
|
|
|
17
17
|
FIXTURES = Path(__file__).parent / "fixtures" / "agents"
|
|
18
18
|
MANIFEST = str(FIXTURES / "sample-manifest.json")
|
|
19
19
|
SCAN = str(FIXTURES / "sample-scan-report.json")
|
|
20
|
-
_MET = re.compile(
|
|
20
|
+
_MET = re.compile(
|
|
21
|
+
r'\bMet\b|\bMET\b|(?i:"(?:status|verdict|result)"\s*:\s*"met"|\|\s*met\s*\||\bstatus\s*[:=]\s*met\b)'
|
|
22
|
+
)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
@pytest.fixture(autouse=True)
|
|
@@ -130,6 +132,41 @@ def test_check_never_emits_met(tmp_path):
|
|
|
130
132
|
assert "not a compliance verdict" in out
|
|
131
133
|
|
|
132
134
|
|
|
135
|
+
@pytest.mark.parametrize(
|
|
136
|
+
"text",
|
|
137
|
+
['{"status": "met"}', '"status": "MET"', "| Met |", "Met"],
|
|
138
|
+
)
|
|
139
|
+
def test_met_guard_matches_every_spelling(text):
|
|
140
|
+
assert _MET.search(text)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.parametrize(
|
|
144
|
+
"text",
|
|
145
|
+
['"status": "partial"', "evidence only; not a compliance verdict"],
|
|
146
|
+
)
|
|
147
|
+
def test_met_guard_ignores_other_values(text):
|
|
148
|
+
assert not _MET.search(text)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _strings(node):
|
|
152
|
+
if isinstance(node, dict):
|
|
153
|
+
for key, value in node.items():
|
|
154
|
+
yield from _strings(key)
|
|
155
|
+
yield from _strings(value)
|
|
156
|
+
elif isinstance(node, list):
|
|
157
|
+
for item in node:
|
|
158
|
+
yield from _strings(item)
|
|
159
|
+
elif isinstance(node, str):
|
|
160
|
+
yield node
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_check_json_has_no_met_value(tmp_path):
|
|
164
|
+
for scan in (None, SCAN, _clean_scan(tmp_path)):
|
|
165
|
+
extra = ["--scan-report", scan] if scan else []
|
|
166
|
+
data = json.loads(_run("check", "-m", MANIFEST, *extra, "-o", "json").output)
|
|
167
|
+
assert all(s.lower() != "met" for s in _strings(data))
|
|
168
|
+
|
|
169
|
+
|
|
133
170
|
def test_report_lists_responsibilities_with_review_flag():
|
|
134
171
|
expected = {r.id for r in get_responsibilities()}
|
|
135
172
|
data = json.loads(_run("report", "-m", MANIFEST, "--format", "json").output)
|
|
@@ -74,3 +74,72 @@ def test_status_for_gguf_model_still_reports_missing_download(monkeypatch, tmp_p
|
|
|
74
74
|
|
|
75
75
|
assert "llama-cpp" in out
|
|
76
76
|
assert "not yet downloaded" in out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _status_with_cache(monkeypatch, cache, model_id: str) -> str:
|
|
80
|
+
monkeypatch.setattr("opencomplai_ai.config.get_active_model", lambda: model_id)
|
|
81
|
+
monkeypatch.setattr("opencomplai_ai.config.get_cache_dir", lambda: cache)
|
|
82
|
+
result = runner.invoke(app, ["ai", "status"])
|
|
83
|
+
assert result.exit_code == 0, result.output
|
|
84
|
+
return result.output
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_status_counts_nested_cache_file(monkeypatch, tmp_path):
|
|
88
|
+
cache = tmp_path / "cache"
|
|
89
|
+
(cache / "codebert-base").mkdir(parents=True)
|
|
90
|
+
(cache / "codebert-base" / "model.onnx").write_bytes(b"x" * (3 * 1024 * 1024))
|
|
91
|
+
|
|
92
|
+
out = _status_with_cache(monkeypatch, cache, "codebert-onnx")
|
|
93
|
+
|
|
94
|
+
assert "cache size : 3 MB" in out
|
|
95
|
+
assert "codebert-base/model.onnx" in out
|
|
96
|
+
assert "not yet downloaded" not in out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_status_small_nested_cache_file_reports_less_than_one_mb(monkeypatch, tmp_path):
|
|
100
|
+
cache = tmp_path / "cache"
|
|
101
|
+
(cache / "sub").mkdir(parents=True)
|
|
102
|
+
(cache / "sub" / "f.bin").write_bytes(b"x" * 10)
|
|
103
|
+
|
|
104
|
+
out = _status_with_cache(monkeypatch, cache, "codebert-onnx")
|
|
105
|
+
|
|
106
|
+
assert "<1 MB" in out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("sub", [False, True])
|
|
110
|
+
def test_status_empty_cache_dir_prints_not_yet_downloaded(monkeypatch, tmp_path, sub):
|
|
111
|
+
cache = tmp_path / "cache"
|
|
112
|
+
cache.mkdir()
|
|
113
|
+
if sub:
|
|
114
|
+
(cache / "empty-sub").mkdir()
|
|
115
|
+
|
|
116
|
+
out = _status_with_cache(monkeypatch, cache, "qwen2.5-coder-1.5b")
|
|
117
|
+
|
|
118
|
+
assert "not yet downloaded" in out
|
|
119
|
+
assert "cache size" not in out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("model_id", ["codebert-onnx", "saas"])
|
|
123
|
+
def test_status_empty_cache_dir_for_no_download_backend_has_no_not_yet_downloaded_line(
|
|
124
|
+
monkeypatch, tmp_path, model_id
|
|
125
|
+
):
|
|
126
|
+
cache = tmp_path / "cache"
|
|
127
|
+
cache.mkdir()
|
|
128
|
+
|
|
129
|
+
out = _status_with_cache(monkeypatch, cache, model_id)
|
|
130
|
+
|
|
131
|
+
assert "not yet downloaded" not in out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize("model_id", ["codebert-onnx", "saas"])
|
|
135
|
+
def test_status_size_mb_zero_prints_unknown(monkeypatch, tmp_path, model_id):
|
|
136
|
+
out = _status(monkeypatch, tmp_path, model_id)
|
|
137
|
+
|
|
138
|
+
assert "size : unknown" in out
|
|
139
|
+
assert "~0 MB" not in out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_status_known_size_still_prints_approximate_mb(monkeypatch, tmp_path):
|
|
143
|
+
out = _status(monkeypatch, tmp_path, "qwen2.5-coder-1.5b")
|
|
144
|
+
|
|
145
|
+
assert "~1000 MB" in out
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from opencomplai_cli.ai_status import cache_files, format_cache_size, spec_size_label
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cache_files_counts_nested_files_sorted(tmp_path):
|
|
10
|
+
(tmp_path / "b.gguf").write_bytes(b"x" * 5)
|
|
11
|
+
(tmp_path / "codebert-base").mkdir()
|
|
12
|
+
(tmp_path / "codebert-base" / "model.onnx").write_bytes(b"y" * 7)
|
|
13
|
+
assert cache_files(tmp_path) == [("b.gguf", 5), ("codebert-base/model.onnx", 7)]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cache_files_ignores_missing_dir_and_empty_subdirs(tmp_path):
|
|
17
|
+
assert cache_files(tmp_path / "nope") == []
|
|
18
|
+
(tmp_path / "a" / "b").mkdir(parents=True)
|
|
19
|
+
assert cache_files(tmp_path) == []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_cache_files_skips_symlinks(tmp_path):
|
|
23
|
+
target = tmp_path / "real.bin"
|
|
24
|
+
target.write_bytes(b"z")
|
|
25
|
+
cache = tmp_path / "cache"
|
|
26
|
+
cache.mkdir()
|
|
27
|
+
try:
|
|
28
|
+
os.symlink(target, cache / "link.bin")
|
|
29
|
+
except (OSError, NotImplementedError):
|
|
30
|
+
pytest.skip("symlinks unavailable")
|
|
31
|
+
assert cache_files(cache) == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_format_cache_size_boundaries():
|
|
35
|
+
assert format_cache_size(0) == "0 MB"
|
|
36
|
+
assert format_cache_size(1) == "<1 MB"
|
|
37
|
+
assert format_cache_size(1048575) == "<1 MB"
|
|
38
|
+
assert format_cache_size(1048576) == "1 MB"
|
|
39
|
+
assert format_cache_size(3 * 1048576 + 5) == "3 MB"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_spec_size_label():
|
|
43
|
+
assert spec_size_label(0) == "unknown"
|
|
44
|
+
assert spec_size_label(-1) == "unknown"
|
|
45
|
+
assert spec_size_label(1000) == "~1000 MB"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Structure and hygiene guard for the newest release block of CHANGELOG.md.
|
|
2
|
+
|
|
3
|
+
Text only: the newest ``## [x.y.z]`` block must match the CLI version, keep the
|
|
4
|
+
Keep a Changelog heading order, have its footer link, carry no internal words,
|
|
5
|
+
and share no bullet with the dashboard notes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
15
|
+
CHANGELOG = ROOT / "CHANGELOG.md"
|
|
16
|
+
SAAS_CHANGELOG = ROOT / "dashboard-saas" / "CHANGELOG.md"
|
|
17
|
+
CLI_PYPROJECT = ROOT / "packages" / "cli" / "pyproject.toml"
|
|
18
|
+
|
|
19
|
+
pytestmark = pytest.mark.skipif(
|
|
20
|
+
not CHANGELOG.is_file(), reason="CHANGELOG.md is not in this tree"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
ORDER = ["Breaking", "Added", "Changed", "Removed", "Fixed", "Security"]
|
|
24
|
+
FORBIDDEN = (
|
|
25
|
+
r"dashboard|hosted|SaaS|tenant|\bSU-\d|\bCL-[A-Z]|HANDOFF|lawyer"
|
|
26
|
+
r"|trust pack|enterprise|private"
|
|
27
|
+
)
|
|
28
|
+
BLOCK_START = re.compile(r"^## \[(\d+\.\d+\.\d+)\]", re.M)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _newest(path: Path) -> tuple[str, str]:
|
|
32
|
+
text = path.read_text(encoding="utf-8")
|
|
33
|
+
first = BLOCK_START.search(text)
|
|
34
|
+
assert first, f"no release block in {path}"
|
|
35
|
+
nxt = BLOCK_START.search(text, first.end())
|
|
36
|
+
end = nxt.start() if nxt else len(text)
|
|
37
|
+
return first.group(1), text[first.start() : end]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _bullets(block: str) -> set[str]:
|
|
41
|
+
return {ln for ln in block.splitlines() if ln.startswith("- ")}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_newest_block_version_matches_cli():
|
|
45
|
+
version, _ = _newest(CHANGELOG)
|
|
46
|
+
cli = tomllib.loads(CLI_PYPROJECT.read_text(encoding="utf-8"))["project"]["version"]
|
|
47
|
+
assert version == cli
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_newest_block_headings_in_order():
|
|
51
|
+
_, block = _newest(CHANGELOG)
|
|
52
|
+
headings = re.findall(r"^### (.+?)\s*$", block, re.M)
|
|
53
|
+
assert len(headings) == len(set(headings)), f"duplicate headings: {headings}"
|
|
54
|
+
unknown = [h for h in headings if h not in ORDER]
|
|
55
|
+
assert not unknown, f"unknown headings: {unknown}"
|
|
56
|
+
assert headings == sorted(headings, key=ORDER.index), headings
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_footer_link_present():
|
|
60
|
+
version, _ = _newest(CHANGELOG)
|
|
61
|
+
link = (
|
|
62
|
+
f"[{version}]: https://github.com/Opencomplai/opencomplai"
|
|
63
|
+
f"/releases/tag/v{version}"
|
|
64
|
+
)
|
|
65
|
+
assert link in CHANGELOG.read_text(encoding="utf-8").splitlines()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_newest_release_has_no_forbidden_words():
|
|
69
|
+
_, block = _newest(CHANGELOG)
|
|
70
|
+
pattern = re.compile(FORBIDDEN, re.I)
|
|
71
|
+
hits = [ln for ln in block.splitlines() if pattern.search(ln)]
|
|
72
|
+
assert not hits, f"internal words in the newest block: {hits}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_saas_notes_share_no_bullet_with_public():
|
|
76
|
+
if not SAAS_CHANGELOG.is_file():
|
|
77
|
+
pytest.skip("dashboard-saas/CHANGELOG.md is not in this tree")
|
|
78
|
+
public_version, public = _newest(CHANGELOG)
|
|
79
|
+
saas_version, saas = _newest(SAAS_CHANGELOG)
|
|
80
|
+
assert saas_version == public_version
|
|
81
|
+
shared = _bullets(public) & _bullets(saas)
|
|
82
|
+
assert not shared, f"bullets in both blocks: {sorted(shared)}"
|