Skip to content

fix: return copies from Scenario secret_get and action_get#2379

Open
tonyandrewmeyer wants to merge 4 commits intocanonical:mainfrom
tonyandrewmeyer:fix/scenario-secret-action-get-copy
Open

fix: return copies from Scenario secret_get and action_get#2379
tonyandrewmeyer wants to merge 4 commits intocanonical:mainfrom
tonyandrewmeyer:fix/scenario-secret-action-get-copy

Conversation

@tonyandrewmeyer
Copy link
Collaborator

@tonyandrewmeyer tonyandrewmeyer commented Mar 15, 2026

Summary

  • secret_get() in _MockModelBackend returned secret.latest_content and secret.tracked_content directly, without copying. In production, Secret.get_content() returns self._content.copy(). Mutating the returned dict would silently corrupt internal Scenario state.
  • action_get() in _MockModelBackend returned action.params directly. In production, action_get() returns fresh data from Juju, and in Harness it builds a new dict each time. Same mutation-leaks-into-state issue.

Both are the same class of bug fixed in commit be090122 for relation_get, which returned data.copy() to prevent the mock's internal state from being shared with the caller.

Changes

  • testing/src/scenario/mocking.py: secret_get() now returns .copy() on both the latest_content and tracked_content paths; action_get() now returns dict(action.params).
  • Regression tests added in test_secrets.py and test_actions.py that verify mutating the returned dict does not affect subsequent calls.

🤖 Generated with Claude Code but owned by me.

`_MockModelBackend.secret_get()` returned `secret.latest_content` and
`secret.tracked_content` directly, and `action_get()` returned
`action.params` directly. This meant that mutating the returned dict
would silently corrupt the Scenario internal state, a divergence from
production behaviour where fresh data is returned each time.

Apply the same fix as commit be09012 (which addressed the identical
issue in `relation_get`): return `.copy()` / `dict()` so callers get
independent copies.

Includes regression tests that verify mutating the returned dict does
not affect subsequent calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonyandrewmeyer tonyandrewmeyer force-pushed the fix/scenario-secret-action-get-copy branch from 8aa7b17 to 430050e Compare March 15, 2026 07:35
claude added 2 commits March 17, 2026 13:29
Juju requires secrets to have at least one key with string keys and
values. Validate this in Secret.__post_init__ so charmers get a clear
error at construction time rather than a confusing failure during .run().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rather than raising on the first non-string key/value, collect all bad
entries and report them together. Also use lowercase dict[str, str] in
the error message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonyandrewmeyer tonyandrewmeyer force-pushed the fix/scenario-secret-action-get-copy branch from 82b2591 to f955cf2 Compare March 17, 2026 00:29
ctx.run(ctx.on.action('foo', params={'bar': 10}), State())

assert len(results) == 1
mutated, fresh = results[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we tend to prefer styling this as written already, but doesn't this look cool?

    [(mutated, fresh)] = results

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, no? 😂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥲

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

James, why not make it more magic: (mut, fsh), = results 🙃

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look as cool 🤓

- Move _validate_content below __post_init__ in Secret
- Use copy.deepcopy for action_get (params can be nested)
- Dedent test assertions outside context manager scope

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tonyandrewmeyer tonyandrewmeyer requested a review from dimaqq March 17, 2026 04:37
Comment on lines +327 to +328
k: v for k, v in content.items()
if not isinstance(k, str) or not isinstance(v, str)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruff wants these on one line...

Comment on lines +332 to +333
f'Secret.{name} should be dict[str, str]; '
f'found non-string key(s)/value(s): {bad}',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here too, ruff prefers a single line.

I think the tooling that you use didn't catch our custom line lenght limit.

@dimaqq
Copy link
Contributor

dimaqq commented Mar 18, 2026

test_storedstate_consistency fails, the secret included into stored state (whatever what means!) needs a dummy field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants