mainsequence 9.0.6.dev41__tar.gz → 9.0.7.dev42__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.
Files changed (105) hide show
  1. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/PKG-INFO +1 -1
  2. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/PKG-INFO +1 -1
  3. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/SOURCES.txt +0 -2
  4. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/pyproject.toml +1 -1
  5. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_auth_precedence.py +10 -52
  6. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_cli_auth_contract.py +2 -61
  7. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_cli_secure_storage.py +1 -8
  8. mainsequence-9.0.7.dev42/tests/test_client.py +50 -0
  9. mainsequence-9.0.7.dev42/tests/test_code_repository_documentation_contract.py +110 -0
  10. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_dependency_extras.py +0 -39
  11. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_http_retry_budget.py +9 -22
  12. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_job_client.py +2 -27
  13. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_logconf.py +8 -50
  14. mainsequence-9.0.6.dev41/tests/test_cli.py +0 -8527
  15. mainsequence-9.0.6.dev41/tests/test_client.py +0 -70
  16. mainsequence-9.0.6.dev41/tests/test_code_repository_documentation_contract.py +0 -222
  17. mainsequence-9.0.6.dev41/tests/test_filter_normalization.py +0 -4433
  18. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/LICENSE +0 -0
  19. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/README.md +0 -0
  20. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/AGENTS.md +0 -0
  21. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  22. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  23. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
  24. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  25. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  26. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  27. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  28. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  29. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  30. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/__init__.py +0 -0
  31. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/__main__.py +0 -0
  32. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/_request_identity.py +0 -0
  33. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/bootstrap.py +0 -0
  34. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/__init__.py +0 -0
  35. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/api.py +0 -0
  36. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/browser_auth.py +0 -0
  37. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/cli.py +0 -0
  38. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/code_repository_status.py +0 -0
  39. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/config.py +0 -0
  40. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/docker_utils.py +0 -0
  41. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/doctor.py +0 -0
  42. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/local_ops.py +0 -0
  43. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/model_filters.py +0 -0
  44. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/pydantic_cli.py +0 -0
  45. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/sdk_utils.py +0 -0
  46. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/ssh_utils.py +0 -0
  47. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/cli/ui.py +0 -0
  48. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/__init__.py +0 -0
  49. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/_transport.py +0 -0
  50. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/agent_runtime_models.py +0 -0
  51. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/base.py +0 -0
  52. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/client.py +0 -0
  53. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/compute_validation.py +0 -0
  54. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/exceptions.py +0 -0
  55. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/github_issues.py +0 -0
  56. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/inference.py +0 -0
  57. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/models_foundry.py +0 -0
  58. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/models_helpers.py +0 -0
  59. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/models_user.py +0 -0
  60. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/observability.py +0 -0
  61. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/utils.py +0 -0
  62. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/client/value_sets.py +0 -0
  63. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/code_repository_context.py +0 -0
  64. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/code_repository_skills.py +0 -0
  65. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/defaults.py +0 -0
  66. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/instrumentation/__init__.py +0 -0
  67. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/instrumentation/utils.py +0 -0
  68. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/logconf.py +0 -0
  69. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/repository_identity_security.py +0 -0
  70. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/runtime_flags.py +0 -0
  71. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/scaffold_skills.py +0 -0
  72. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/server/__init__.py +0 -0
  73. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/server/caller_assertions.py +0 -0
  74. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence/server/fastapi.py +0 -0
  75. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/dependency_links.txt +0 -0
  76. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/entry_points.txt +0 -0
  77. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/requires.txt +0 -0
  78. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/mainsequence.egg-info/top_level.txt +0 -0
  79. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/setup.cfg +0 -0
  80. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_auth_bootstrap.py +0 -0
  81. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_caller_assertions.py +0 -0
  82. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_canonical_api_route_contract.py +0 -0
  83. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_cli_browser_auth.py +0 -0
  84. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_code_repository_branch_contract.py +0 -0
  85. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_code_repository_runtime_context.py +0 -0
  86. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_code_repository_skills.py +0 -0
  87. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_dev_release_version.py +0 -0
  88. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_environment_resource_context.py +0 -0
  89. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_environment_response_contracts.py +0 -0
  90. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_fastapi_request_identity.py +0 -0
  91. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_github_issues_client.py +0 -0
  92. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_independent_context.py +0 -0
  93. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_inference_client.py +0 -0
  94. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_instrumentation.py +0 -0
  95. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_local_ops.py +0 -0
  96. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_models_user_request_bound_auth.py +0 -0
  97. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_notification_uid_contract.py +0 -0
  98. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_observability_client.py +0 -0
  99. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_scaffold_skills.py +0 -0
  100. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_secret_client_model.py +0 -0
  101. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_source_ownership.py +0 -0
  102. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_ssh_utils.py +0 -0
  103. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_tolerant_response_reading.py +0 -0
  104. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_tolerant_value_sets.py +0 -0
  105. {mainsequence-9.0.6.dev41 → mainsequence-9.0.7.dev42}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.6.dev41
