phronomy 0.16.0 → 0.18.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -1,191 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "cgi"
4
-
5
- module Phronomy
6
- module LlmContextWindow
7
- # Assembler collects all four context regions and produces the final
8
- # {system:, messages:, tool_classes:} hash consumed by Agent::Base.
9
- #
10
- # Regions:
11
- # 1. Instruction — system prompt text set via #add_instruction
12
- # 2. Capability — tool classes registered via #add_capability
13
- # 3. Knowledge — external facts injected via #add_knowledge (generates XML tags)
14
- # 4. Conversation — historical messages added via #add_messages
15
- #
16
- # Token budgeting:
17
- # When a budget is given, Assembler validates that the supplied conversation
18
- # messages fit after instruction, knowledge, and capability costs are
19
- # accounted for. It does not prune messages; Manifest-first Context Policy
20
- # is responsible for context selection.
21
- # Knowledge chunks are always included in full (they are assumed to be
22
- # pre-screened by the caller). When no budget is given all messages are
23
- # passed through unchanged.
24
- #
25
- # @example
26
- # assembler = Phronomy::LlmContextWindow::Assembler.new(budget: budget)
27
- # assembler.add_instruction("You are a helpful assistant.")
28
- # assembler.add_knowledge("The user lives in Tokyo.", type: :entity, trusted: false)
29
- # assembler.add_messages(manager.load(thread_id: "t1", query: user_input))
30
- # context = assembler.build
31
- # # => { system: "You are ...\n<context ...>...</context>", messages: [...] }
32
- class Assembler
33
- # Builds a single XML context tag string.
34
- # Exposed as a class method so callers (e.g. Agent::Base) can build
35
- # static knowledge XML tags independently of an Assembler instance.
36
- #
37
- # @param text [String]
38
- # @param type [Symbol, String]
39
- # @param trusted [Boolean]
40
- # @return [String]
41
- # @api private
42
- # mutant:disable - text.to_str and plain text (no to_s) are genuine equivalents when text is a String; type.to_str is genuine equivalent when type is a String
43
- def self.xml_tag(text, type:, trusted: false)
44
- "<context type=\"#{CGI.escapeHTML(type.to_s)}\" trusted=\"#{trusted}\">\n#{CGI.escapeHTML(text.to_s)}\n</context>"
45
- end
46
-
47
- # @param budget [Phronomy::LlmContextWindow::TokenBudget, nil]
48
- # when nil no budget validation is performed
49
- # @api private
50
- # mutant:disable - @instruction = nil deletion is a genuine equivalent (uninitialized Ruby instance variables return nil)
51
- def initialize(budget: nil)
52
- @budget = budget
53
- @instruction = nil
54
- @tool_classes = []
55
- @knowledge_chunks = []
56
- @messages = []
57
- end
58
-
59
- # Register tool classes (Region 2).
60
- # Estimates their token cost and deducts it from the budget so that
61
- # budget estimation accounts for tool definition overhead.
62
- #
63
- # @param tool_classes [Array<Class, Object>] tool classes or instances
64
- # @return [self]
65
- # @api private
66
- def add_capability(tool_classes)
67
- @tool_classes = Array(tool_classes)
68
- self
69
- end
70
-
71
- # Set the system instruction text (Region 1).
72
- # Calling this multiple times replaces the previous value.
73
- #
74
- # @param text [String]
75
- # @return [self]
76
- # @api private
77
- # mutant:disable - text.to_str and plain text (no .to_s) are genuine equivalents when callers always pass a String
78
- def add_instruction(text)
79
- @instruction = text.to_s
80
- self
81
- end
82
-
83
- # Append a knowledge chunk (Region 3).
84
- # The chunk is wrapped in an XML context tag automatically.
85
- #
86
- # @param text [String]
87
- # @param type [Symbol, String] semantic label for the context tag (e.g. :entity, :rag, :static)
88
- # @param trusted [Boolean] false (default) indicates externally sourced data
89
- # @param source [String, nil] optional source label (e.g. filename); included in the
90
- # XML tag so the LLM can produce grounded citations. Omitted when nil.
91
- # @return [self]
92
- # @api private
93
- # mutant:disable - {text:} (shorthand, no .to_s) and text.to_str are genuine equivalents when text is a String; {type:} shorthand is genuine equivalent because xml_context_tag always calls .to_s on chunk[:type]
94
- def add_knowledge(text, type:, trusted: false, source: nil)
95
- @knowledge_chunks << {text: text.to_s, type: type.to_s, trusted: trusted, source: source}
96
- self
97
- end
98
-
99
- # Set conversation messages (Region 4). Replaces any previously set messages.
100
- #
101
- # @param messages [Array] message-like objects with #role and #content
102
- # @return [self]
103
- # @api private
104
- # mutant:disable - @messages = messages (no Array()) is a genuine equivalent when callers always pass an Array
105
- def add_messages(messages)
106
- @messages = Array(messages)
107
- self
108
- end
109
-
110
- # Returns the number of tokens available for conversation messages after
111
- # accounting for instruction, knowledge, and capability overhead.
112
- # Returns +nil+ when no budget is configured.
113
- #
114
- # @return [Integer, nil]
115
- # @api private
116
- def available_for_messages
117
- return nil unless @budget
118
- knowledge_text = @knowledge_chunks.map { |c| xml_context_tag(c) }.join("\n\n")
119
- system_parts = [@instruction, knowledge_text.empty? ? nil : knowledge_text].compact
120
- system_text = system_parts.join("\n\n")
121
- used = TokenEstimator.estimate(system_text) + estimate_capability_tokens
122
- @budget.available(used: used)
123
- end
124
-
125
- # Assemble the context.
126
- #
127
- # @return [Hash{Symbol => Object}]
128
- # :system [String, nil] combined system prompt (instruction + knowledge XML tags)
129
- # :messages [Array] conversation messages, trimmed to budget if set
130
- # :tool_classes [Array] tool classes/instances to register with the chat
131
- # @api private
132
- # Raises {Phronomy::ContextLengthError} when a budget is set and the
133
- # conversation messages do not fit within the remaining token allowance.
134
- # No automatic trimming is performed — callers must pre-process messages
135
- # before passing them to the Assembler.
136
- #
137
- # mutant:disable - multiple genuine equivalent mutations: map{}.join("\n\n") → map{} is genuine; `unless knowledge_text.empty?` vs ternary is genuine; `{ system: unless system_text.empty? }` vs ternary is genuine; `messages:` shorthand vs `messages: messages` is genuine
138
- def build
139
- knowledge_text = @knowledge_chunks.map { |c| xml_context_tag(c) }.join("\n\n")
140
- system_parts = [@instruction, knowledge_text.empty? ? nil : knowledge_text].compact
141
- system_text = system_parts.join("\n\n")
142
-
143
- if @budget && @messages.any?
144
- capability_tokens = estimate_capability_tokens
145
- used = TokenEstimator.estimate(system_text) + capability_tokens
146
- remaining = @budget.available(used: used)
147
- msg_tokens = @messages.sum { |m| TokenEstimator.estimate(m.content.to_s) }
148
- if msg_tokens > remaining
149
- raise Phronomy::ContextLengthError,
150
- "Context exceeds token budget: messages require #{msg_tokens} tokens but " \
151
- "only #{remaining} available (context_window=#{@budget.context_window}, " \
152
- "used_by_system=#{used}). Use the Context Policy path to manage message budget."
153
- end
154
- end
155
-
156
- {
157
- system: system_text.empty? ? nil : system_text,
158
- messages: @messages,
159
- tool_classes: @tool_classes
160
- }
161
- end
162
-
163
- private
164
-
165
- # Estimates the token cost of all registered tool classes.
166
- # Uses each tool's description and parameter names as a proxy for its
167
- # JSON Schema size. This is a deliberate simplification — exact token
168
- # counts require provider-specific schema serialization which lives in
169
- # RubyLLM. The estimate errs on the side of being slightly conservative
170
- # so that the conversation budget is not over-allocated.
171
- def estimate_capability_tokens
172
- @tool_classes.sum do |tc|
173
- # Instantiated tool objects (e.g. Phronomy::Tools::Mcp instances) may not be a Class.
174
- next 0 unless tc.is_a?(Class) && tc.respond_to?(:description)
175
-
176
- text = [tc.description.to_s]
177
- if tc.respond_to?(:parameters)
178
- tc.parameters.each_key { |k| text << k.to_s }
179
- end
180
- TokenEstimator.estimate(text.join(" "))
181
- end
182
- end
183
-
184
- # mutant:disable - multiple genuine equivalent mutations: chunk.fetch(key) vs chunk[key] (key always present); chunk[:text] no .to_s / .to_str are genuine (stored as String); chunk[:type] no .to_s / .to_str are genuine (stored as String); chunk[:source] no .to_s / .to_str are genuine (truthy branch, always String); src_attr chunk.fetch(:source) is genuine (source key always present)
185
- def xml_context_tag(chunk)
186
- src_attr = chunk[:source] ? " source=\"#{CGI.escapeHTML(chunk[:source].to_s)}\"" : ""
187
- "<context type=\"#{CGI.escapeHTML(chunk[:type].to_s)}\"#{src_attr} trusted=\"#{chunk[:trusted]}\">\n#{CGI.escapeHTML(chunk[:text].to_s)}\n</context>"
188
- end
189
- end
190
- end
191
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module LlmContextWindow
5
- # Caches the assembled static system prompt text keyed by a SHA-256
6
- # fingerprint of the agent's instructions + static knowledge content.
7
- # Each instance is owned by one thread (stored in +Thread.current+).
8
- class ContextVersionCache
9
- # @return [String, nil] last stored fingerprint
10
- attr_reader :fingerprint
11
-
12
- # @return [String, nil] cached system prompt text
13
- attr_reader :system_text
14
-
15
- # @return [Integer] estimated token count of #system_text
16
- attr_reader :system_tokens
17
-
18
- def initialize
19
- @fingerprint = nil
20
- @system_text = nil
21
- @system_tokens = 0
22
- end
23
-
24
- # Returns true when the given fingerprint matches the stored one.
25
- #
26
- # @param fingerprint [String] SHA-256 hex digest to compare
27
- # @return [Boolean]
28
- # @api private
29
- def valid?(fingerprint)
30
- !@fingerprint.nil? && !@system_text.nil? && @fingerprint == fingerprint
31
- end
32
-
33
- # Update the cache with a new fingerprint and system text.
34
- #
35
- # @param fingerprint [String] new SHA-256 hex digest
36
- # @param system_text [String] fully assembled system prompt text
37
- # @api private
38
- def update(fingerprint:, system_text:)
39
- @fingerprint = fingerprint
40
- @system_text = system_text.to_s
41
- @system_tokens = TokenEstimator.estimate(@system_text)
42
- end
43
-
44
- # Clear all cached values (used for testing and forced invalidation).
45
- def reset
46
- @fingerprint = nil
47
- @system_text = nil
48
- @system_tokens = 0
49
- end
50
- end
51
- end
52
- end
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Testing
5
- # A deterministic event dispatcher for use in tests.
6
- #
7
- # Wraps a {Thread::Queue} and dispatches events one at a time via {#tick}
8
- # or drains all pending events via {#tick_until_idle}. Tests can inspect
9
- # queue depth and verify event ordering without wall-clock sleeps.
10
- #
11
- # @example
12
- # scheduler = Phronomy::Testing::FakeScheduler.new
13
- # scheduler.post(:a)
14
- # scheduler.post(:b)
15
- # scheduler.queue_depth # => 2
16
- # scheduler.tick # dispatches :a
17
- # scheduler.queue_depth # => 1
18
- # scheduler.tick_until_idle
19
- # scheduler.dispatched # => [:a, :b]
20
- class FakeScheduler
21
- # @return [Array] all events dispatched so far (in order)
22
- attr_reader :dispatched
23
-
24
- def initialize
25
- @queue = Thread::Queue.new
26
- @dispatched = []
27
- @handlers = {}
28
- end
29
-
30
- # Enqueue an event for later dispatch.
31
- #
32
- # @param event [Object]
33
- # @return [self]
34
- # @api private
35
- def post(event)
36
- @queue.push(event)
37
- self
38
- end
39
-
40
- # Dispatch the next queued event.
41
- # Calls the registered handler (if any) and records the event.
42
- # Returns the dispatched event, or +nil+ if the queue is empty.
43
- #
44
- # @return [Object, nil]
45
- # @api private
46
- def tick
47
- return nil if @queue.empty?
48
-
49
- event = begin
50
- @queue.pop(true)
51
- rescue
52
- nil
53
- end
54
- return nil unless event
55
-
56
- @dispatched << event
57
- handler = @handlers[event.class] || @handlers[:any]
58
- handler&.call(event)
59
- event
60
- end
61
-
62
- # Dispatch events until the queue is empty.
63
- # Bounded by +max_ticks+ to prevent infinite loops.
64
- #
65
- # @param max_ticks [Integer]
66
- # @return [Integer] number of events dispatched
67
- # @api private
68
- def tick_until_idle(max_ticks: 1000)
69
- count = 0
70
- while !@queue.empty? && count < max_ticks
71
- tick
72
- count += 1
73
- end
74
- count
75
- end
76
-
77
- # Returns the number of events waiting to be dispatched.
78
- # @return [Integer]
79
- # @api private
80
- def queue_depth
81
- @queue.size
82
- end
83
-
84
- # Register a handler block for events of the given class.
85
- # Use +:any+ to handle all event types.
86
- #
87
- # @param klass [Class, :any]
88
- # @yield [event]
89
- # @return [self]
90
- # @api private
91
- def on(klass, &block)
92
- @handlers[klass] = block
93
- self
94
- end
95
-
96
- # Returns true when the queue is empty.
97
- # @return [Boolean]
98
- # @api private
99
- def idle?
100
- @queue.empty?
101
- end
102
- end
103
- end
104
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Testing
5
- # RSpec helper module that provides a deterministic {Runtime} backed by
6
- # {Phronomy::Runtime::FakeScheduler}.
7
- #
8
- # Include this module in your RSpec describe/context blocks and call
9
- # {#with_fake_scheduler} to run a block of code inside a fully
10
- # synchronous, event-logged runtime.
11
- #
12
- # @example Basic usage (no clock)
13
- # include Phronomy::Testing::SchedulerHelpers
14
- #
15
- # it "records completed events" do
16
- # with_fake_scheduler do |sched|
17
- # Phronomy::Runtime.instance.spawn(name: "my-task") { 42 }
18
- # expect(sched.event_log.map { |e| e[:type] }).to include(:completed)
19
- # end
20
- # end
21
- #
22
- # @example With a FakeClock
23
- # include Phronomy::Testing::SchedulerHelpers
24
- #
25
- # it "surfaces pending timers" do
26
- # clock = Phronomy::Testing::FakeClock.new
27
- # with_fake_scheduler(clock: clock) do |sched|
28
- # clock.schedule(seconds: 5) { :fired }
29
- # expect(sched.pending_timers.first[:fire_at]).to eq(5.0)
30
- # end
31
- # end
32
- module SchedulerHelpers
33
- # Run +block+ with a {Phronomy::Runtime} that uses
34
- # {Phronomy::Runtime::FakeScheduler}.
35
- #
36
- # The global runtime is replaced for the duration of the block and
37
- # restored afterwards, whether the block raises or not.
38
- #
39
- # @param clock [Phronomy::Testing::FakeClock, nil]
40
- # Optional fake clock to inject into the scheduler for timer support
41
- # and event timestamping.
42
- # @yield [scheduler, clock] the {Runtime::FakeScheduler} and the clock
43
- # @return [Object] the return value of the block
44
- # @api private
45
- def with_fake_scheduler(clock: nil)
46
- scheduler = Phronomy::Runtime::FakeScheduler.new
47
- scheduler.clock = clock if clock
48
- runtime = Phronomy::Runtime.new(scheduler: scheduler)
49
- original = Phronomy::Runtime.default_if_initialized_for_test
50
- Phronomy::Runtime.replace_default_for_test(runtime)
51
- begin
52
- yield scheduler, clock
53
- ensure
54
- result = nil
55
- begin
56
- result = runtime.shutdown
57
- ensure
58
- Phronomy::Runtime.restore_default_for_test(original)
59
- end
60
- unless result&.cleanup_complete?
61
- raise Phronomy::RuntimeShutdownError,
62
- "Temporary test Runtime did not shut down completely"
63
- end
64
- end
65
- end
66
- end
67
- end
68
- end