Skip to content

feat: add --local-source to E2E smoke test + fix input_schema test - #52

Closed
Brian Krabach (bkrabach) wants to merge 1 commit into
mainfrom
feat/add-local-source-to-e2e-smoke-test-and-fix-input-schema-test
Closed

feat: add --local-source to E2E smoke test + fix input_schema test#52
Brian Krabach (bkrabach) wants to merge 1 commit into
mainfrom
feat/add-local-source-to-e2e-smoke-test-and-fix-input-schema-test

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Adds --local-source flag to e2e-smoke-test.sh for cross-repo validation. Also fixes test_tool_protocol_defines_input_schema to test behavioral contract instead of Protocol metaclass introspection.

--local-source

Allows overriding additional packages (CLI, foundation, modules) with local checkouts during E2E testing. Each path is copied into the Docker container and installed with pip install --force-reinstall --no-deps.

Testing

  • 150 passed, 0 failed, 1 skipped
  • E2E smoke test passes with 7 local overrides

Adds --local-source flag to e2e-smoke-test.sh for cross-repo validation. Allows overriding additional packages (CLI, foundation, modules) with local checkouts during E2E testing. Each path is copied into the Docker container and installed with pip install --force-reinstall --no-deps.

Also fixes test_tool_protocol_defines_input_schema to test behavioral contract (using getattr with default) instead of Protocol metaclass introspection. Python's Protocol metaclass does not expose default property implementations via hasattr/vars, so the test now validates the actual calling pattern (getattr(tool, 'input_schema', {})).

## Testing
- 150 passed, 0 failed, 1 skipped
- E2E smoke test passes with 7 local overrides
@bkrabach

Copy link
Copy Markdown
Collaborator Author

Replacing with a focused PR for --local-source only.

Ken Chau (kenotron-ms) added a commit that referenced this pull request May 8, 2026
…us.cost_usd — revives PR #73

Revives the approach from amplifier-core PR #73 (closed without merge 2026-05-07T18:15:32Z)
with one critical refinement: use when_used='always' instead of default mode='json'-only.

PROBLEM:
M2 cost-tracking introduced cost_usd: Decimal | None on Usage and SessionStatus. Without
serializer help, plain model_dump() (called by every orchestrator: loop-streaming line 472,505,
loop-basic 361, loop-events 307,337) emits Decimal values into events, which crash json.dumps()
at every downstream JSONL write site (hooks-logging, hooks-backup, hook-shell, context-persistent,
foundation/serialization.py). A "patch every consumer" approach doesn't scale to N orchestrators
or future hook modules.

FIX:
Move conversion to the model itself using @field_serializer("cost_usd", when_used="always"):
- model_dump() and model_dump(mode='json') both return JSON-safe strings for cost_usd
- Direct attribute access still returns Decimal for in-memory math
- None survives as None (preserves None ≠ Decimal("0") invariant for "unknown" vs "free")
- Plus _json_default(obj) in models.py + default= in ToolResult.get_serialized_output()
  for non-Pydantic paths where tool outputs may carry Decimal

WHY "when_used='always'" MATTERS:
PR #73 used default field_serializer behavior, which fires only on mode='json'. But every
orchestrator calls plain model_dump(). Verification today (Pydantic 2.13.4) confirmed that
without when_used='always', Decimal values still leak into events on the plain-dump path.

VERIFIED BEHAVIORS (10 tests):
✓ model_dump() returns string for cost_usd
✓ json.dumps(usage.model_dump()) doesn't crash
✓ Direct usage.cost_usd access still returns Decimal
✓ Existing field_validator rejecting float still fires
✓ extra="allow" still works for provider-specific fields
✓ None survives as None (not "None")
✓ Round-trip model_validate(model_dump()) rebuilds with Decimal
✓ Precision preserved across 0.0000123, 12345.6789, 0, 1E-9
✓ Plain model_dump() and mode='json' produce identical output
✓ int coerces to Decimal then serializes to string