3
+ Version: 9.0.7.dev42
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.6.dev41
3
+ Version: 9.0.7.dev42
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -66,7 +66,6 @@ tests/test_auth_bootstrap.py
66
66
  tests/test_auth_precedence.py
67
67
  tests/test_caller_assertions.py
68
68
  tests/test_canonical_api_route_contract.py
69
- tests/test_cli.py
70
69
  tests/test_cli_auth_contract.py
71
70
  tests/test_cli_browser_auth.py
72
71
  tests/test_cli_secure_storage.py
@@ -80,7 +79,6 @@ tests/test_dev_release_version.py
80
79
  tests/test_environment_resource_context.py
81
80
  tests/test_environment_response_contracts.py
82
81
  tests/test_fastapi_request_identity.py
83
- tests/test_filter_normalization.py
84
82
  tests/test_github_issues_client.py
85
83
  tests/test_http_retry_budget.py
86
84
  tests/test_independent_context.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.6.dev41"
7
+ version = "9.0.7.dev42"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -4,68 +4,23 @@ import base64
4
4
  import importlib
5
5
  import json
6
6
  import os
7
- import pathlib
8
- import sys
9
7
  import time
10
- import types
11
8
 
12
9
  import pytest
13
10
  import requests
14
11
 
12
+ from tests._support import load_sdk_submodule as _load_mainsequence_submodule
13
+
14
+ pytestmark = pytest.mark.usefixtures("isolated_sdk_imports", "clean_runtime_context_environment")
15
+
16
+
15
17
  _REMOVED_DOMAIN_TOKEN = "PRO" + "JECT"
16
18
  _UNSUPPORTED_REPOSITORY_UID_ENV = "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_UID"
17
- _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV = (
18
- "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_BRANCH_UID"
19
- )
19
+ _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV = "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_BRANCH_UID"
20
20
  _UNSUPPORTED_ENVIRONMENT_UID_ENV = (
21
21
  "MAIN_SEQUENCE_ORGANIZATION_" + _REMOVED_DOMAIN_TOKEN + "_ENVIRONMENT_UID"
22
22
  )
23
23
 
24
- _RUNTIME_CONTEXT_ENV_NAMES = (
25
- _UNSUPPORTED_REPOSITORY_UID_ENV,
26
- _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV,
27
- "MAINSEQUENCE_REPOSITORY_BRANCH",
28
- _UNSUPPORTED_ENVIRONMENT_UID_ENV,
29
- )
30
-
31
-
32
- @pytest.fixture(autouse=True)
33
- def _clear_runtime_context_environment():
34
- original_modules = {
35
- name: module
36
- for name, module in sys.modules.items()
37
- if name == "mainsequence" or name.startswith("mainsequence.")
38
- }
39
- for name in _RUNTIME_CONTEXT_ENV_NAMES:
40
- os.environ.pop(name, None)
41
- yield
42
- for name in _RUNTIME_CONTEXT_ENV_NAMES:
43
- os.environ.pop(name, None)
44
- for name in tuple(sys.modules):
45
- if name == "mainsequence" or name.startswith("mainsequence."):
46
- sys.modules.pop(name, None)
47
- sys.modules.update(original_modules)
48
-
49
-
50
- def _load_mainsequence_submodule(module_name: str):
51
- repo_root = pathlib.Path(__file__).resolve().parents[1]
52
- pkg_root = repo_root / "mainsequence"
53
- client_pkg_root = pkg_root / "client"
54
-
55
- for name in tuple(sys.modules):
56
- if name == "mainsequence" or name.startswith("mainsequence."):
57
- sys.modules.pop(name, None)
58
-
59
- pkg = types.ModuleType("mainsequence")
60
- pkg.__path__ = [str(pkg_root)]
61
- sys.modules["mainsequence"] = pkg
62
-
63
- client_pkg = types.ModuleType("mainsequence.client")
64
- client_pkg.__path__ = [str(client_pkg_root)]
65
- sys.modules["mainsequence.client"] = client_pkg
66
-
67
- return importlib.import_module(module_name)
68
-
69
24
 
70
25
  class _FakeResponse:
71
26
  def __init__(self, status_code: int, payload: dict | None = None):
@@ -363,7 +318,10 @@ def test_runtime_credential_provider_does_not_mutate_unsupported_identity_enviro
363
318
  assert first_headers["Authorization"] == "Bearer organization-runtime-access"
364
319
  assert second_headers == first_headers
365
320
  assert calls["count"] == 1
366
- assert os.environ[_UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV] == "caller-supplied-value-must-be-ignored"
321
+ assert (
322
+ os.environ[_UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV]
323
+ == "caller-supplied-value-must-be-ignored"
324
+ )
367
325
 
368
326
 
369
327
  def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch):
@@ -9,6 +9,7 @@ import requests
9
9
  from typer.testing import CliRunner
10
10
 
11
11
  from mainsequence.cli import cli as cli_mod
12
+ from tests._support import jwt_with_expiry as _jwt
12
13
 
13
14
  runner = CliRunner()
14
15
 
@@ -32,59 +33,6 @@ def test_cli_exposes_core_and_code_repository_commands():
32
33
  assert "code-repository" in result.output
33
34
 
34
35
 
35
- def test_manual_jwt_login_persists_tokens(monkeypatch):
36
- saved = {}
37
- monkeypatch.setattr(cli_mod.cfg, "backend_url", lambda: "https://backend.example")
38
- monkeypatch.setattr(
39
- cli_mod.cfg,
40
- "save_tokens",
41
- lambda username, access, refresh: (
42
- saved.update(username=username, access=access, refresh=refresh) or True
43
- ),
44
- )
45
-
46
- result = runner.invoke(
47
- cli_mod.app, ["login", "--access-token", "access", "--refresh-token", "refresh"]
48
- )
49
-
50
- assert result.exit_code == 0
51
- assert saved == {"username": "", "access": "access", "refresh": "refresh"}
52
- assert "Signed in" in result.output
53
-
54
-
55
- def test_browser_login_uses_existing_auth_helper(monkeypatch):
56
- monkeypatch.setattr(cli_mod.cfg, "backend_url", lambda: "https://backend.example")
57
- monkeypatch.setattr(cli_mod.cfg, "save_tokens", lambda *_: True)
58
- monkeypatch.setattr(
59
- cli_mod,
60
- "login_via_browser",
61
- lambda **kwargs: {"access": "browser-access", "refresh": "browser-refresh"},
62
- )
63
-
64
- result = runner.invoke(cli_mod.app, ["login", "--no-open"])
65
-
66
- assert result.exit_code == 0
67
- assert "Signed in" in result.output
68
-
69
-
70
- def test_mcp_login_uses_existing_handoff_helper(monkeypatch):
71
- monkeypatch.setattr(cli_mod.cfg, "backend_url", lambda: "https://backend.example")
72
- monkeypatch.setattr(cli_mod.cfg, "save_tokens", lambda *_: True)
73
-
74
- def handoff(**kwargs):
75
- kwargs["on_handoff"](
76
- {"mcp_tool": "auth.cli_authorize", "mcp_arguments": {"handoff_uid": "h"}}
77
- )
78
- return {"access": "mcp-access", "refresh": "mcp-refresh"}
79
-
80
- monkeypatch.setattr(cli_mod, "login_via_mcp_handoff", handoff)
81
-
82
- result = runner.invoke(cli_mod.app, ["login", "--mcp"])
83
-
84
- assert result.exit_code == 0
85
- assert "auth.cli_authorize" in result.output
86
-
87
-
88
36
  def test_logout_revokes_before_clearing(monkeypatch):
