gowalk-cicd 1.0.0

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 (84) hide show
  1. package/CLAUDE.md +94 -0
  2. package/README.md +417 -0
  3. package/action/.daemux-version +1 -0
  4. package/action/MIGRATION.md +232 -0
  5. package/action/action.yml +973 -0
  6. package/action/prompts/generate_dependent_fields.prompt.yml +78 -0
  7. package/action/prompts/generate_descriptions.prompt.yml +59 -0
  8. package/action/scripts/app_context_scanner.py +239 -0
  9. package/action/scripts/asc_build_history.py +284 -0
  10. package/action/scripts/asc_common.py +246 -0
  11. package/action/scripts/asc_metadata_applier.py +368 -0
  12. package/action/scripts/asc_metadata_detector.py +250 -0
  13. package/action/scripts/asc_version_create.py +365 -0
  14. package/action/scripts/asc_version_fetch.py +274 -0
  15. package/action/scripts/asc_version_reuse.py +177 -0
  16. package/action/scripts/auto_detect.py +391 -0
  17. package/action/scripts/autoupdate_check.sh +118 -0
  18. package/action/scripts/cert_factory.py +190 -0
  19. package/action/scripts/cfg_io.py +27 -0
  20. package/action/scripts/cfg_resolve.py +147 -0
  21. package/action/scripts/commit_bot_changes.sh +82 -0
  22. package/action/scripts/creds_store.py +328 -0
  23. package/action/scripts/keychain.py +103 -0
  24. package/action/scripts/lookup_app_id.py +51 -0
  25. package/action/scripts/manage_marketing_version.py +337 -0
  26. package/action/scripts/metadata_constants.py +102 -0
  27. package/action/scripts/mmv_decide_create.py +181 -0
  28. package/action/scripts/mmv_floor_check.py +265 -0
  29. package/action/scripts/next_build_number.py +195 -0
  30. package/action/scripts/pbxproj_editor.py +220 -0
  31. package/action/scripts/prepare_signing.py +223 -0
  32. package/action/scripts/profile_io.py +64 -0
  33. package/action/scripts/profile_manager.py +307 -0
  34. package/action/scripts/read_config.py +315 -0
  35. package/action/scripts/resolve_marketing_version.py +173 -0
  36. package/action/scripts/set_app_store_whats_new.py +317 -0
  37. package/action/scripts/team_resolver.py +130 -0
  38. package/action/scripts/test_app_context_scanner.py +167 -0
  39. package/action/scripts/test_asc_build_history.py +221 -0
  40. package/action/scripts/test_asc_builds_prerelease.py +219 -0
  41. package/action/scripts/test_asc_common_timeouts.py +121 -0
  42. package/action/scripts/test_asc_metadata_applier.py +850 -0
  43. package/action/scripts/test_asc_metadata_detector.py +445 -0
  44. package/action/scripts/test_auto_detect.py +432 -0
  45. package/action/scripts/test_cfg_resolve_credentials.py +48 -0
  46. package/action/scripts/test_compute_next_version.py +95 -0
  47. package/action/scripts/test_compute_next_version_patch_backcompat.py +41 -0
  48. package/action/scripts/test_compute_next_version_strictness.py +67 -0
  49. package/action/scripts/test_fetch_versions.py +163 -0
  50. package/action/scripts/test_ground_truth_floor.py +104 -0
  51. package/action/scripts/test_manage_marketing_version.py +134 -0
  52. package/action/scripts/test_manage_marketing_version_autoroll.py +149 -0
  53. package/action/scripts/test_manage_marketing_version_floor.py +170 -0
  54. package/action/scripts/test_manage_marketing_version_match.py +187 -0
  55. package/action/scripts/test_mmv_409_classifier.py +231 -0
  56. package/action/scripts/test_mmv_autobump_persist_gate.py +131 -0
  57. package/action/scripts/test_mmv_decide_create.py +119 -0
  58. package/action/scripts/test_mmv_floor_check.py +158 -0
  59. package/action/scripts/test_mmv_floor_crosscheck.py +132 -0
  60. package/action/scripts/test_mmv_helpers.py +57 -0
  61. package/action/scripts/test_next_build_number.py +174 -0
  62. package/action/scripts/test_read_config_auto_detect.py +257 -0
  63. package/action/scripts/test_resolve_marketing_version.py +298 -0
  64. package/action/scripts/test_reuse_stale_editable.py +182 -0
  65. package/action/scripts/test_set_app_store_whats_new.py +71 -0
  66. package/action/scripts/test_team_fallback_wiring.py +157 -0
  67. package/action/scripts/test_team_resolver.py +249 -0
  68. package/action/scripts/tests_common.py +167 -0
  69. package/action/scripts/version_utils.py +373 -0
  70. package/android-action/.daemux-version +1 -0
  71. package/android-action/action.yml +92 -0
  72. package/android-action/scripts/android_config.py +130 -0
  73. package/android-action/scripts/bitrise_deploy.py +160 -0
  74. package/android-action/scripts/find_bundle.py +25 -0
  75. package/android-action/scripts/play_preflight.py +69 -0
  76. package/android-action/scripts/resolve_android.py +70 -0
  77. package/android-action/scripts/sign_bundle.py +70 -0
  78. package/android-action/scripts/test_android_config.py +97 -0
  79. package/android-action/scripts/test_bitrise_deploy.py +124 -0
  80. package/android-action/scripts/test_sign_bundle.py +82 -0
  81. package/bin/cli.mjs +63 -0
  82. package/package.json +57 -0
  83. package/src/install.mjs +208 -0
  84. package/templates/deploy.yml +150 -0
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for next_build_number.py.
4
+
5
+ All ASC HTTP is stubbed via unittest.mock; no network calls. Focus:
6
+ * ``/builds`` alone returns the correct max + 1.
7
+ * ``/preReleaseVersions`` surfaces fresh uploads missing from ``/builds``.
8
+ * Union takes the max across both sources.
9
+ * Non-integer / malformed version strings are ignored, not fatal.
10
+ * ASC failure falls back to the local pbxproj CURRENT_PROJECT_VERSION.
11
+ * Both sources empty + no pbxproj -> exit non-zero with a clear error.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import io
17
+ import sys
18
+ import unittest
19
+ from contextlib import redirect_stdout
20
+ from pathlib import Path
21
+ from unittest import mock
22
+
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
24
+
25
+ import next_build_number as nb # noqa: E402
26
+
27
+
28
+ def _build(version: str, state: str = "VALID", uploaded: str = "") -> dict:
29
+ return {
30
+ "type": "builds",
31
+ "attributes": {
32
+ "version": version,
33
+ "processingState": state,
34
+ "uploadedDate": uploaded,
35
+ },
36
+ }
37
+
38
+
39
+ class BuildVersionIntTests(unittest.TestCase):
40
+ def test_parses_integer_version_string(self):
41
+ self.assertEqual(nb._build_version_int(_build("12")), 12)
42
+
43
+ def test_returns_none_for_non_integer(self):
44
+ self.assertIsNone(nb._build_version_int(_build("1.0.3")))
45
+
46
+ def test_returns_none_for_missing_attributes(self):
47
+ self.assertIsNone(nb._build_version_int({}))
48
+
49
+
50
+ class FetchBuildsTests(unittest.TestCase):
51
+ def test_returns_versions_from_builds_endpoint(self):
52
+ payload = {"data": [_build("3"), _build("7"), _build("1")]}
53
+ with mock.patch.object(nb, "get_json", return_value=payload) as mget:
54
+ got = nb.fetch_build_versions("111", "tok")
55
+ self.assertEqual(got, {1, 3, 7})
56
+ # Confirm the call used filter[app] and no state filter.
57
+ _args, kwargs = mget.call_args
58
+ self.assertEqual(kwargs["params"]["filter[app]"], "111")
59
+ self.assertNotIn("filter[processingState]", kwargs["params"])
60
+
61
+ def test_empty_data_returns_empty_set(self):
62
+ with mock.patch.object(nb, "get_json", return_value={"data": []}):
63
+ self.assertEqual(nb.fetch_build_versions("111", "tok"), set())
64
+
65
+
66
+ class FetchPreReleaseVersionsTests(unittest.TestCase):
67
+ def test_harvests_included_builds(self):
68
+ payload = {
69
+ "data": [{"type": "preReleaseVersions", "id": "P1"}],
70
+ "included": [
71
+ _build("5", "PROCESSING", "2026-04-18T10:00:00Z"),
72
+ {"type": "apps", "id": "X"}, # non-build resource ignored
73
+ _build("9"),
74
+ ],
75
+ }
76
+ with mock.patch.object(nb, "get_json", return_value=payload):
77
+ got = nb.fetch_prerelease_versions("111", "tok")
78
+ self.assertEqual(got, {5, 9})
79
+
80
+ def test_missing_included_is_handled(self):
81
+ with mock.patch.object(nb, "get_json", return_value={"data": []}):
82
+ got = nb.fetch_prerelease_versions("111", "tok")
83
+ self.assertEqual(got, set())
84
+
85
+
86
+ class ResolveNextBuildNumberTests(unittest.TestCase):
87
+ def test_max_across_both_sources_plus_one(self):
88
+ with mock.patch.object(nb, "fetch_build_versions", return_value={1, 2}), \
89
+ mock.patch.object(nb, "fetch_prerelease_versions", return_value={5}):
90
+ self.assertEqual(nb.resolve_next_build_number("111", "tok"), 6)
91
+
92
+ def test_prerelease_fills_gap_when_builds_empty(self):
93
+ # Simulates the exact bug: /builds misses the fresh upload but
94
+ # /preReleaseVersions still sees it.
95
+ with mock.patch.object(nb, "fetch_build_versions", return_value=set()), \
96
+ mock.patch.object(nb, "fetch_prerelease_versions", return_value={1}):
97
+ self.assertEqual(nb.resolve_next_build_number("111", "tok"), 2)
98
+
99
+ def test_both_empty_returns_one(self):
100
+ with mock.patch.object(nb, "fetch_build_versions", return_value=set()), \
101
+ mock.patch.object(nb, "fetch_prerelease_versions", return_value=set()):
102
+ self.assertEqual(nb.resolve_next_build_number("111", "tok"), 1)
103
+
104
+ def test_ignores_non_integer_versions(self):
105
+ # fetch_* functions already filter; resolve just unions and maxes.
106
+ with mock.patch.object(nb, "fetch_build_versions", return_value={3}), \
107
+ mock.patch.object(nb, "fetch_prerelease_versions", return_value={3}):
108
+ self.assertEqual(nb.resolve_next_build_number("111", "tok"), 4)
109
+
110
+
111
+ class MainFallbackTests(unittest.TestCase):
112
+ def _run_main(self) -> str:
113
+ buf = io.StringIO()
114
+ with redirect_stdout(buf):
115
+ nb.main()
116
+ return buf.getvalue().strip()
117
+
118
+ def test_asc_success_prints_next(self):
119
+ env = {
120
+ "APP_STORE_APPLE_ID": "111",
121
+ "ASC_KEY_ID": "K",
122
+ "ASC_ISSUER_ID": "I",
123
+ "ASC_KEY_PATH": "/tmp/x",
124
+ }
125
+ with mock.patch.dict(nb.os.environ, env, clear=False), \
126
+ mock.patch.object(nb, "make_jwt", return_value="tok"), \
127
+ mock.patch.object(nb, "resolve_next_build_number", return_value=42):
128
+ self.assertEqual(self._run_main(), "42")
129
+
130
+ def test_asc_failure_falls_back_to_pbxproj(self):
131
+ env = {
132
+ "APP_STORE_APPLE_ID": "111",
133
+ "ASC_KEY_ID": "K",
134
+ "ASC_ISSUER_ID": "I",
135
+ "ASC_KEY_PATH": "/tmp/x",
136
+ }
137
+ with mock.patch.dict(nb.os.environ, env, clear=False), \
138
+ mock.patch.object(nb, "make_jwt", return_value="tok"), \
139
+ mock.patch.object(
140
+ nb, "resolve_next_build_number",
141
+ side_effect=RuntimeError("ASC 503"),
142
+ ), \
143
+ mock.patch.object(nb, "_read_pbxproj_build_number", return_value=7):
144
+ self.assertEqual(self._run_main(), "8")
145
+
146
+ def test_asc_failure_and_no_pbxproj_exits_nonzero(self):
147
+ env = {
148
+ "APP_STORE_APPLE_ID": "111",
149
+ "ASC_KEY_ID": "K",
150
+ "ASC_ISSUER_ID": "I",
151
+ "ASC_KEY_PATH": "/tmp/x",
152
+ }
153
+ with mock.patch.dict(nb.os.environ, env, clear=False), \
154
+ mock.patch.object(nb, "make_jwt", return_value="tok"), \
155
+ mock.patch.object(
156
+ nb, "resolve_next_build_number",
157
+ side_effect=RuntimeError("ASC 503"),
158
+ ), \
159
+ mock.patch.object(nb, "_read_pbxproj_build_number", return_value=None):
160
+ with self.assertRaises(SystemExit):
161
+ nb.main()
162
+
163
+ def test_missing_apple_id_raises_actionable_error(self):
164
+ env_missing = {k: "" for k in [
165
+ "APP_STORE_APPLE_ID", "ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_KEY_PATH",
166
+ ]}
167
+ with mock.patch.dict(nb.os.environ, env_missing, clear=False):
168
+ with self.assertRaises(SystemExit) as ctx:
169
+ nb.main()
170
+ self.assertIn("APP_STORE_APPLE_ID", str(ctx.exception))
171
+
172
+
173
+ if __name__ == "__main__":
174
+ unittest.main()
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Integration tests for read_config (post-YAML-removal). Verify that:
4
+ - resolve_xcode runs auto_detect when input is empty.
5
+ - resolve_xcode honors INPUT_* overrides.
6
+ - resolve_app calls auto_detect_bundle_id when input is empty.
7
+ - Source labels for log output are auditable.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ import unittest
14
+ from pathlib import Path
15
+ from unittest import mock
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+
19
+ import auto_detect # noqa: E402
20
+ import read_config # noqa: E402
21
+
22
+
23
+ def _empty_inputs() -> dict:
24
+ return {
25
+ "PROJECT": "",
26
+ "WORKSPACE": "",
27
+ "SCHEME": "",
28
+ "CONFIGURATION": "",
29
+ "PROFILE_NAME": "",
30
+ "BUNDLE_ID": "",
31
+ "TEAM_ID": "",
32
+ "APP_STORE_APPLE_ID": "",
33
+ "USES_NON_EXEMPT": "",
34
+ "WHATS_NEW": "",
35
+ "LOCALE": "",
36
+ "RUN_TESTS": "",
37
+ "TEST_COMMAND": "",
38
+ "TEST_DESTINATION": "",
39
+ }
40
+
41
+
42
+ class ResolveXcodeAutoDetectTests(unittest.TestCase):
43
+ def setUp(self):
44
+ auto_detect._clear_caches()
45
+
46
+ def test_auto_detect_used_when_inputs_empty(self):
47
+ workspace = Path("/fake/workspace")
48
+ with mock.patch.object(
49
+ auto_detect,
50
+ "auto_detect_project",
51
+ return_value=("MyApp.xcodeproj", ""),
52
+ ) as m_proj, mock.patch.object(
53
+ auto_detect, "auto_detect_scheme", return_value="MyApp",
54
+ ) as m_scheme:
55
+ xc = read_config.resolve_xcode(workspace, _empty_inputs())
56
+
57
+ m_proj.assert_called_once_with(workspace)
58
+ m_scheme.assert_called_once()
59
+ self.assertEqual(xc["project"], ("MyApp.xcodeproj", "auto-detect"))
60
+ self.assertEqual(xc["workspace"], ("", "empty"))
61
+ self.assertEqual(xc["scheme"], ("MyApp", "auto-detect"))
62
+ self.assertEqual(xc["configuration"], ("Release", "default"))
63
+ self.assertEqual(xc["profile_name"], ("MyApp CI", "default"))
64
+
65
+ def test_input_value_wins_over_auto_detect(self):
66
+ workspace = Path("/fake/workspace")
67
+ inputs = _empty_inputs()
68
+ inputs["PROJECT"] = "OverrideProject.xcodeproj"
69
+ inputs["SCHEME"] = "OverrideScheme"
70
+ with mock.patch.object(
71
+ auto_detect, "auto_detect_project",
72
+ return_value=("Auto.xcodeproj", ""),
73
+ ) as m_proj, mock.patch.object(
74
+ auto_detect, "auto_detect_scheme", return_value="AutoScheme",
75
+ ) as m_scheme:
76
+ xc = read_config.resolve_xcode(workspace, inputs)
77
+ m_proj.assert_not_called()
78
+ m_scheme.assert_not_called()
79
+ self.assertEqual(xc["project"], ("OverrideProject.xcodeproj", "input"))
80
+ self.assertEqual(xc["scheme"], ("OverrideScheme", "input"))
81
+
82
+ def test_workspace_input_wins(self):
83
+ workspace = Path("/fake/workspace")
84
+ inputs = _empty_inputs()
85
+ inputs["WORKSPACE"] = "MyApp.xcworkspace"
86
+ with mock.patch.object(
87
+ auto_detect, "auto_detect_scheme", return_value="MyApp",
88
+ ):
89
+ xc = read_config.resolve_xcode(workspace, inputs)
90
+ self.assertEqual(xc["workspace"], ("MyApp.xcworkspace", "input"))
91
+ self.assertEqual(xc["project"], ("", "empty"))
92
+
93
+ def test_workspace_auto_detect_preferred_over_xcodeproj(self):
94
+ workspace = Path("/fake/workspace")
95
+ with mock.patch.object(
96
+ auto_detect, "auto_detect_project",
97
+ return_value=("", "MyApp.xcworkspace"),
98
+ ), mock.patch.object(
99
+ auto_detect, "auto_detect_scheme", return_value="MyApp",
100
+ ):
101
+ xc = read_config.resolve_xcode(workspace, _empty_inputs())
102
+ self.assertEqual(xc["workspace"], ("MyApp.xcworkspace", "auto-detect"))
103
+ self.assertEqual(xc["project"], ("", "empty"))
104
+
105
+ def test_scheme_empty_when_auto_detect_fails(self):
106
+ workspace = Path("/fake/workspace")
107
+ with mock.patch.object(
108
+ auto_detect, "auto_detect_project",
109
+ return_value=("MyApp.xcodeproj", ""),
110
+ ), mock.patch.object(
111
+ auto_detect, "auto_detect_scheme", return_value=None,
112
+ ):
113
+ xc = read_config.resolve_xcode(workspace, _empty_inputs())
114
+ self.assertEqual(xc["scheme"], ("", "empty"))
115
+
116
+ def test_configuration_input_wins_over_default(self):
117
+ workspace = Path("/fake/workspace")
118
+ inputs = _empty_inputs()
119
+ inputs["CONFIGURATION"] = "Debug"
120
+ with mock.patch.object(
121
+ auto_detect, "auto_detect_project", return_value=("X.xcodeproj", ""),
122
+ ), mock.patch.object(
123
+ auto_detect, "auto_detect_scheme", return_value="X",
124
+ ):
125
+ xc = read_config.resolve_xcode(workspace, inputs)
126
+ self.assertEqual(xc["configuration"], ("Debug", "input"))
127
+
128
+
129
+ class ResolveAppAutoDetectTests(unittest.TestCase):
130
+ def setUp(self):
131
+ auto_detect._clear_caches()
132
+
133
+ def _xc(self, **over) -> dict:
134
+ base = {
135
+ "project": "MyApp.xcodeproj",
136
+ "workspace": "",
137
+ "scheme": "MyApp",
138
+ "configuration": "Release",
139
+ }
140
+ base.update(over)
141
+ return base
142
+
143
+ def test_auto_detects_bundle_when_input_empty(self):
144
+ creds = {"key_id": "K", "issuer_id": "I", "key_path": "/tmp/k.p8"}
145
+ scripts_dir = Path(__file__).resolve().parent
146
+ workspace = Path("/fake/workspace")
147
+ with mock.patch.object(
148
+ auto_detect,
149
+ "auto_detect_bundle_id",
150
+ return_value="com.example.myapp",
151
+ ) as m_bundle, mock.patch.object(
152
+ read_config,
153
+ "derive_team_if_empty",
154
+ return_value=("TEAM123456", "derived_from_asc_key"),
155
+ ), mock.patch.object(
156
+ read_config, "lookup_app_id_via_api", return_value="1234567890",
157
+ ):
158
+ app = read_config.resolve_app(
159
+ _empty_inputs(), creds, scripts_dir,
160
+ workspace=workspace, xcode=self._xc(),
161
+ )
162
+ m_bundle.assert_called_once()
163
+ self.assertEqual(app["bundle_id"], ("com.example.myapp", "auto-detect"))
164
+ self.assertEqual(app["team_id"], ("TEAM123456", "derived_from_asc_key"))
165
+ self.assertEqual(app["app_store_apple_id"], ("1234567890", "api-lookup"))
166
+
167
+ def test_input_bundle_wins_over_auto_detect(self):
168
+ creds = {"key_id": "K", "issuer_id": "I", "key_path": "/tmp/k.p8"}
169
+ scripts_dir = Path(__file__).resolve().parent
170
+ workspace = Path("/fake/workspace")
171
+ inputs = _empty_inputs()
172
+ inputs["BUNDLE_ID"] = "com.input.bundle"
173
+ with mock.patch.object(
174
+ auto_detect, "auto_detect_bundle_id", return_value="com.auto.different",
175
+ ) as m_bundle, mock.patch.object(
176
+ read_config, "derive_team_if_empty",
177
+ return_value=("T", "derived_from_asc_key"),
178
+ ), mock.patch.object(
179
+ read_config, "lookup_app_id_via_api", return_value="1",
180
+ ):
181
+ app = read_config.resolve_app(
182
+ inputs, creds, scripts_dir,
183
+ workspace=workspace, xcode=self._xc(),
184
+ )
185
+ m_bundle.assert_not_called()
186
+ self.assertEqual(app["bundle_id"], ("com.input.bundle", "input"))
187
+
188
+ def test_input_team_id_wins_over_derive(self):
189
+ creds = {"key_id": "K", "issuer_id": "I", "key_path": "/tmp/k.p8"}
190
+ scripts_dir = Path(__file__).resolve().parent
191
+ workspace = Path("/fake/workspace")
192
+ inputs = _empty_inputs()
193
+ inputs["TEAM_ID"] = "INPUTTEAM1"
194
+ inputs["BUNDLE_ID"] = "com.x.y"
195
+ with mock.patch.object(
196
+ read_config, "derive_team_if_empty",
197
+ ) as m_derive, mock.patch.object(
198
+ read_config, "lookup_app_id_via_api", return_value="1",
199
+ ):
200
+ app = read_config.resolve_app(
201
+ inputs, creds, scripts_dir,
202
+ workspace=workspace, xcode=self._xc(),
203
+ )
204
+ m_derive.assert_not_called()
205
+ self.assertEqual(app["team_id"], ("INPUTTEAM1", "input"))
206
+
207
+ def test_input_apple_id_skips_api_lookup(self):
208
+ creds = {"key_id": "K", "issuer_id": "I", "key_path": "/tmp/k.p8"}
209
+ scripts_dir = Path(__file__).resolve().parent
210
+ workspace = Path("/fake/workspace")
211
+ inputs = _empty_inputs()
212
+ inputs["APP_STORE_APPLE_ID"] = "9999999"
213
+ inputs["BUNDLE_ID"] = "com.x.y"
214
+ with mock.patch.object(
215
+ read_config, "derive_team_if_empty",
216
+ return_value=("T", "derived_from_asc_key"),
217
+ ), mock.patch.object(
218
+ read_config, "lookup_app_id_via_api",
219
+ ) as m_lookup:
220
+ app = read_config.resolve_app(
221
+ inputs, creds, scripts_dir,
222
+ workspace=workspace, xcode=self._xc(),
223
+ )
224
+ m_lookup.assert_not_called()
225
+ self.assertEqual(app["app_store_apple_id"], ("9999999", "input"))
226
+
227
+
228
+ class DefaultsTests(unittest.TestCase):
229
+ """Defaults emitted when nothing comes from inputs."""
230
+
231
+ def test_resolve_testflight_returns_default_whats_new_and_locale(self):
232
+ tf = read_config.resolve_testflight(_empty_inputs())
233
+ self.assertEqual(tf["whats_new"][1], "default")
234
+ self.assertIn("Improved performance", tf["whats_new"][0])
235
+ self.assertEqual(tf["locale"], ("en-US", "default"))
236
+
237
+ def test_resolve_testflight_input_wins(self):
238
+ inputs = _empty_inputs()
239
+ inputs["WHATS_NEW"] = "release notes here"
240
+ inputs["LOCALE"] = "fr-FR"
241
+ tf = read_config.resolve_testflight(inputs)
242
+ self.assertEqual(tf["whats_new"], ("release notes here", "input"))
243
+ self.assertEqual(tf["locale"], ("fr-FR", "input"))
244
+
245
+ def test_resolve_tests_defaults_to_false(self):
246
+ tests = read_config.resolve_tests(_empty_inputs())
247
+ self.assertEqual(tests["run_tests"], ("false", "default"))
248
+ self.assertEqual(tests["test_command"], ("", "empty"))
249
+ self.assertEqual(tests["test_destination"], ("", "empty"))
250
+
251
+ def test_resolve_ios_defaults_to_false(self):
252
+ ios = read_config.resolve_ios(_empty_inputs())
253
+ self.assertEqual(ios["uses_non_exempt"], ("false", "default"))
254
+
255
+
256
+ if __name__ == "__main__":
257
+ unittest.main()