NEW TESTS:
- tests/test_cost_usd_serialization.py: 25 tests (all pass)
- 129 adjacent tests still pass (test_message_models, test_session, test_tool_result_autopop,
  test_interfaces, test_generated_equivalence)

VERSION BUMP:
1.5.0 → 1.5.1 (PATCH, per release-mandate)
- pyproject.toml
- crates/amplifier-core/Cargo.toml
- bindings/python/Cargo.toml
- python/amplifier_core/__init__.py

E2E SMOKE TEST: PASSED (run by user before push, per release-mandate Pre-Merge Gate)

FILES CHANGED:
- python/amplifier_core/message_models.py: +field_serializer on Usage.cost_usd
- python/amplifier_core/models.py: +field_serializer on SessionStatus.cost_usd + _json_default + default= in ToolResult
- tests/test_cost_usd_serialization.py: new, 234 lines
- Version files: all bumped to 1.5.1
- Cargo.lock: regenerated by Rust toolchain

UNBLOCKS M2/M3 milestone (microsoft-amplifier/amplifier-support#225):
Once landed, these 11 PRs can drop per-consumer Decimal sanitizer code:
- foundation #190: drop Decimal→str branch from serialization.py
- hooks-logging #5: close as superseded (model_dump(mode='json') becomes redundant)
- provider PRs: anthropic #52, openai #32, azure-openai #24, gemini #28, vllm #22, ollama #14, chat-completions #5
- consumer PRs: app-cli #171, hooks-streaming-ui #11 (unchanged)

Plus 3 modules no longer need follow-up PRs:
- amplifier-module-hooks-backup (automatic safety)
- amplifier-module-hook-shell (automatic safety)
- amplifier-module-context-persistent (automatic safety)

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Salil Das (sadlilas) pushed a commit that referenced this pull request May 8, 2026
…cost stamping (#73)

* fix(core): @field_serializer(when_used='always') on Usage/SessionStatus.cost_usd — revives PR #73

Revives the approach from amplifier-core PR #73 (closed without merge 2026-05-07T18:15:32Z)
with one critical refinement: use when_used='always' instead of default mode='json'-only.

PROBLEM:
M2 cost-tracking introduced cost_usd: Decimal | None on Usage and SessionStatus. Without
serializer help, plain model_dump() (called by every orchestrator: loop-streaming line 472,505,
loop-basic 361, loop-events 307,337) emits Decimal values into events, which crash json.dumps()
at every downstream JSONL write site (hooks-logging, hooks-backup, hook-shell, context-persistent,
foundation/serialization.py). A "patch every consumer" approach doesn't scale to N orchestrators
or future hook modules.

FIX:
Move conversion to the model itself using @field_serializer("cost_usd", when_used="always"):
- model_dump() and model_dump(mode='json') both return JSON-safe strings for cost_usd
- Direct attribute access still returns Decimal for in-memory math
- None survives as None (preserves None ≠ Decimal("0") invariant for "unknown" vs "free")
- Plus _json_default(obj) in models.py + default= in ToolResult.get_serialized_output()
  for non-Pydantic paths where tool outputs may carry Decimal

WHY "when_used='always'" MATTERS:
PR #73 used default field_serializer behavior, which fires only on mode='json'. But every
orchestrator calls plain model_dump(). Verification today (Pydantic 2.13.4) confirmed that
without when_used='always', Decimal values still leak into events on the plain-dump path.

VERIFIED BEHAVIORS (10 tests):
✓ model_dump() returns string for cost_usd
✓ json.dumps(usage.model_dump()) doesn't crash
✓ Direct usage.cost_usd access still returns Decimal
✓ Existing field_validator rejecting float still fires
✓ extra="allow" still works for provider-specific fields
✓ None survives as None (not "None")
✓ Round-trip model_validate(model_dump()) rebuilds with Decimal
✓ Precision preserved across 0.0000123, 12345.6789, 0, 1E-9
✓ Plain model_dump() and mode='json' produce identical output
✓ int coerces to Decimal then serializes to string

NEW TESTS:
- tests/test_cost_usd_serialization.py: 25 tests (all pass)
- 129 adjacent tests still pass (test_message_models, test_session, test_tool_result_autopop,
  test_interfaces, test_generated_equivalence)

VERSION BUMP:
1.5.0 → 1.5.1 (PATCH, per release-mandate)
- pyproject.toml
- crates/amplifier-core/Cargo.toml
- bindings/python/Cargo.toml
- python/amplifier_core/__init__.py

E2E SMOKE TEST: PASSED (run by user before push, per release-mandate Pre-Merge Gate)

FILES CHANGED:
- python/amplifier_core/message_models.py: +field_serializer on Usage.cost_usd
- python/amplifier_core/models.py: +field_serializer on SessionStatus.cost_usd + _json_default + default= in ToolResult
- tests/test_cost_usd_serialization.py: new, 234 lines
- Version files: all bumped to 1.5.1
- Cargo.lock: regenerated by Rust toolchain

UNBLOCKS M2/M3 milestone (microsoft-amplifier/amplifier-support#225):
Once landed, these 11 PRs can drop per-consumer Decimal sanitizer code:
- foundation #190: drop Decimal→str branch from serialization.py
- hooks-logging #5: close as superseded (model_dump(mode='json') becomes redundant)
- provider PRs: anthropic #52, openai #32, azure-openai #24, gemini #28, vllm #22, ollama #14, chat-completions #5
- consumer PRs: app-cli #171, hooks-streaming-ui #11 (unchanged)

Plus 3 modules no longer need follow-up PRs:
- amplifier-module-hooks-backup (automatic safety)
- amplifier-module-hook-shell (automatic safety)
- amplifier-module-context-persistent (automatic safety)

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* test: update test_model_dump_includes_cost_usd_as_decimal for new contract

The test was written for the OLD contract (model_dump() returns Decimal).
With @field_serializer(when_used='always') on Usage.cost_usd, plain
model_dump() now returns string — same as mode='json'. Renamed and
updated assertion to match. Added a "direct attribute access still
returns Decimal" assertion to make the bidirectional contract explicit.

Self-correction: I missed sweeping bindings/python/tests/ in the
original PR's verification pass; only ran tests/. The CI catch is
exactly what the release-mandate Pre-Merge Gate is for.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* test: drop bug-history references; lead docstrings with the contract

Test docstrings shouldn't read like a changelog. A reader landing on
these tests cares about what cost_usd actually is, not which milestone
introduced it or which PR-number-of-the-week it was last fixed in.

- Module docstring opens with "cost_usd is Decimal in memory ... JSON-safe
  string when serialized" and the None ≠ Decimal("0") invariant
- Class docstrings: "Decimal in memory, string on the wire"
- Per-test docstrings describe what the test verifies, not why history
  forced it (no @field_serializer, no _json_default, no PR #73, no M2)
- Also: regenerate uv.lock for the 1.5.0 → 1.5.1 version bump that was
  missed when bumping the other version files manually

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

---------

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Ken Chau (kenotron-ms) added a commit that referenced this pull request May 9, 2026
Fixes microsoft-amplifier/amplifier-support#228.

All four unguarded json.dumps() call sites at the Python/Rust FFI
boundary now pass default=str so Decimal, datetime, and any other
non-JSON-native type becomes its str() representation instead of
raising TypeError and crashing the caller.

Sites fixed:
- helpers.rs: new json_dumps_safe() DRY helper used at all Rust sites
- hooks.rs: emit(), set_default_fields(), emit_and_collect()
- bridges.rs: PyHookHandlerBridge::handle() result-serialization path
- loader_grpc.py: GrpcToolBridge._serialize_input()

Incidental fix: provider-anthropic cost_usd crash (PR #52) is resolved
automatically. str(Decimal("1.23")) == "1.23", identical to the existing
@field_serializer output on the Pydantic model path.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Ken Chau (kenotron-ms) added a commit that referenced this pull request May 9, 2026
Fixes microsoft-amplifier/amplifier-support#228.

All four unguarded json.dumps() call sites at the Python/Rust FFI
boundary now pass default=str so Decimal, datetime, and any other
non-JSON-native type becomes its str() representation instead of
raising TypeError and crashing the caller.

Sites fixed:
- helpers.rs: new json_dumps_safe() DRY helper used at all Rust sites
- hooks.rs: emit(), set_default_fields(), emit_and_collect()
- bridges.rs: PyHookHandlerBridge::handle() result-serialization path
- loader_grpc.py: GrpcToolBridge._serialize_input()

Incidental fix: provider-anthropic cost_usd crash (PR #52) is resolved
automatically. str(Decimal("1.23")) == "1.23", identical to the existing
@field_serializer output on the Pydantic model path.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Ken Chau (kenotron-ms) added a commit that referenced this pull request May 9, 2026
All four unguarded json.dumps() call sites at the Python/Rust FFI
boundary now pass default=str so Decimal, datetime, and any other
non-JSON-native type becomes its str() representation instead of
raising TypeError and crashing the caller.

Sites fixed:
- helpers.rs: new json_dumps_safe() DRY helper used at all Rust sites
- hooks.rs: emit(), set_default_fields(), emit_and_collect()
- bridges.rs: PyHookHandlerBridge::handle() result-serialization path
- loader_grpc.py: GrpcToolBridge._serialize_input()

Incidental fix: provider-anthropic cost_usd crash (PR #52) is resolved
automatically. str(Decimal("1.23")) == "1.23", identical to the existing
@field_serializer output on the Pydantic model path.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Brian Krabach (bkrabach) pushed a commit that referenced this pull request May 12, 2026
…76)

* fix(bindings): sanitize non-JSON-native types at emit() FFI boundary


All four unguarded json.dumps() call sites at the Python/Rust FFI
boundary now pass default=str so Decimal, datetime, and any other
non-JSON-native type becomes its str() representation instead of
raising TypeError and crashing the caller.

Sites fixed:
- helpers.rs: new json_dumps_safe() DRY helper used at all Rust sites
- hooks.rs: emit(), set_default_fields(), emit_and_collect()
- bridges.rs: PyHookHandlerBridge::handle() result-serialization path
- loader_grpc.py: GrpcToolBridge._serialize_input()

Incidental fix: provider-anthropic cost_usd crash (PR #52) is resolved
automatically. str(Decimal("1.23")) == "1.23", identical to the existing
@field_serializer output on the Pydantic model path.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* chore: bump version to 1.5.2

- pyproject.toml: 1.5.1 → 1.5.2
- crates/amplifier-core/Cargo.toml: 1.5.1 → 1.5.2
- bindings/python/Cargo.toml: 1.5.1 → 1.5.2
- python/amplifier_core/__init__.py: 1.5.1 → 1.5.2
- scripts/bump_version.py: regex fix to handle __version__ (trailing __)
- Cargo.lock: updated

Verified: E2E smoke test PASSED — manylinux wheel built, pristine-import OK, Docker container test passed.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* chore: format Python bindings with rustfmt

Fix rustfmt CI check failure by applying standard formatting to:
- json_dumps_safe function signature in helpers.rs
- test binding in tests/mod.rs

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* fix: extend json_dumps_safe() to all FFI json.dumps() call sites + structural guard test

- Apply json_dumps_safe() to all remaining unguarded json.dumps() calls:
  * session.rs: session config serialization
  * coordinator/mod.rs: coordinator config serialization
  * wasm.rs: tool input, provider request/response, hook data, context message, set_messages, approval request (7 sites)
- Add no_raw_json_dumps_outside_helpers structural test that scans .rs files and fails
  if raw json.dumps() calls appear outside the safe helper, preventing future regressions
- Remove private repo reference from test docstring

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

---------

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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.

1 participant