89
37
  calls = []
90
38
  monkeypatch.setattr(
@@ -99,6 +47,7 @@ def test_logout_revokes_before_clearing(monkeypatch):
99
47
 
100
48
  assert result.exit_code == 0
101
49
  assert calls == ["revoke", "clear", "overrides"]
50
+ assert "backend session revoked" in result.output
102
51
 
103
52
 
104
53
  def test_doctor_uses_full_local_diagnostics(monkeypatch):
@@ -396,14 +345,6 @@ def _now() -> int:
396
345
  return int(time.time())
397
346
 
398
347
 
399
- def _jwt(expiry: int | None) -> str:
400
- import base64
401
-
402
- claims = {} if expiry is None else {"exp": expiry}
403
- body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
404
- return f"e30.{body}.signature"
405
-
406
-
407
348
  def test_current_access_token_reuses_a_token_that_is_still_valid(monkeypatch):
408
349
  from mainsequence.cli import api as api_mod
409
350
 
@@ -7,6 +7,7 @@ import json
7
7
  from keyring import errors as keyring_errors
8
8
 
9
9
  from mainsequence.cli import config
10
+ from tests._support import jwt_with_expiry as _jwt
10
11
 
11
12
 
12
13
  class MemoryKeyring:
@@ -897,14 +898,6 @@ def test_macos_record_updated_by_a_released_version_is_still_read(monkeypatch, t
897
898
  assert config.clear_tokens() is True
898
899
 
899
900
 
900
- def _jwt(expiry: int | None) -> str:
901
- import base64
902
-
903
- claims = {} if expiry is None else {"exp": expiry}
904
- body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
905
- return f"e30.{body}.signature"
906
-
907
-
908
901
  def test_token_expiry_reads_the_claim_and_tolerates_other_values():
909
902
  assert config.token_expiry(_jwt(1_900_000_000)) == 1_900_000_000
910
903
  assert config.token_expiry(_jwt(None)) is None
@@ -0,0 +1,50 @@
1
+ import pytest
2
+
3
+ import mainsequence.client as msc
4
+
5
+ # Every test in this module drives a real backend over the network and needs
6
+ # SDK credentials, so none of them can run in CI. See `[tool.pytest.ini_options]`
7
+ # in pyproject.toml: `pytest` deselects them, `pytest -m live` runs them.
8
+ pytestmark = pytest.mark.live
9
+
10
+
11
+ def test_code_repository_image_filter():
12
+ images = msc.CodeRepositoryImage.filter()
13
+ if not images:
14
+ pytest.skip("No CodeRepository images available for filter test.")
15
+
16
+ image = images[0]
17
+ repo_hash = image.code_repository_commit_hash
18
+
19
+ filtered_by_hash = msc.CodeRepositoryImage.filter(code_repository_commit_hash=repo_hash)
20
+ assert any(img.uid == image.uid for img in filtered_by_hash)
21
+
22
+ filtered_by_hash_in = msc.CodeRepositoryImage.filter(
23
+ code_repository_commit_hash__in=[repo_hash]
24
+ )
25
+ assert any(img.uid == image.uid for img in filtered_by_hash_in)
26
+
27
+
28
+ def test_code_repository_resource_filter():
29
+ resources = msc.CodeRepositoryResource.filter()
30
+ if not resources:
31
+ pytest.skip("No CodeRepository resources available for filter test.")
32
+
33
+ resource = resources[0]
34
+ filtered_by_uid = msc.CodeRepositoryResource.filter(uid=resource.uid)
35
+ assert any(item.uid == resource.uid for item in filtered_by_uid)
36
+
37
+ filtered_by_uid_in = msc.CodeRepositoryResource.filter(uid__in=[resource.uid])
38
+ assert any(item.uid == resource.uid for item in filtered_by_uid_in)
39
+
40
+ if resource.repo_commit_sha:
41
+ filtered_by_repo_commit = msc.CodeRepositoryResource.filter(
42
+ repo_commit_sha=resource.repo_commit_sha
43
+ )
44
+ assert any(item.uid == resource.uid for item in filtered_by_repo_commit)
45
+
46
+ if resource.resource_type:
47
+ filtered_by_resource_type = msc.CodeRepositoryResource.filter(
48
+ resource_type=resource.resource_type
49
+ )
50
+ assert any(item.uid == resource.uid for item in filtered_by_resource_type)
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
9
+ SKILL_ROOT = REPOSITORY_ROOT / "agent_scaffold" / "skills"
10
+ DOCUMENTATION_SITE_ROOT = "https://mainsequence-sdk.github.io/mainsequence-sdk"
11
+ # The wheel ships `agent_scaffold/skills` and no `docs/`, and the CLI copies those
12
+ # skills into a user's CodeRepository under `.agents/skills/<namespace>/`. There,
13
+ # `docs/` is that repository's own directory, so a skill that names an SDK
14
+ # documentation page by repository-relative path names a file the agent reading it
15
+ # cannot open. Skills cite the published documentation site instead.
16
+ SKILL_REPOSITORY_DOCUMENTATION_PATH = re.compile(r"`(docs/[A-Za-z0-9_./-]+\.md)`")
17
+ SKILL_DOCUMENTATION_URL = re.compile(re.escape(DOCUMENTATION_SITE_ROOT) + r"(/[A-Za-z0-9_./-]*)")
18
+ # Skills are copied into a CodeRepository under `.agents/skills/<namespace>/`, so
19
+ # a cross-skill reference maps back to a shipped `agent_scaffold/skills` path.
20
+ SKILL_CROSS_REFERENCE = re.compile(r"`\.agents/skills/[A-Za-z0-9_]+/([A-Za-z0-9_./-]+/SKILL\.md)`")
21
+
22
+
23
+ def _shipped_scaffold_documents() -> tuple[Path, ...]:
24
+ """Every Markdown file the wheel ships under `agent_scaffold`.
25
+
26
+ `AGENTS.md` ships beside the skills and several skills make it their first
27
+ mandatory read, so it carries the same reference obligations they do.
28
+ """
29
+
30
+ return tuple(sorted((REPOSITORY_ROOT / "agent_scaffold").rglob("*.md")))
31
+
32
+
33
+ def _documentation_page_for_url_path(url_path: str) -> Path | None:
34
+ """Resolve a documentation-site URL path to the `docs/` page that renders it.
35
+
36
+ MkDocs serves directory URLs, so `/knowledge/meta_tables/migrations/` renders
37
+ `docs/knowledge/meta_tables/migrations.md` and `/knowledge/meta_tables/`
38
+ renders that directory's `index.md`. The transform is pure, so a skill's
39
+ documentation links are checkable without reaching the network.
40
+ """
41
+
42
+ relative = url_path.strip("/")
43
+ if not relative:
44
+ return REPOSITORY_ROOT / "docs" / "index.md"
45
+ candidates = (
46
+ REPOSITORY_ROOT / "docs" / f"{relative}.md",
47
+ REPOSITORY_ROOT / "docs" / relative / "index.md",
48
+ )
49
+ return next((candidate for candidate in candidates if candidate.is_file()), None)
50
+
51
+
52
+ @pytest.mark.parametrize(
53
+ "path",
54
+ _shipped_scaffold_documents(),
55
+ ids=lambda path: str(path.relative_to(REPOSITORY_ROOT)),
56
+ )
57
+ def test_skills_do_not_cite_repository_relative_documentation_paths(path: Path) -> None:
58
+ relative_path = path.relative_to(REPOSITORY_ROOT)
59
+ text = path.read_text(encoding="utf-8")
60
+
61
+ repository_paths = sorted(set(SKILL_REPOSITORY_DOCUMENTATION_PATH.findall(text)))
62
+
63
+ assert not repository_paths, (
64
+ f"{relative_path} cites SDK documentation by repository-relative path: "
65
+ f"{repository_paths}. The wheel ships no `docs/`, so cite "
66
+ f"{DOCUMENTATION_SITE_ROOT}/... instead."
67
+ )
68
+
69
+
70
+ @pytest.mark.parametrize(
71
+ "path",
72
+ _shipped_scaffold_documents(),
73
+ ids=lambda path: str(path.relative_to(REPOSITORY_ROOT)),
74
+ )
75
+ def test_skill_documentation_urls_render_an_existing_page(path: Path) -> None:
76
+ relative_path = path.relative_to(REPOSITORY_ROOT)
77
+ text = path.read_text(encoding="utf-8")
78
+
79
+ unrendered = sorted(
80
+ {
81
+ url_path
82
+ for url_path in SKILL_DOCUMENTATION_URL.findall(text)
83
+ if _documentation_page_for_url_path(url_path) is None
84
+ }
85
+ )
86
+
87
+ assert not unrendered, (
88
+ f"{relative_path} links documentation-site paths that no `docs/` page "
89
+ f"renders: {unrendered}"
90
+ )
91
+
92
+
93
+ @pytest.mark.parametrize(
94
+ "path",
95
+ _shipped_scaffold_documents(),
96
+ ids=lambda path: str(path.relative_to(REPOSITORY_ROOT)),
97
+ )
98
+ def test_skill_cross_references_resolve(path: Path) -> None:
99
+ relative_path = path.relative_to(REPOSITORY_ROOT)
100
+ text = path.read_text(encoding="utf-8")
101
+
102
+ missing = sorted(
103
+ {
104
+ reference
105
+ for reference in SKILL_CROSS_REFERENCE.findall(text)
106
+ if not (SKILL_ROOT / reference).is_file()
107
+ }
108
+ )
109
+
110
+ assert not missing, f"{relative_path} references skills that are not shipped: {missing}"
@@ -1,50 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pathlib
4
- import sys
5
4
  import tomllib
6
- import types
7
5
 
8
- import pytest
9
6
  from packaging.requirements import Requirement
10
7
 
11
8
 
12
- def _snapshot_mainsequence_modules() -> dict[str, types.ModuleType]:
13
- return {
14
- name: module
15
- for name, module in sys.modules.items()
16
- if name == "mainsequence" or name.startswith("mainsequence.")
17
- }
18
-
19
-
20
- def _reset_mainsequence_modules() -> None:
21
- for name in tuple(sys.modules):
22
- if name == "mainsequence" or name.startswith("mainsequence."):
23
- sys.modules.pop(name, None)
24
-
25
-
26
- @pytest.fixture(autouse=True)
27
- def _restore_mainsequence_modules_after_import_test():
28
- snapshot = _snapshot_mainsequence_modules()
29
- yield
30
- _reset_mainsequence_modules()
31
- sys.modules.update(snapshot)
32
-
33
-
34
- def _seed_mainsequence_packages() -> None:
35
- repo_root = pathlib.Path(__file__).resolve().parents[1]
36
- pkg_root = repo_root / "mainsequence"
37
- client_pkg_root = pkg_root / "client"
38
-
39
- pkg = types.ModuleType("mainsequence")
40
- pkg.__path__ = [str(pkg_root)]
41
- sys.modules["mainsequence"] = pkg
42
-
43
- client_pkg = types.ModuleType("mainsequence.client")
44
- client_pkg.__path__ = [str(client_pkg_root)]
45
- sys.modules["mainsequence.client"] = client_pkg
46
-
47
-
48
9
  def test_streamlit_is_absent_from_dependency_contracts() -> None:
49
10
  repo_root = pathlib.Path(__file__).resolve().parents[1]
50
11
  project = tomllib.loads((repo_root / "pyproject.toml").read_text(encoding="utf-8"))
@@ -43,15 +43,14 @@ def clock(monkeypatch):
43
43
  return clock
44
44
 
45
45
 
46
- @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"])
47
46
  @pytest.mark.parametrize("error", [requests.ReadTimeout, requests.ConnectionError])
48
- def test_write_transport_failure_is_not_replayed(monkeypatch, method, error):
47
+ def test_write_transport_failure_is_not_replayed(monkeypatch, error):
49
48
  session = Mock(headers=requests.structures.CaseInsensitiveDict())
50
- request = getattr(session, method.lower())
49
+ request = session.post
51
50
  request.side_effect = error("uncertain result")
52
51
  monkeypatch.setattr(utils.time, "sleep", lambda _: None)
53
52
 
54
- response = utils.make_request(session, method, "https://example.test/", None, time_out=1)
53
+ response = utils.make_request(session, "POST", "https://example.test/", None, time_out=1)
55
54
 
56
55
  assert response.code == "expired"
57
56
  assert request.call_count == 1
@@ -72,8 +71,10 @@ def test_programming_error_is_not_retried(monkeypatch):
72
71
  assert session.get.call_count == 1
73
72
 
74
73
 
75
- @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"])
76
- @pytest.mark.parametrize("status", [429, 500, 502, 503, 504])
74
+ @pytest.mark.parametrize(
75
+ ("method", "status"),
76
+ [("POST", 429), ("POST", 503), ("PUT", 503), ("PATCH", 503), ("DELETE", 503)],
77
+ )
77
78
  def test_write_status_is_not_retried_by_adapter(monkeypatch, clock, method, status):
78
79
  send = Mock(return_value=answer(status, {"Retry-After": "60"}))
79
80
  monkeypatch.setattr(requests.adapters.HTTPAdapter, "send", send)
@@ -85,15 +86,14 @@ def test_write_status_is_not_retried_by_adapter(monkeypatch, clock, method, stat
85
86
  assert clock.sleeps == []
86
87
 
87
88
 
88
- @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"])
89
89
  @pytest.mark.parametrize(
90
90
  "error", [requests.ReadTimeout, requests.ConnectTimeout, requests.ConnectionError]
91
91
  )
92
- def test_adapter_never_retries_write_exceptions(monkeypatch, clock, method, error):
92
+ def test_adapter_never_retries_write_exceptions(monkeypatch, clock, error):
93
93
  send = Mock(side_effect=error("failed"))
94
94
  monkeypatch.setattr(requests.adapters.HTTPAdapter, "send", send)
95
95
  with utils.build_session() as session, pytest.raises(error):
96
- session.request(method, "https://example.test/", timeout=1)
96
+ session.post("https://example.test/", timeout=1)
97
97
 
98
98
  assert send.call_count == 1
99
99
  assert clock.sleeps == []
@@ -391,19 +391,6 @@ def slow_server(delay=0.15, status=200):
391
391
  server.server_close()
392
392
 
393
393
 
394
- @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"])
395
- def test_slow_server_receives_exactly_one_write(method):
396
- with slow_server() as (url, calls), utils.build_session() as session:
397
- started = time.monotonic()
398
- response = utils.make_request(session, method, url, None, time_out=0.05)
399
- elapsed = time.monotonic() - started
400
- assert response.code == "expired"
401
- assert calls == [method]
402
- assert elapsed < 0.15
403
- time.sleep(0.16)
404
- assert calls == [method]
405
-
406
-
407
394
  def test_slow_get_uses_one_total_budget():
408
395
  with (
409
396
  slow_server(delay=0.04, status=503) as (url, calls),
@@ -5,31 +5,7 @@ import types
5
5
 
6
6
  import pytest
7
7
 
8
-
9
- def _snapshot_mainsequence_modules() -> dict[str, types.ModuleType]:
10
- return {
11
- name: module
12
- for name, module in sys.modules.items()
13
- if name == "mainsequence" or name.startswith("mainsequence.")
14
- }
15
-
16
-
17
- @pytest.fixture(autouse=True)
18
- def _restore_mainsequence_modules():
19
- """Undo the stub packages this module installs into ``sys.modules``.
20
-
21
- ``_load_models_helpers_module`` replaces the real ``mainsequence`` packages
22
- with stubs so ``models_helpers`` can be imported without the heavy client
23
- stack. Without this fixture the stubs leak into every test module that runs
24
- afterwards, and ``monkeypatch.setattr("mainsequence.client....")`` there
25
- fails with ``module 'mainsequence' has no attribute 'client'``.
26
- """
27
- snapshot = _snapshot_mainsequence_modules()
28
- yield
29
- for name in tuple(sys.modules):
30
- if name == "mainsequence" or name.startswith("mainsequence."):
31
- sys.modules.pop(name, None)
32
- sys.modules.update(snapshot)
8
+ pytestmark = pytest.mark.usefixtures("isolated_sdk_imports")
33
9
 
34
10
 
35
11
  def _load_models_helpers_module():
@@ -137,8 +113,7 @@ def test_job_run_job_posts_to_canonical_action(monkeypatch):
137
113
  assert captured == {
138
114
  "r_type": "POST",
139
115
  "url": (
140
- "https://backend.test/api/v1/jobs/"
141
- "7d0ab07c-d1c0-4b7f-9c69-3c1a41c0a4da/run-job/"
116
+ "https://backend.test/api/v1/jobs/" "7d0ab07c-d1c0-4b7f-9c69-3c1a41c0a4da/run-job/"
142
117
  ),
143
118
  "payload": {"json": {"command_args": ["--name", "demo-from-cli"]}},
144
119
  "timeout": 30,
@@ -1,66 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
- import importlib
4
3
  import os
5
- import pathlib
6
- import sys
7
- import types
8
4
 
9
5
  import pytest
10
6
  import requests
11
7
 
8
+ from tests._support import RETIRED_RUNTIME_CONTEXT_ENV_NAMES as _RUNTIME_CONTEXT_ENV_NAMES
9
+ from tests._support import load_sdk_submodule as _load_mainsequence_submodule
10
+
11
+ pytestmark = pytest.mark.usefixtures("isolated_sdk_imports", "clean_runtime_context_environment")
12
+
13
+
12
14
  _REMOVED_DOMAIN_TOKEN = "PRO" + "JECT"
13
15
  _UNSUPPORTED_REPOSITORY_UID_ENV = "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_UID"
14
- _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV = (
15
- "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_BRANCH_UID"
16
- )
17
- _UNSUPPORTED_REPOSITORY_NUMERIC_ID_ENV = (
18
- "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_ID"
19
- )
16
+ _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV = "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_BRANCH_UID"
17
+ _UNSUPPORTED_REPOSITORY_NUMERIC_ID_ENV = "MAIN_SEQUENCE_" + _REMOVED_DOMAIN_TOKEN + "_ID"
20
18
  _UNSUPPORTED_ENVIRONMENT_UID_ENV = (
21
19
  "MAIN_SEQUENCE_ORGANIZATION_" + _REMOVED_DOMAIN_TOKEN + "_ENVIRONMENT_UID"
22
20
  )
23
21
 
24
- _RUNTIME_CONTEXT_ENV_NAMES = (
25
- _UNSUPPORTED_REPOSITORY_UID_ENV,
26
- _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV,
27
- "MAINSEQUENCE_REPOSITORY_BRANCH",
28
- _UNSUPPORTED_ENVIRONMENT_UID_ENV,
29
- )
30
-
31
-
32
- @pytest.fixture(autouse=True)
33
- def _clear_runtime_context_environment():
34
- original_modules = {
35
- name: module
36
- for name, module in sys.modules.items()
37
- if name == "mainsequence" or name.startswith("mainsequence.")
38
- }
39
- for name in _RUNTIME_CONTEXT_ENV_NAMES:
40
- os.environ.pop(name, None)
41
- yield
42
- for name in _RUNTIME_CONTEXT_ENV_NAMES:
43
- os.environ.pop(name, None)
44
- for name in tuple(sys.modules):
45
- if name == "mainsequence" or name.startswith("mainsequence."):
46
- sys.modules.pop(name, None)
47
- sys.modules.update(original_modules)
48
-
49
-
50
- def _load_mainsequence_submodule(module_name: str):
51
- repo_root = pathlib.Path(__file__).resolve().parents[1]
52
- pkg_root = repo_root / "mainsequence"
53
-
54
- for name in tuple(sys.modules):
55
- if name == "mainsequence" or name.startswith("mainsequence."):
56
- sys.modules.pop(name, None)
57
-
58
- pkg = types.ModuleType("mainsequence")
59
- pkg.__path__ = [str(pkg_root)]
60
- sys.modules["mainsequence"] = pkg
61
-
62
- return importlib.import_module(module_name)
63
-
64
22
 
65
23
  def test_is_running_in_pod_uses_job_run_uid(monkeypatch):
66
24
  runtime_flags = _load_mainsequence_submodule("mainsequence.runtime_flags")