@phase2 @acms @acms_pipeline Feature: ACMS v1 Context Assembly Pipeline As a CleverAgents developer I want to assemble context fragments into budget-constrained payloads So that actors receive relevant, prioritized context within token limits # --------------------------------------------------------------------------- # ContextFragment — Domain Model # --------------------------------------------------------------------------- @acms_fragment Scenario: Create a context fragment with defaults Given a context fragment with uko_node "project://app/main.py" and content "hello world" and token_count 10 Then the fragment uko_node should be "project://app/main.py" And the fragment content should be "hello world" And the fragment relevance score should be 0.5 And the fragment token count should be 10 And the fragment detail depth should be 0 And the fragment tier should be "warm" And the fragment metadata should be empty And the fragment id should be set And the fragment created_at should be a datetime And the fragment provenance resource_uri should be set @acms_fragment Scenario: Create a fragment with all fields specified Given a context fragment with all fields: | field | value | | uko_node | project://app/io | | content | Use async IO pattern | | score | 0.95 | | tokens | 150 | | detail_depth | 5 | | tier | hot | | meta_key | priority | | meta_val | high | | resource_uri | project://app/io | Then the fragment uko_node should be "project://app/io" And the fragment content should be "Use async IO pattern" And the fragment relevance score should be 0.95 And the fragment token count should be 150 And the fragment detail depth should be 5 And the fragment tier should be "hot" And the fragment metadata key "priority" should be "high" @acms_fragment @validation Scenario: Invalid relevance score rejected When I create a fragment with relevance score 1.5 Then an ACMS validation error should be raised @acms_fragment @validation Scenario: Invalid token count rejected When I create a fragment with token count -1 Then an ACMS validation error should be raised @acms_fragment Scenario: Invalid tier rejected Given the ACMS pipeline modules are available When I create a fragment with invalid tier "lukewarm" Then an ACMS validation error should be raised @acms_fragment @validation Scenario: Missing uko_node rejected When I create a fragment with empty uko_node Then an ACMS validation error should be raised @acms_fragment @validation Scenario: Detail depth out of range rejected When I create a fragment with detail depth 10 Then an ACMS validation error should be raised # --------------------------------------------------------------------------- # Structural field assertions — verify payload fields exist # --------------------------------------------------------------------------- @acms_fragment @structural Scenario: ContextFragment has all spec-required fields Given a context fragment with uko_node "project://app/main.py" and content "x" and token_count 1 Then the fragment should have field "fragment_id" And the fragment should have field "uko_node" And the fragment should have field "content" And the fragment should have field "detail_depth" And the fragment should have field "token_count" And the fragment should have field "relevance_score" And the fragment should have field "provenance" And the fragment should have field "tier" And the fragment should have field "metadata" And the fragment should have field "created_at" @acms_payload @structural Scenario: ContextPayload has all spec-required fields Given the following context fragments: | uko_node | content | score | tokens | | project://app/main.py | alpha | 0.9 | 100 | And a context budget with max_tokens 4096 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should have field "payload_id" And the payload should have field "plan_id" And the payload should have field "fragments" And the payload should have field "total_tokens" And the payload should have field "budget" And the payload should have field "budget_used" And the payload should have field "strategies_used" And the payload should have field "context_hash" And the payload should have field "preamble" And the payload should have field "provenance_map" And the payload should have field "assembled_at" # --------------------------------------------------------------------------- # ContextBudget — Domain Model # --------------------------------------------------------------------------- @acms_budget Scenario: Context budget calculates available tokens Given a context budget with max_tokens 4096 and reserved_tokens 512 Then the available tokens should be 3584 @acms_budget Scenario: Context budget with zero reserved tokens Given a context budget with max_tokens 2048 and reserved_tokens 0 Then the available tokens should be 2048 @acms_budget Scenario: Budget rejects reserved_tokens >= max_tokens Given the ACMS pipeline modules are available When I create a budget with reserved_tokens equal to max_tokens Then an ACMS validation error should be raised @acms_budget @validation Scenario: Budget rejects reserved_tokens greater than max_tokens When I create a budget with reserved_tokens greater than max_tokens Then an ACMS validation error should be raised # --------------------------------------------------------------------------- # ACMSPipeline — Assembly # --------------------------------------------------------------------------- @acms_assemble Scenario: Assemble with relevance strategy selects highest-relevance fragments Given the following context fragments: | uko_node | content | score | tokens | | project://app/file.py | alpha | 0.9 | 100 | | project://app/decision/1 | beta | 0.3 | 100 | | project://app/resource/1 | gamma | 0.7 | 100 | And a context budget with max_tokens 250 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should contain 2 fragments And the first fragment content should be "alpha" And the second fragment content should be "gamma" @acms_assemble Scenario: Assemble with recency strategy selects most recent fragments Given context fragments with different timestamps: | uko_node | content | tokens | created_at | | project://app/old.py | old | 100 | 2024-01-01T00:00:00+00:00 | | project://app/mid.py | mid | 100 | 2024-06-01T00:00:00+00:00 | | project://app/new.py | new | 100 | 2025-01-01T00:00:00+00:00 | And a context budget with max_tokens 200 and reserved_tokens 0 When I assemble with strategy "recency" Then the payload should contain 2 fragments And the first fragment content should be "new" And the second fragment content should be "mid" @acms_assemble Scenario: Assemble with tiered strategy prioritizes hot tier Given the following tiered context fragments: | uko_node | content | score | tokens | tier | | project://app/cold.py | cold-item | 0.9 | 100 | cold | | project://app/hot.py | hot-item | 0.85 | 100 | hot | | project://app/warm.py | warm-item | 0.8 | 100 | warm | And a context budget with max_tokens 200 and reserved_tokens 0 When I assemble with strategy "tiered" Then the payload should contain 2 fragments And the first fragment content should be "hot-item" And the second fragment content should be "warm-item" @acms_assemble Scenario: Assembly respects token budget Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 500 | | project://app/b.py | b | 0.8 | 500 | | project://app/c.py | c | 0.7 | 500 | And a context budget with max_tokens 1000 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should contain 2 fragments And the payload total tokens should be 1000 And the payload should be within budget @acms_assemble Scenario: All fragments exceed budget individually Given the following context fragments: | uko_node | content | score | tokens | | project://app/huge.py | huge | 0.9 | 5000 | And a context budget with max_tokens 100 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should contain 0 fragments And the payload total tokens should be 0 @acms_assemble Scenario: Assembly with empty fragments returns empty payload Given no context fragments And a context budget with max_tokens 4096 and reserved_tokens 512 When I assemble with strategy "relevance" Then the payload should contain 0 fragments And the payload total tokens should be 0 @acms_assemble Scenario: Assembly with single fragment within budget includes it Given the following context fragments: | uko_node | content | score | tokens | | project://app/only.py | only | 0.5 | 100 | And a context budget with max_tokens 4096 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should contain 1 fragments And the first fragment content should be "only" @acms_assemble Scenario: Register and use custom context strategy Given a custom context strategy that reverses fragment order And the following context fragments: | uko_node | content | score | tokens | | project://app/first.py | first | 0.5 | 50 | | project://app/second.py | second | 0.9 | 50 | And a context budget with max_tokens 200 and reserved_tokens 0 When I assemble with strategy "reverse" Then the payload should contain 2 fragments And the first fragment content should be "second" @acms_assemble Scenario: Assemble with default strategy when none specified Given the following context fragments: | uko_node | content | score | tokens | | project://app/alpha.py | alpha | 0.9 | 100 | | project://app/beta.py | beta | 0.3 | 100 | And a context budget with max_tokens 250 and reserved_tokens 0 When I assemble without specifying a strategy Then the payload strategies used should include "relevance" And the payload should contain 2 fragments @acms_assemble @validation Scenario: Non-existent strategy name raises error Given the following context fragments: | uko_node | content | score | tokens | | project://app/alpha.py | alpha | 0.9 | 100 | And a context budget with max_tokens 4096 and reserved_tokens 0 When I assemble with strategy "nonexistent" Then an ACMS strategy error should be raised # --------------------------------------------------------------------------- # Strategy overwrite test # --------------------------------------------------------------------------- @acms_assemble Scenario: Registering strategy with existing name overwrites it Given a custom context strategy that reverses fragment order And the following context fragments: | uko_node | content | score | tokens | | project://app/first.py | first | 0.5 | 50 | | project://app/second.py | second | 0.9 | 50 | And a context budget with max_tokens 200 and reserved_tokens 0 When I register the reverse strategy as "relevance" And I assemble with strategy "relevance" Then the first fragment content should be "second" # --------------------------------------------------------------------------- # ContextPayload — Properties # --------------------------------------------------------------------------- @acms_payload Scenario: Payload is_within_budget property is correct Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 200 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload should be within budget @acms_payload Scenario: Payload remaining_tokens property is correct Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload remaining tokens should be 400 @acms_payload Scenario: Payload budget_used is correct Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 250 | And a context budget with max_tokens 1000 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload budget used should be 0.25 @acms_payload Scenario: Payload context_hash is non-empty Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload context hash should be non-empty @acms_payload Scenario: Payload provenance_map maps fragment IDs Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload provenance map should map each fragment ID @acms_payload Scenario: Payload strategies_used lists the strategy name Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "tiered" Then the payload strategies used should include "tiered" # --------------------------------------------------------------------------- # Payload structural assertions # --------------------------------------------------------------------------- @acms_payload @structural Scenario: Payload plan_id, payload_id, and assembled_at are populated Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | a | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload plan_id should be "01JQTESTPN00000000000000AA" And the payload payload_id should be a non-empty ULID And the payload assembled_at should be a recent UTC datetime # --------------------------------------------------------------------------- # Budget validation edge case # --------------------------------------------------------------------------- @acms_budget @validation Scenario: Budget rejects reserved_tokens strictly greater than max_tokens When I create a budget with reserved_tokens 200 and max_tokens 100 Then an ACMS validation error should be raised # --------------------------------------------------------------------------- # Pipeline component DI injection (T1) # --------------------------------------------------------------------------- @acms_assemble @di Scenario: Custom strategy selector is invoked during assembly Given a pipeline with a custom tracking strategy selector And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the custom strategy selector should have been called # v1: strategies_used is hardcoded from the caller's input, not the # selector output. This assertion validates the v1 pass-through path; # multi-strategy fusion in a future milestone will make it meaningful. And the payload strategies used should include "relevance" @acms_assemble @di Scenario: Custom budget allocator is invoked during assembly Given a pipeline with a custom budget allocator that halves the budget And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the custom budget allocator should have been called @acms_assemble @di Scenario: Custom strategy executor is invoked during assembly Given a pipeline with a custom strategy executor that reverses fragments And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.5 | 50 | | project://app/b.py | beta | 0.9 | 50 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the custom strategy executor should have been called And the first fragment content should be "beta" @acms_assemble @di Scenario: Custom deduplicator is invoked during assembly Given a pipeline with a custom deduplicator that removes duplicates by uko_node And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | | project://app/a.py | alpha2 | 0.5 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the custom deduplicator should have been called And the payload should contain 1 fragments @acms_assemble @di Scenario: Custom preamble generator is invoked during assembly Given a pipeline with a custom preamble generator And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload preamble should be "Custom preamble: 1 fragments" # --------------------------------------------------------------------------- # SkeletonCompressor (T2) # --------------------------------------------------------------------------- @acms_assemble @skeleton Scenario: DefaultSkeletonCompressor returns fragments unchanged Given the ACMS pipeline modules are available When I compress fragments with the default skeleton compressor and budget 100 Then the compressed fragments should equal the original fragments @acms_assemble @skeleton Scenario: Custom skeleton compressor can truncate fragments Given the ACMS pipeline modules are available When I compress fragments with a truncating skeleton compressor and budget 50 Then the compressed fragments should have reduced content # --------------------------------------------------------------------------- # Multi-candidate allocation (T3) # --------------------------------------------------------------------------- @acms_assemble @allocation Scenario: Budget allocator distributes proportionally across multiple candidates Given the ACMS pipeline modules are available When I allocate budget 1000 across candidates with confidences 0.8 and 0.2 Then the first candidate should receive approximately 800 tokens And the second candidate should receive approximately 200 tokens And the total allocated should not exceed 1000 @acms_assemble @allocation Scenario: Strategy selector returns all strategies when no hint given Given the ACMS pipeline modules are available When I select strategies with no strategy hint from 3 registered strategies Then all 3 strategies should be returned with confidence scores # --------------------------------------------------------------------------- # plan_id ULID validation (SEC-1, SPEC-1, TEST-GAP-1) # --------------------------------------------------------------------------- @acms_payload @validation Scenario: Payload rejects empty plan_id Given the ACMS pipeline modules are available When I create a payload with an empty plan_id Then an ACMS validation error should be raised @acms_payload @validation Scenario: Payload accepts valid ULID plan_id Given the ACMS pipeline modules are available When I create a payload with plan_id "01JQTESTPN00000000000000AA" Then the created payload plan_id should be "01JQTESTPN00000000000000AA" @acms_payload @validation Scenario: Payload rejects 25-character plan_id (too short for ULID) Given the ACMS pipeline modules are available When I create a payload with plan_id "01JQTESTPN0000000000000AA" Then an ACMS validation error should be raised @acms_payload @validation Scenario: Payload rejects 27-character plan_id (too long for ULID) Given the ACMS pipeline modules are available When I create a payload with plan_id "01JQTESTPN000000000000000AA" Then an ACMS validation error should be raised @acms_payload @validation Scenario: Payload rejects lowercase plan_id Given the ACMS pipeline modules are available When I create a payload with plan_id "01jqtestpn00000000000000aa" Then an ACMS validation error should be raised @acms_payload @validation Scenario Outline: Payload rejects plan_id with excluded Crockford char Given the ACMS pipeline modules are available When I create a payload with plan_id "" Then an ACMS validation error should be raised Examples: | char | plan_id | | I | 01JQTESTPN000000000000I0AA | | L | 01JQTESTPN000000000000L0AA | | O | 01JQTESTPN000000000000O0AA | | U | 01JQTESTPN000000000000U0AA | @acms_payload @validation @security Scenario: Payload rejects path traversal in plan_id Given the ACMS pipeline modules are available When I create a payload with plan_id "01JQTESTPN0000000/../00AAA" Then an ACMS validation error should be raised @acms_payload @validation @security Scenario: Payload rejects plan_id with slashes Given the ACMS pipeline modules are available When I create a payload with plan_id "01JQTESTPN00000/0000000AAA" Then an ACMS validation error should be raised @acms_payload @validation Scenario: Payload rejects plan_id with whitespace Given the ACMS pipeline modules are available When I create a payload with plan_id " " Then an ACMS validation error should be raised # --------------------------------------------------------------------------- # Strategy introspection (coverage: capabilities, can_handle, explain) # --------------------------------------------------------------------------- @acms_strategy @introspection Scenario: Each built-in strategy declares distinct capabilities Given the ACMS pipeline modules are available When I inspect the capabilities of "relevance" Then the strategy should declare supports_semantic_search as true When I inspect the capabilities of "recency" Then the strategy should declare supports_temporal_archaeology as true When I inspect the capabilities of "tiered" Then the strategy should declare supports_semantic_search as false @acms_strategy @introspection Scenario: Strategy confidence ranking determines selector priority Given the ACMS pipeline modules are available When I query can_handle on all three built-in strategies Then "relevance" should have the highest confidence And "tiered" should have higher confidence than "recency" @acms_strategy @introspection Scenario: Strategy explain returns strategy-specific descriptions Given the ACMS pipeline modules are available When I call explain on each built-in strategy Then the "relevance" explanation should mention "relevance score" And the "recency" explanation should mention "creation time" And the "tiered" explanation should mention "tier priority" # --------------------------------------------------------------------------- # Fragment metadata overflow # --------------------------------------------------------------------------- @acms_fragment @validation Scenario: Fragment rejects metadata with more than 64 entries Given the ACMS pipeline modules are available When I create a fragment with 65 metadata entries Then an ACMS validation error should be raised @acms_fragment @validation Scenario: Fragment accepts metadata with exactly 64 entries Given the ACMS pipeline modules are available When I create a fragment with 64 metadata entries Then the fragment should have 64 metadata entries # --------------------------------------------------------------------------- # Early plan_id rejection in assemble() # --------------------------------------------------------------------------- @acms_assemble @validation Scenario: Pipeline rejects invalid plan_id before executing the pipeline Given the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with plan_id "not-a-valid-ulid" Then an ACMS ValueError should be raised mentioning "ULID" # --------------------------------------------------------------------------- # Provenance map immutability (TEST-2) # --------------------------------------------------------------------------- @acms_payload @immutability Scenario: Provenance map is immune to external mutation after payload creation Given the ACMS pipeline modules are available When I create a payload with a mutable provenance map and mutate the source Then the payload provenance map should be unchanged # --------------------------------------------------------------------------- # Allocator rounding edge cases (TEST-3) # --------------------------------------------------------------------------- @acms_assemble @allocation Scenario: Budget allocator uses full budget with odd split across 3 candidates Given the ACMS pipeline modules are available When I allocate budget 999 across 3 candidates with equal confidence Then the total allocated should equal exactly 999 And each allocation should be 333 or 334 @acms_assemble @allocation Scenario: Budget allocator handles zero-confidence equal split without token loss Given the ACMS pipeline modules are available When I allocate budget 100 across 3 candidates with zero confidence Then the total allocated should equal exactly 100 @acms_assemble @allocation Scenario: Budget allocator distributes full budget with unequal confidences Given the ACMS pipeline modules are available When I allocate budget 10 across 3 candidates with confidences 0.5 0.3 0.2 Then the total allocated should equal exactly 10 # --------------------------------------------------------------------------- # Strategy selector fallback (TEST-4) # --------------------------------------------------------------------------- @acms_assemble @di Scenario: Assembly falls back when custom selector omits the requested strategy Given a pipeline with a custom strategy selector that returns no candidates And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | | project://app/b.py | beta | 0.5 | 100 | And a context budget with max_tokens 500 and reserved_tokens 0 When I assemble with strategy "relevance" Then the empty strategy selector should have been called And the payload strategies used should include "relevance" And the payload should contain 2 fragments # --------------------------------------------------------------------------- # Issue #3563 — SkeletonCompressor invocation in assemble() (TDD capture) # --------------------------------------------------------------------------- @acms_assemble @skeleton @tdd_issue @tdd_issue_3563 @issue_3563 Scenario: TDD capture — assemble() invokes skeleton compressor when parent_fragments provided Given the ACMS pipeline modules are available And a spy skeleton compressor is registered And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 1000 and reserved_tokens 0 And parent fragments for skeleton compression: | uko_node | content | score | tokens | | project://parent/x.py | parent code | 0.8 | 200 | When I assemble with parent fragments and skeleton_ratio 0.15 Then the spy skeleton compressor should have been called And the spy compressor should have received the parent fragments And the spy compressor skeleton_budget should be 150 @acms_assemble @skeleton @issue_3563 Scenario: assemble() includes skeleton_fragments in the returned ContextPayload Given the ACMS pipeline modules are available And a spy skeleton compressor is registered And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 1000 and reserved_tokens 0 And parent fragments for skeleton compression: | uko_node | content | score | tokens | | project://parent/x.py | parent code | 0.8 | 200 | When I assemble with parent fragments and skeleton_ratio 0.15 Then the payload skeleton_fragments should be non-empty And the payload skeleton_fragments should equal the spy compressor output @acms_assemble @skeleton @issue_3563 Scenario: assemble() skeleton_fragments is empty when no parent_fragments provided Given the ACMS pipeline modules are available And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 1000 and reserved_tokens 0 When I assemble with strategy "relevance" Then the payload skeleton_fragments should be empty @acms_assemble @skeleton @issue_3563 Scenario: skeleton_ratio governs the skeleton budget passed to the compressor Given the ACMS pipeline modules are available And a spy skeleton compressor is registered And the following context fragments: | uko_node | content | score | tokens | | project://app/a.py | alpha | 0.9 | 100 | And a context budget with max_tokens 2000 and reserved_tokens 0 And parent fragments for skeleton compression: | uko_node | content | score | tokens | | project://parent/x.py | parent code | 0.8 | 200 | When I assemble with parent fragments and skeleton_ratio 0.25 Then the spy compressor skeleton_budget should be 500 And the first fragment content should be "alpha"