Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 26
Follow-ups from #441: secret-helper migration, ten more bug fixes, and coverage for every remaining untested module#444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2efd49a
refactor(web): use canonical secret helpers in api_v3; make ConfigMan…
claude 51e6c27
fix: repair broken helper paths across display, cache, odds, logging,…
claude c871176
test: cover the previously untested modules
claude 2653502
test: real schedule/dim coverage for DisplayController; fix two vacuo…
claude c9239d0
ci: raise coverage floor to 48%
claude 41f91a4
fix: address CodeQL alert and review findings
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -269,20 +269,47 @@ def load_config(self) -> Dict[str, Any]: | ||
| self.logger.error(error_msg, exc_info=True) | ||
| raise ConfigError(error_msg, config_path=self.config_path) from e | ||
| @staticmethod | ||
| def _is_parallel_secrets_list(value: Any) -> bool: | ||
| """True for the parallel-placeholder list shape emitted by | ||
| ``secret_helpers.separate_secrets`` for array-item secrets: a | ||
| non-empty list whose elements are ALL dicts (``{}`` marks an item | ||
| with no secrets). Any other list-shaped secrets value is a | ||
| whole-key secret (e.g. a list of secret scalars).""" | ||
| return (isinstance(value, list) and bool(value) | ||
| and all(isinstance(item, dict) for item in value)) | ||
| def _strip_secrets_recursive(self, data_to_filter: Dict[str, Any], secrets: Dict[str, Any]) -> Dict[str, Any]: | ||
| """Recursively remove secret keys from a dictionary.""" | ||
| result = {} | ||
| for key, value in data_to_filter.items(): | ||
| if key in secrets: | ||
| if isinstance(value, dict) and isinstance(secrets[key], dict): | ||
| # This key is a shared group, recurse | ||
| stripped_sub_dict = self._strip_secrets_recursive(value, secrets[key]) | ||
| if stripped_sub_dict: # Only add if there's non-secret data left | ||
| result[key] = stripped_sub_dict | ||
| # Else, it's a secret key at this level, so we skip it | ||
| else: | ||
| if key not in secrets: | ||
| # This key is not in secrets, so we keep it | ||
| result[key] = value | ||
| continue | ||
| sec = secrets[key] | ||
| if isinstance(value, dict) and isinstance(sec, dict): | ||
| # This key is a shared group, recurse | ||
| stripped_sub_dict = self._strip_secrets_recursive(value, sec) | ||
| if stripped_sub_dict: # Only add if there's non-secret data left | ||
| result[key] = stripped_sub_dict | ||
| elif isinstance(value, list) and self._is_parallel_secrets_list(sec): | ||
| # Parallel-list shape from separate_secrets: sec[i] holds the | ||
| # secret fields of value[i] ({} = item i has none). Strip each | ||
| # item and ALWAYS keep the list — indices must survive so the | ||
| # merge-on-load can realign secrets with their items. The | ||
| # regular list's length is authoritative: extra secrets | ||
| # entries are ignored. | ||
| stripped_items = [] | ||
| for i, item in enumerate(value): | ||
| s_item = sec[i] if i < len(sec) else {} | ||
| if isinstance(item, dict) and s_item: | ||
| stripped_items.append(self._strip_secrets_recursive(item, s_item)) | ||
| else: | ||
| stripped_items.append(item) | ||
| result[key] = stripped_items | ||
| # Else: whole-key secret (scalar, list of secret scalars, or a | ||
| # shape mismatch) -> drop the key entirely. Never leak. | ||
ChuckBuilds marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return result | ||
| def _load_secrets_for_save(self) -> Dict[str, Any]: | ||
| @@ -358,11 +385,39 @@ def get_secret(self, key: str) -> Optional[Any]: | ||
| return None | ||
| def _deep_merge(self, target: Dict[str, Any], source: Dict[str, Any]) -> None: | ||
| """Deep merge source dict into target dict.""" | ||
| """Deep merge source dict into target dict. | ||
| Sole call site: merging config_secrets.json into the loaded config. | ||
| Understands the parallel-list shape separate_secrets emits for | ||
| array-item secrets (see _is_parallel_secrets_list): each secrets | ||
| list item is merged into the config list item at the same index | ||
| ({} placeholders skipped). The config list's length is | ||
| authoritative — a user deleting an array item from config.json | ||
| must not have it resurrected from a stale secrets entry.""" | ||
| for key, value in source.items(): | ||
| if key in target and isinstance(target[key], dict) and isinstance(value, dict): | ||
| self._deep_merge(target[key], value) | ||
| elif (key in target and isinstance(target[key], list) | ||
| and self._is_parallel_secrets_list(value)): | ||
| tlist = target[key] | ||
| for i, s_item in enumerate(value): | ||
| if i >= len(tlist): | ||
| # Interpolate only config-side data here — nothing | ||
| # iterated out of the secrets dict (not even the key | ||
| # name) may reach the log. | ||
| self.logger.warning( | ||
| "A secrets list is longer than the config list it " | ||
| "parallels (config has %d item(s)); ignoring the " | ||
| "extra entries", len(tlist)) | ||
| break | ||
| if not s_item: | ||
| continue # {} placeholder: item i has no secrets | ||
| if isinstance(tlist[i], dict): | ||
| self._deep_merge(tlist[i], s_item) | ||
| else: | ||
| tlist[i] = s_item # shape drift; the secret wins | ||
| else: | ||
| # Scalars AND whole-secret scalar arrays: replace (legacy). | ||
| target[key] = value | ||
| def _create_config_from_template(self) -> None: | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.