devcycle-ruby-server-sdk 3.6.1 → 3.7.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.
- checksums.yaml +4 -4
- data/lib/devcycle-ruby-server-sdk/api/client.rb +102 -26
- data/lib/devcycle-ruby-server-sdk/api/dev_cycle_provider.rb +32 -5
- data/lib/devcycle-ruby-server-sdk/eval_hooks_runner.rb +135 -0
- data/lib/devcycle-ruby-server-sdk/eval_reasons.rb +13 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm +0 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams.proto +8 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams_pb.rb +25 -59
- data/lib/devcycle-ruby-server-sdk/localbucketing/update_wasm.sh +2 -2
- data/lib/devcycle-ruby-server-sdk/models/eval_hook.rb +28 -0
- data/lib/devcycle-ruby-server-sdk/models/eval_hook_context.rb +22 -0
- data/lib/devcycle-ruby-server-sdk/models/variable.rb +9 -1
- data/lib/devcycle-ruby-server-sdk/version.rb +1 -1
- data/lib/devcycle-ruby-server-sdk.rb +6 -0
- data/spec/api/devcycle_api_spec.rb +11 -9
- data/spec/devcycle_provider_spec.rb +157 -7
- data/spec/eval_hooks_runner_spec.rb +410 -0
- data/spec/eval_hooks_spec.rb +245 -0
- metadata +10 -2
@@ -4,18 +4,109 @@ require 'spec_helper'
|
|
4
4
|
require 'open_feature/sdk'
|
5
5
|
|
6
6
|
context 'user_from_openfeature_context' do
|
7
|
-
context 'user_id' do
|
7
|
+
context 'user_id validation' do
|
8
|
+
it 'raises error when no user ID fields are provided' do
|
9
|
+
context = OpenFeature::SDK::EvaluationContext.new(email: 'test@example.com')
|
10
|
+
expect {
|
11
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
12
|
+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'raises error when targeting_key is not a string' do
|
16
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 123)
|
17
|
+
expect {
|
18
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
19
|
+
}.to raise_error(ArgumentError, "User ID field 'targeting_key' must be a string, got Integer")
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'raises error when user_id is not a string' do
|
23
|
+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 123)
|
24
|
+
expect {
|
25
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
26
|
+
}.to raise_error(ArgumentError, "User ID field 'user_id' must be a string, got Integer")
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises error when userId is not a string' do
|
30
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 123)
|
31
|
+
expect {
|
32
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
33
|
+
}.to raise_error(ArgumentError, "User ID field 'userId' must be a string, got Integer")
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'raises error when targeting_key is nil' do
|
37
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)
|
38
|
+
expect {
|
39
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
40
|
+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
|
41
|
+
end
|
8
42
|
|
9
|
-
it '
|
10
|
-
context = OpenFeature::SDK::EvaluationContext.new(
|
43
|
+
it 'raises error when targeting_key is empty string' do
|
44
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: '')
|
45
|
+
expect {
|
46
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
47
|
+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'raises error when user_id is empty string' do
|
51
|
+
context = OpenFeature::SDK::EvaluationContext.new(user_id: '')
|
52
|
+
expect {
|
53
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
54
|
+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'raises error when userId is empty string' do
|
58
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: '')
|
59
|
+
expect {
|
60
|
+
DevCycle::Provider.user_from_openfeature_context(context)
|
61
|
+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'user_id fields priority' do
|
66
|
+
it 'returns a user with the user_id from the context when only user_id is provided' do
|
67
|
+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value')
|
11
68
|
user = DevCycle::Provider.user_from_openfeature_context(context)
|
12
|
-
expect(user.user_id).to eq('
|
69
|
+
expect(user.user_id).to eq('user_id_value')
|
13
70
|
end
|
14
71
|
|
15
|
-
it 'returns a user with the targeting_key from the context' do
|
16
|
-
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: '
|
72
|
+
it 'returns a user with the targeting_key from the context when only targeting_key is provided' do
|
73
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value')
|
17
74
|
user = DevCycle::Provider.user_from_openfeature_context(context)
|
18
|
-
expect(user.user_id).to eq('
|
75
|
+
expect(user.user_id).to eq('targeting_key_value')
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'returns a user with the userId from the context when only userId is provided' do
|
79
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId_value')
|
80
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
81
|
+
expect(user.user_id).to eq('userId_value')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'prioritizes targeting_key over user_id' do
|
85
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value')
|
86
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
87
|
+
expect(user.user_id).to eq('targeting_key_value')
|
88
|
+
expect(user.customData).to eq({})
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'prioritizes targeting_key over userId' do
|
92
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', userId: 'userId_value')
|
93
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
94
|
+
expect(user.user_id).to eq('targeting_key_value')
|
95
|
+
expect(user.customData).to eq({})
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'prioritizes user_id over userId' do
|
99
|
+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value', userId: 'userId_value')
|
100
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
101
|
+
expect(user.user_id).to eq('user_id_value')
|
102
|
+
expect(user.customData).to eq({})
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'prioritizes targeting_key over both user_id and userId' do
|
106
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value', userId: 'userId_value')
|
107
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
108
|
+
expect(user.user_id).to eq('targeting_key_value')
|
109
|
+
expect(user.customData).to eq({})
|
19
110
|
end
|
20
111
|
end
|
21
112
|
context 'email' do
|
@@ -31,6 +122,19 @@ context 'user_from_openfeature_context' do
|
|
31
122
|
expect(user.user_id).to eq('targeting_key')
|
32
123
|
expect(user.email).to eq('email')
|
33
124
|
end
|
125
|
+
it 'returns a user with a valid userId and email' do
|
126
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', email: 'email')
|
127
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
128
|
+
expect(user.user_id).to eq('userId')
|
129
|
+
expect(user.email).to eq('email')
|
130
|
+
end
|
131
|
+
it 'prioritizes targeting_key over user_id with email' do
|
132
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', email: 'email')
|
133
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
134
|
+
expect(user.user_id).to eq('targeting_key')
|
135
|
+
expect(user.email).to eq('email')
|
136
|
+
expect(user.customData).to eq({})
|
137
|
+
end
|
34
138
|
end
|
35
139
|
|
36
140
|
context 'customData' do
|
@@ -40,6 +144,18 @@ context 'user_from_openfeature_context' do
|
|
40
144
|
expect(user.user_id).to eq('user_id')
|
41
145
|
expect(user.customData).to eq({ 'key' => 'value' })
|
42
146
|
end
|
147
|
+
it 'returns a user with userId and customData' do
|
148
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', customData: { 'key' => 'value' })
|
149
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
150
|
+
expect(user.user_id).to eq('userId')
|
151
|
+
expect(user.customData).to eq({ 'key' => 'value' })
|
152
|
+
end
|
153
|
+
it 'excludes all user ID fields from customData' do
|
154
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', customData: { 'key' => 'value' })
|
155
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
156
|
+
expect(user.user_id).to eq('targeting_key')
|
157
|
+
expect(user.customData).to eq({ 'key' => 'value' })
|
158
|
+
end
|
43
159
|
end
|
44
160
|
|
45
161
|
context 'privateCustomData' do
|
@@ -49,6 +165,12 @@ context 'user_from_openfeature_context' do
|
|
49
165
|
expect(user.user_id).to eq('user_id')
|
50
166
|
expect(user.privateCustomData).to eq({ 'key' => 'value' })
|
51
167
|
end
|
168
|
+
it 'returns a user with userId and privateCustomData' do
|
169
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', privateCustomData: { 'key' => 'value' })
|
170
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
171
|
+
expect(user.user_id).to eq('userId')
|
172
|
+
expect(user.privateCustomData).to eq({ 'key' => 'value' })
|
173
|
+
end
|
52
174
|
end
|
53
175
|
|
54
176
|
context 'appVersion' do
|
@@ -65,6 +187,20 @@ context 'user_from_openfeature_context' do
|
|
65
187
|
expect(user.user_id).to eq('user_id')
|
66
188
|
expect(user.appBuild).to eq(1)
|
67
189
|
end
|
190
|
+
|
191
|
+
it 'returns a user with userId and appVersion' do
|
192
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appVersion: '1.0.0')
|
193
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
194
|
+
expect(user.user_id).to eq('userId')
|
195
|
+
expect(user.appVersion).to eq('1.0.0')
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'returns a user with userId and appBuild' do
|
199
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appBuild: 1)
|
200
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
201
|
+
expect(user.user_id).to eq('userId')
|
202
|
+
expect(user.appBuild).to eq(1)
|
203
|
+
end
|
68
204
|
end
|
69
205
|
context 'randomFields' do
|
70
206
|
it 'returns a user with customData fields mapped to any non-standard fields' do
|
@@ -73,6 +209,20 @@ context 'user_from_openfeature_context' do
|
|
73
209
|
expect(user.user_id).to eq('user_id')
|
74
210
|
expect(user.customData).to eq({ 'randomField' => 'value' })
|
75
211
|
end
|
212
|
+
|
213
|
+
it 'returns a user with userId and customData fields mapped to any non-standard fields' do
|
214
|
+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', randomField: 'value')
|
215
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
216
|
+
expect(user.user_id).to eq('userId')
|
217
|
+
expect(user.customData).to eq({ 'randomField' => 'value' })
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'excludes all user ID fields from custom data with random fields' do
|
221
|
+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', randomField: 'value')
|
222
|
+
user = DevCycle::Provider.user_from_openfeature_context(context)
|
223
|
+
expect(user.user_id).to eq('targeting_key')
|
224
|
+
expect(user.customData).to eq({ 'randomField' => 'value' })
|
225
|
+
end
|
76
226
|
end
|
77
227
|
|
78
228
|
context 'provider' do
|
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'devcycle-ruby-server-sdk/eval_hooks_runner'
|
3
|
+
|
4
|
+
describe DevCycle::EvalHooksRunner do
|
5
|
+
let(:test_context) { DevCycle::HookContext.new(key: 'test-key', user: 'test-user', default_value: 'test-default') }
|
6
|
+
|
7
|
+
describe 'initialization' do
|
8
|
+
it 'initializes with empty hooks array' do
|
9
|
+
runner = DevCycle::EvalHooksRunner.new
|
10
|
+
expect(runner.eval_hooks).to be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'initializes with provided hooks' do
|
14
|
+
hook = DevCycle::EvalHook.new
|
15
|
+
runner = DevCycle::EvalHooksRunner.new([hook])
|
16
|
+
expect(runner.eval_hooks).to include(hook)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#add_hook' do
|
21
|
+
it 'adds a hook to the runner' do
|
22
|
+
runner = DevCycle::EvalHooksRunner.new
|
23
|
+
hook = DevCycle::EvalHook.new
|
24
|
+
|
25
|
+
runner.add_hook(hook)
|
26
|
+
expect(runner.eval_hooks).to include(hook)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can add multiple hooks' do
|
30
|
+
runner = DevCycle::EvalHooksRunner.new
|
31
|
+
hook1 = DevCycle::EvalHook.new
|
32
|
+
hook2 = DevCycle::EvalHook.new
|
33
|
+
|
34
|
+
runner.add_hook(hook1)
|
35
|
+
runner.add_hook(hook2)
|
36
|
+
|
37
|
+
expect(runner.eval_hooks).to include(hook1, hook2)
|
38
|
+
expect(runner.eval_hooks.length).to eq(2)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#clear_hooks' do
|
43
|
+
it 'removes all hooks from the runner' do
|
44
|
+
runner = DevCycle::EvalHooksRunner.new
|
45
|
+
hook = DevCycle::EvalHook.new
|
46
|
+
|
47
|
+
runner.add_hook(hook)
|
48
|
+
expect(runner.eval_hooks).not_to be_empty
|
49
|
+
|
50
|
+
runner.clear_hooks
|
51
|
+
expect(runner.eval_hooks).to be_empty
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#run_before_hooks' do
|
56
|
+
it 'runs before hooks in order' do
|
57
|
+
execution_order = []
|
58
|
+
runner = DevCycle::EvalHooksRunner.new
|
59
|
+
|
60
|
+
hook1 = DevCycle::EvalHook.new(
|
61
|
+
before: ->(context) {
|
62
|
+
execution_order << 'hook1'
|
63
|
+
context
|
64
|
+
}
|
65
|
+
)
|
66
|
+
|
67
|
+
hook2 = DevCycle::EvalHook.new(
|
68
|
+
before: ->(context) {
|
69
|
+
execution_order << 'hook2'
|
70
|
+
context
|
71
|
+
}
|
72
|
+
)
|
73
|
+
|
74
|
+
runner.add_hook(hook1)
|
75
|
+
runner.add_hook(hook2)
|
76
|
+
|
77
|
+
result = runner.run_before_hooks(test_context)
|
78
|
+
|
79
|
+
expect(execution_order).to eq(['hook1', 'hook2'])
|
80
|
+
expect(result).to eq(test_context)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'returns modified context from before hook' do
|
84
|
+
runner = DevCycle::EvalHooksRunner.new
|
85
|
+
modified_context = DevCycle::HookContext.new(key: 'modified', user: 'modified-user', default_value: 'modified-default')
|
86
|
+
|
87
|
+
hook = DevCycle::EvalHook.new(
|
88
|
+
before: ->(context) {
|
89
|
+
modified_context
|
90
|
+
}
|
91
|
+
)
|
92
|
+
|
93
|
+
runner.add_hook(hook)
|
94
|
+
result = runner.run_before_hooks(test_context)
|
95
|
+
|
96
|
+
expect(result).to eq(modified_context)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'handles hooks without before callbacks' do
|
100
|
+
runner = DevCycle::EvalHooksRunner.new
|
101
|
+
hook = DevCycle::EvalHook.new # No before callback
|
102
|
+
|
103
|
+
runner.add_hook(hook)
|
104
|
+
result = runner.run_before_hooks(test_context)
|
105
|
+
|
106
|
+
expect(result).to eq(test_context)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'raises BeforeHookError when a before hook raises an error' do
|
110
|
+
runner = DevCycle::EvalHooksRunner.new
|
111
|
+
hook1_called = false
|
112
|
+
|
113
|
+
hook1 = DevCycle::EvalHook.new(
|
114
|
+
before: ->(context) {
|
115
|
+
hook1_called = true
|
116
|
+
raise StandardError, 'Hook 1 error'
|
117
|
+
}
|
118
|
+
)
|
119
|
+
|
120
|
+
hook2 = DevCycle::EvalHook.new(
|
121
|
+
before: ->(context) {
|
122
|
+
# This should not be called because hook1 raises an error
|
123
|
+
context
|
124
|
+
}
|
125
|
+
)
|
126
|
+
|
127
|
+
runner.add_hook(hook1)
|
128
|
+
runner.add_hook(hook2)
|
129
|
+
|
130
|
+
expect { runner.run_before_hooks(test_context) }.to raise_error(DevCycle::BeforeHookError, /Hook 1 error/)
|
131
|
+
expect(hook1_called).to be true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#run_after_hooks' do
|
136
|
+
it 'runs after hooks in order' do
|
137
|
+
execution_order = []
|
138
|
+
runner = DevCycle::EvalHooksRunner.new
|
139
|
+
|
140
|
+
hook1 = DevCycle::EvalHook.new(
|
141
|
+
after: ->(context) {
|
142
|
+
execution_order << 'hook1'
|
143
|
+
}
|
144
|
+
)
|
145
|
+
|
146
|
+
hook2 = DevCycle::EvalHook.new(
|
147
|
+
after: ->(context) {
|
148
|
+
execution_order << 'hook2'
|
149
|
+
}
|
150
|
+
)
|
151
|
+
|
152
|
+
runner.add_hook(hook1)
|
153
|
+
runner.add_hook(hook2)
|
154
|
+
|
155
|
+
runner.run_after_hooks(test_context)
|
156
|
+
|
157
|
+
expect(execution_order).to eq(['hook1', 'hook2'])
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'handles hooks without after callbacks' do
|
161
|
+
runner = DevCycle::EvalHooksRunner.new
|
162
|
+
hook = DevCycle::EvalHook.new # No after callback
|
163
|
+
|
164
|
+
expect { runner.run_after_hooks(test_context) }.not_to raise_error
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'raises AfterHookError when an after hook raises an error' do
|
168
|
+
runner = DevCycle::EvalHooksRunner.new
|
169
|
+
hook1_called = false
|
170
|
+
|
171
|
+
hook1 = DevCycle::EvalHook.new(
|
172
|
+
after: ->(context) {
|
173
|
+
hook1_called = true
|
174
|
+
raise StandardError, 'Hook 1 error'
|
175
|
+
}
|
176
|
+
)
|
177
|
+
|
178
|
+
hook2 = DevCycle::EvalHook.new(
|
179
|
+
after: ->(context) {
|
180
|
+
# This should not be called because hook1 raises an error
|
181
|
+
}
|
182
|
+
)
|
183
|
+
|
184
|
+
runner.add_hook(hook1)
|
185
|
+
runner.add_hook(hook2)
|
186
|
+
|
187
|
+
expect { runner.run_after_hooks(test_context) }.to raise_error(DevCycle::AfterHookError, /Hook 1 error/)
|
188
|
+
expect(hook1_called).to be true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#run_error_hooks' do
|
193
|
+
it 'runs error hooks with context and error' do
|
194
|
+
runner = DevCycle::EvalHooksRunner.new
|
195
|
+
error_hook_called = false
|
196
|
+
received_error = nil
|
197
|
+
|
198
|
+
hook = DevCycle::EvalHook.new(
|
199
|
+
error: ->(context, error) {
|
200
|
+
error_hook_called = true
|
201
|
+
received_error = error
|
202
|
+
}
|
203
|
+
)
|
204
|
+
|
205
|
+
runner.add_hook(hook)
|
206
|
+
test_error = StandardError.new('Test error')
|
207
|
+
|
208
|
+
runner.run_error_hooks(test_context, test_error)
|
209
|
+
|
210
|
+
expect(error_hook_called).to be true
|
211
|
+
expect(received_error).to eq(test_error)
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'runs multiple error hooks in order' do
|
215
|
+
execution_order = []
|
216
|
+
runner = DevCycle::EvalHooksRunner.new
|
217
|
+
|
218
|
+
hook1 = DevCycle::EvalHook.new(
|
219
|
+
error: ->(context, error) {
|
220
|
+
execution_order << 'hook1'
|
221
|
+
}
|
222
|
+
)
|
223
|
+
|
224
|
+
hook2 = DevCycle::EvalHook.new(
|
225
|
+
error: ->(context, error) {
|
226
|
+
execution_order << 'hook2'
|
227
|
+
}
|
228
|
+
)
|
229
|
+
|
230
|
+
runner.add_hook(hook1)
|
231
|
+
runner.add_hook(hook2)
|
232
|
+
|
233
|
+
test_error = StandardError.new('Test error')
|
234
|
+
runner.run_error_hooks(test_context, test_error)
|
235
|
+
|
236
|
+
expect(execution_order).to eq(['hook1', 'hook2'])
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'handles hooks without error callbacks' do
|
240
|
+
runner = DevCycle::EvalHooksRunner.new
|
241
|
+
hook = DevCycle::EvalHook.new # No error callback
|
242
|
+
|
243
|
+
test_error = StandardError.new('Test error')
|
244
|
+
expect { runner.run_error_hooks(test_context, test_error) }.not_to raise_error
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'continues execution when an error hook raises an error' do
|
248
|
+
runner = DevCycle::EvalHooksRunner.new
|
249
|
+
hook1_called = false
|
250
|
+
hook2_called = false
|
251
|
+
|
252
|
+
hook1 = DevCycle::EvalHook.new(
|
253
|
+
error: ->(context, error) {
|
254
|
+
hook1_called = true
|
255
|
+
raise StandardError, 'Error hook error'
|
256
|
+
}
|
257
|
+
)
|
258
|
+
|
259
|
+
hook2 = DevCycle::EvalHook.new(
|
260
|
+
error: ->(context, error) {
|
261
|
+
hook2_called = true
|
262
|
+
}
|
263
|
+
)
|
264
|
+
|
265
|
+
runner.add_hook(hook1)
|
266
|
+
runner.add_hook(hook2)
|
267
|
+
|
268
|
+
test_error = StandardError.new('Test error')
|
269
|
+
runner.run_error_hooks(test_context, test_error)
|
270
|
+
|
271
|
+
expect(hook1_called).to be true
|
272
|
+
expect(hook2_called).to be true
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe '#run_finally_hooks' do
|
277
|
+
it 'runs finally hooks in order' do
|
278
|
+
execution_order = []
|
279
|
+
runner = DevCycle::EvalHooksRunner.new
|
280
|
+
|
281
|
+
hook1 = DevCycle::EvalHook.new(
|
282
|
+
on_finally: ->(context) {
|
283
|
+
execution_order << 'hook1'
|
284
|
+
}
|
285
|
+
)
|
286
|
+
|
287
|
+
hook2 = DevCycle::EvalHook.new(
|
288
|
+
on_finally: ->(context) {
|
289
|
+
execution_order << 'hook2'
|
290
|
+
}
|
291
|
+
)
|
292
|
+
|
293
|
+
runner.add_hook(hook1)
|
294
|
+
runner.add_hook(hook2)
|
295
|
+
|
296
|
+
runner.run_finally_hooks(test_context)
|
297
|
+
|
298
|
+
expect(execution_order).to eq(['hook1', 'hook2'])
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'handles hooks without finally callbacks' do
|
302
|
+
runner = DevCycle::EvalHooksRunner.new
|
303
|
+
hook = DevCycle::EvalHook.new # No finally callback
|
304
|
+
|
305
|
+
expect { runner.run_finally_hooks(test_context) }.not_to raise_error
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'continues execution when a finally hook raises an error' do
|
309
|
+
runner = DevCycle::EvalHooksRunner.new
|
310
|
+
hook1_called = false
|
311
|
+
hook2_called = false
|
312
|
+
|
313
|
+
hook1 = DevCycle::EvalHook.new(
|
314
|
+
on_finally: ->(context) {
|
315
|
+
hook1_called = true
|
316
|
+
raise StandardError, 'Finally hook error'
|
317
|
+
}
|
318
|
+
)
|
319
|
+
|
320
|
+
hook2 = DevCycle::EvalHook.new(
|
321
|
+
on_finally: ->(context) {
|
322
|
+
hook2_called = true
|
323
|
+
}
|
324
|
+
)
|
325
|
+
|
326
|
+
runner.add_hook(hook1)
|
327
|
+
runner.add_hook(hook2)
|
328
|
+
|
329
|
+
runner.run_finally_hooks(test_context)
|
330
|
+
|
331
|
+
expect(hook1_called).to be true
|
332
|
+
expect(hook2_called).to be true
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
describe DevCycle::EvalHook do
|
338
|
+
describe 'initialization' do
|
339
|
+
it 'initializes with no callbacks' do
|
340
|
+
hook = DevCycle::EvalHook.new
|
341
|
+
expect(hook.before).to be_nil
|
342
|
+
expect(hook.after).to be_nil
|
343
|
+
expect(hook.on_finally).to be_nil
|
344
|
+
expect(hook.error).to be_nil
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'initializes with provided callbacks' do
|
348
|
+
before_callback = ->(context) { context }
|
349
|
+
after_callback = ->(context) { }
|
350
|
+
error_callback = ->(context, error) { }
|
351
|
+
finally_callback = ->(context) { }
|
352
|
+
|
353
|
+
hook = DevCycle::EvalHook.new(
|
354
|
+
before: before_callback,
|
355
|
+
after: after_callback,
|
356
|
+
error: error_callback,
|
357
|
+
on_finally: finally_callback
|
358
|
+
)
|
359
|
+
|
360
|
+
expect(hook.before).to eq(before_callback)
|
361
|
+
expect(hook.after).to eq(after_callback)
|
362
|
+
expect(hook.error).to eq(error_callback)
|
363
|
+
expect(hook.on_finally).to eq(finally_callback)
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'initializes with partial callbacks' do
|
367
|
+
before_callback = ->(context) { context }
|
368
|
+
|
369
|
+
hook = DevCycle::EvalHook.new(before: before_callback)
|
370
|
+
|
371
|
+
expect(hook.before).to eq(before_callback)
|
372
|
+
expect(hook.after).to be_nil
|
373
|
+
expect(hook.on_finally).to be_nil
|
374
|
+
expect(hook.error).to be_nil
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
describe DevCycle::HookContext do
|
380
|
+
describe 'initialization' do
|
381
|
+
it 'initializes with required parameters' do
|
382
|
+
user = { user_id: 'test-user' }
|
383
|
+
context = DevCycle::HookContext.new(
|
384
|
+
key: 'test-key',
|
385
|
+
user: user,
|
386
|
+
default_value: 'test-default'
|
387
|
+
)
|
388
|
+
|
389
|
+
expect(context.key).to eq('test-key')
|
390
|
+
expect(context.user).to eq(user)
|
391
|
+
expect(context.default_value).to eq('test-default')
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'allows modification of attributes' do
|
395
|
+
context = DevCycle::HookContext.new(
|
396
|
+
key: 'original-key',
|
397
|
+
user: 'original-user',
|
398
|
+
default_value: 'original-default'
|
399
|
+
)
|
400
|
+
|
401
|
+
context.key = 'modified-key'
|
402
|
+
context.user = 'modified-user'
|
403
|
+
context.default_value = 'modified-default'
|
404
|
+
|
405
|
+
expect(context.key).to eq('modified-key')
|
406
|
+
expect(context.user).to eq('modified-user')
|
407
|
+
expect(context.default_value).to eq('modified-default')
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|