mudis 0.7.0 → 0.7.1

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.
@@ -1,138 +1,138 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "climate_control"
5
-
6
- RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
7
- before do
8
- Mudis.reset!
9
- Mudis.configure do |c|
10
- c.max_ttl = 60 # 60 seconds max
11
- end
12
- end
13
-
14
- describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
15
- before do
16
- Mudis.reset!
17
- Mudis.configure do |c|
18
- c.default_ttl = 60
19
- end
20
- end
21
-
22
- it "applies default_ttl when expires_in is nil" do
23
- Mudis.write("foo", "bar") # no explicit expires_in
24
- meta = Mudis.inspect("foo")
25
- expect(meta[:expires_at]).not_to be_nil
26
- expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
27
- end
28
-
29
- it "respects expires_in if explicitly given" do
30
- Mudis.write("short_lived", "bar", expires_in: 10)
31
- meta = Mudis.inspect("short_lived")
32
- expect(meta[:expires_at]).not_to be_nil
33
- expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
34
- end
35
-
36
- it "applies max_ttl over default_ttl if both are set" do
37
- Mudis.configure do |c|
38
- c.default_ttl = 120
39
- c.max_ttl = 30
40
- end
41
-
42
- Mudis.write("capped", "baz") # no explicit expires_in
43
- meta = Mudis.inspect("capped")
44
- expect(meta[:expires_at]).not_to be_nil
45
- expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
46
- end
47
-
48
- it "stores forever if default_ttl and expires_in are nil" do
49
- Mudis.configure do |c|
50
- c.default_ttl = nil
51
- end
52
-
53
- Mudis.write("forever", "ever")
54
- meta = Mudis.inspect("forever")
55
- expect(meta[:expires_at]).to be_nil
56
- end
57
- end
58
-
59
- it "clamps expires_in to max_ttl if it exceeds max_ttl" do
60
- Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
61
-
62
- metadata = Mudis.inspect("foo")
63
- ttl = metadata[:expires_at] - metadata[:created_at]
64
-
65
- expect(ttl).to be <= 60
66
- expect(ttl).to be > 0
67
- end
68
-
69
- it "respects expires_in if below max_ttl" do
70
- Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
71
-
72
- metadata = Mudis.inspect("bar")
73
- ttl = metadata[:expires_at] - metadata[:created_at]
74
-
75
- expect(ttl).to be_within(1).of(30)
76
- end
77
-
78
- it "allows nil expires_in (no expiry) if not required" do
79
- Mudis.write("baz", "no expiry")
80
-
81
- metadata = Mudis.inspect("baz")
82
- expect(metadata[:expires_at]).to be_nil
83
- end
84
- end
85
-
86
- RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
87
- after { Mudis.reset! }
88
-
89
- describe "bucket configuration" do
90
- it "defaults to 32 buckets if ENV is nil" do
91
- Mudis.instance_variable_set(:@buckets, nil) # force recomputation
92
- ClimateControl.modify(MUDIS_BUCKETS: nil) do
93
- expect(Mudis.send(:buckets)).to eq(32)
94
- end
95
- end
96
-
97
- it "raises if MUDIS_BUCKETS is 0 or less" do
98
- expect do
99
- Mudis.instance_variable_set(:@buckets, nil) # force recomputation
100
- ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
101
- end.to raise_error(ArgumentError, /bucket count must be > 0/)
102
-
103
- expect do
104
- Mudis.instance_variable_set(:@buckets, nil) # force recomputation
105
- ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
106
- end.to raise_error(ArgumentError, /bucket count must be > 0/)
107
- end
108
- end
109
-
110
- describe "memory configuration" do
111
- it "raises if max_bytes is set to 0 or less" do
112
- expect do
113
- Mudis.max_bytes = 0
114
- end.to raise_error(ArgumentError, /max_bytes must be > 0/)
115
-
116
- expect do
117
- Mudis.max_bytes = -1
118
- end.to raise_error(ArgumentError, /max_bytes must be > 0/)
119
- end
120
-
121
- it "raises if max_value_bytes is 0 or less via config" do
122
- expect do
123
- Mudis.configure do |c|
124
- c.max_value_bytes = 0
125
- end
126
- end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
127
- end
128
-
129
- it "raises if max_value_bytes exceeds max_bytes" do
130
- expect do
131
- Mudis.configure do |c|
132
- c.max_bytes = 1_000_000
133
- c.max_value_bytes = 2_000_000
134
- end
135
- end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
136
- end
137
- end
138
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "climate_control"
5
+
6
+ RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
7
+ before do
8
+ Mudis.reset!
9
+ Mudis.configure do |c|
10
+ c.max_ttl = 60 # 60 seconds max
11
+ end
12
+ end
13
+
14
+ describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
15
+ before do
16
+ Mudis.reset!
17
+ Mudis.configure do |c|
18
+ c.default_ttl = 60
19
+ end
20
+ end
21
+
22
+ it "applies default_ttl when expires_in is nil" do
23
+ Mudis.write("foo", "bar") # no explicit expires_in
24
+ meta = Mudis.inspect("foo")
25
+ expect(meta[:expires_at]).not_to be_nil
26
+ expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
27
+ end
28
+
29
+ it "respects expires_in if explicitly given" do
30
+ Mudis.write("short_lived", "bar", expires_in: 10)
31
+ meta = Mudis.inspect("short_lived")
32
+ expect(meta[:expires_at]).not_to be_nil
33
+ expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
34
+ end
35
+
36
+ it "applies max_ttl over default_ttl if both are set" do
37
+ Mudis.configure do |c|
38
+ c.default_ttl = 120
39
+ c.max_ttl = 30
40
+ end
41
+
42
+ Mudis.write("capped", "baz") # no explicit expires_in
43
+ meta = Mudis.inspect("capped")
44
+ expect(meta[:expires_at]).not_to be_nil
45
+ expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
46
+ end
47
+
48
+ it "stores forever if default_ttl and expires_in are nil" do
49
+ Mudis.configure do |c|
50
+ c.default_ttl = nil
51
+ end
52
+
53
+ Mudis.write("forever", "ever")
54
+ meta = Mudis.inspect("forever")
55
+ expect(meta[:expires_at]).to be_nil
56
+ end
57
+ end
58
+
59
+ it "clamps expires_in to max_ttl if it exceeds max_ttl" do
60
+ Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
61
+
62
+ metadata = Mudis.inspect("foo")
63
+ ttl = metadata[:expires_at] - metadata[:created_at]
64
+
65
+ expect(ttl).to be <= 60
66
+ expect(ttl).to be > 0
67
+ end
68
+
69
+ it "respects expires_in if below max_ttl" do
70
+ Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
71
+
72
+ metadata = Mudis.inspect("bar")
73
+ ttl = metadata[:expires_at] - metadata[:created_at]
74
+
75
+ expect(ttl).to be_within(1).of(30)
76
+ end
77
+
78
+ it "allows nil expires_in (no expiry) if not required" do
79
+ Mudis.write("baz", "no expiry")
80
+
81
+ metadata = Mudis.inspect("baz")
82
+ expect(metadata[:expires_at]).to be_nil
83
+ end
84
+ end
85
+
86
+ RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
87
+ after { Mudis.reset! }
88
+
89
+ describe "bucket configuration" do
90
+ it "defaults to 32 buckets if ENV is nil" do
91
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
92
+ ClimateControl.modify(MUDIS_BUCKETS: nil) do
93
+ expect(Mudis.send(:buckets)).to eq(32)
94
+ end
95
+ end
96
+
97
+ it "raises if MUDIS_BUCKETS is 0 or less" do
98
+ expect do
99
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
100
+ ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
101
+ end.to raise_error(ArgumentError, /bucket count must be > 0/)
102
+
103
+ expect do
104
+ Mudis.instance_variable_set(:@buckets, nil) # force recomputation
105
+ ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
106
+ end.to raise_error(ArgumentError, /bucket count must be > 0/)
107
+ end
108
+ end
109
+
110
+ describe "memory configuration" do
111
+ it "raises if max_bytes is set to 0 or less" do
112
+ expect do
113
+ Mudis.max_bytes = 0
114
+ end.to raise_error(ArgumentError, /max_bytes must be > 0/)
115
+
116
+ expect do
117
+ Mudis.max_bytes = -1
118
+ end.to raise_error(ArgumentError, /max_bytes must be > 0/)
119
+ end
120
+
121
+ it "raises if max_value_bytes is 0 or less via config" do
122
+ expect do
123
+ Mudis.configure do |c|
124
+ c.max_value_bytes = 0
125
+ end
126
+ end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
127
+ end
128
+
129
+ it "raises if max_value_bytes exceeds max_bytes" do
130
+ expect do
131
+ Mudis.configure do |c|
132
+ c.max_bytes = 1_000_000
133
+ c.max_value_bytes = 2_000_000
134
+ end
135
+ end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
136
+ end
137
+ end
138
+ end
@@ -1,33 +1,33 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- RSpec.describe "Mudis Memory Guardrails" do
6
- before do
7
- Mudis.reset!
8
- Mudis.stop_expiry_thread
9
- Mudis.instance_variable_set(:@buckets, 1)
10
- Mudis.instance_variable_set(:@stores, [{}])
11
- Mudis.instance_variable_set(:@mutexes, [Mutex.new])
12
- Mudis.instance_variable_set(:@lru_heads, [nil])
13
- Mudis.instance_variable_set(:@lru_tails, [nil])
14
- Mudis.instance_variable_set(:@lru_nodes, [{}])
15
- Mudis.instance_variable_set(:@current_bytes, [0])
16
-
17
- Mudis.max_value_bytes = nil
18
- Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
19
- Mudis.hard_memory_limit = true
20
- Mudis.instance_variable_set(:@max_bytes, 100)
21
- end
22
-
23
- it "rejects writes that exceed max memory" do
24
- big_value = "a" * 90
25
- Mudis.write("a", big_value)
26
- expect(Mudis.read("a")).to eq(big_value)
27
-
28
- big_value2 = "b" * 90
29
- Mudis.write("b", big_value2)
30
- expect(Mudis.read("b")).to be_nil
31
- expect(Mudis.metrics[:rejected]).to be > 0
32
- end
33
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis Memory Guardrails" do
6
+ before do
7
+ Mudis.reset!
8
+ Mudis.stop_expiry_thread
9
+ Mudis.instance_variable_set(:@buckets, 1)
10
+ Mudis.instance_variable_set(:@stores, [{}])
11
+ Mudis.instance_variable_set(:@mutexes, [Mutex.new])
12
+ Mudis.instance_variable_set(:@lru_heads, [nil])
13
+ Mudis.instance_variable_set(:@lru_tails, [nil])
14
+ Mudis.instance_variable_set(:@lru_nodes, [{}])
15
+ Mudis.instance_variable_set(:@current_bytes, [0])
16
+
17
+ Mudis.max_value_bytes = nil
18
+ Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
19
+ Mudis.hard_memory_limit = true
20
+ Mudis.instance_variable_set(:@max_bytes, 100)
21
+ end
22
+
23
+ it "rejects writes that exceed max memory" do
24
+ big_value = "a" * 90
25
+ Mudis.write("a", big_value)
26
+ expect(Mudis.read("a")).to eq(big_value)
27
+
28
+ big_value2 = "b" * 90
29
+ Mudis.write("b", big_value2)
30
+ expect(Mudis.read("b")).to be_nil
31
+ expect(Mudis.metrics[:rejected]).to be > 0
32
+ end
33
+ end
data/spec/metrics_spec.rb CHANGED
@@ -1,34 +1,34 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "spec_helper"
4
-
5
- RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
6
- it "tracks hits and misses" do
7
- Mudis.write("hit_me", "value")
8
- Mudis.read("hit_me")
9
- Mudis.read("miss_me")
10
- metrics = Mudis.metrics
11
- expect(metrics[:hits]).to eq(1)
12
- expect(metrics[:misses]).to eq(1)
13
- end
14
-
15
- it "includes per-bucket stats" do
16
- Mudis.write("a", "x" * 50)
17
- metrics = Mudis.metrics
18
- expect(metrics).to include(:buckets)
19
- expect(metrics[:buckets]).to be_an(Array)
20
- expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
21
- end
22
-
23
- it "resets only the metrics without clearing cache" do
24
- Mudis.write("metrics_key", "value")
25
- Mudis.read("metrics_key")
26
- Mudis.read("missing_key")
27
- expect(Mudis.metrics[:hits]).to eq(1)
28
- expect(Mudis.metrics[:misses]).to eq(1)
29
- Mudis.reset_metrics!
30
- expect(Mudis.metrics[:hits]).to eq(0)
31
- expect(Mudis.metrics[:misses]).to eq(0)
32
- expect(Mudis.read("metrics_key")).to eq("value")
33
- end
34
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
6
+ it "tracks hits and misses" do
7
+ Mudis.write("hit_me", "value")
8
+ Mudis.read("hit_me")
9
+ Mudis.read("miss_me")
10
+ metrics = Mudis.metrics
11
+ expect(metrics[:hits]).to eq(1)
12
+ expect(metrics[:misses]).to eq(1)
13
+ end
14
+
15
+ it "includes per-bucket stats" do
16
+ Mudis.write("a", "x" * 50)
17
+ metrics = Mudis.metrics
18
+ expect(metrics).to include(:buckets)
19
+ expect(metrics[:buckets]).to be_an(Array)
20
+ expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
21
+ end
22
+
23
+ it "resets only the metrics without clearing cache" do
24
+ Mudis.write("metrics_key", "value")
25
+ Mudis.read("metrics_key")
26
+ Mudis.read("missing_key")
27
+ expect(Mudis.metrics[:hits]).to eq(1)
28
+ expect(Mudis.metrics[:misses]).to eq(1)
29
+ Mudis.reset_metrics!
30
+ expect(Mudis.metrics[:hits]).to eq(0)
31
+ expect(Mudis.metrics[:misses]).to eq(0)
32
+ expect(Mudis.read("metrics_key")).to eq("value")
33
+ end
34
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
6
+ let(:socket_path) { "/tmp/mudis.sock" }
7
+ let(:mock_socket) { instance_double(UNIXSocket) }
8
+ let(:client) { MudisClient.new }
9
+
10
+ around do |example|
11
+ ClimateControl.modify("SOCKET_PATH" => socket_path) do
12
+ example.run
13
+ end
14
+ end
15
+
16
+ before do
17
+ allow(UNIXSocket).to receive(:open).and_yield(mock_socket)
18
+ end
19
+
20
+ describe "#read" do
21
+ it "sends a read command and returns the value" do
22
+ payload = { cmd: "read", key: "test_key", namespace: nil }
23
+ response = { ok: true, value: "test_value" }.to_json
24
+
25
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
26
+ expect(mock_socket).to receive(:gets).and_return(response)
27
+
28
+ expect(client.read("test_key")).to eq("test_value")
29
+ end
30
+ end
31
+
32
+ describe "#write" do
33
+ it "sends a write command and returns the value" do
34
+ payload = { cmd: "write", key: "test_key", value: "test_value", ttl: nil, namespace: nil }
35
+ response = { ok: true, value: nil }.to_json
36
+
37
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
38
+ expect(mock_socket).to receive(:gets).and_return(response)
39
+
40
+ expect(client.write("test_key", "test_value")).to be_nil
41
+ end
42
+ end
43
+
44
+ describe "#delete" do
45
+ it "sends a delete command and returns the value" do
46
+ payload = { cmd: "delete", key: "test_key", namespace: nil }
47
+ response = { ok: true, value: nil }.to_json
48
+
49
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
50
+ expect(mock_socket).to receive(:gets).and_return(response)
51
+
52
+ expect(client.delete("test_key")).to be_nil
53
+ end
54
+ end
55
+
56
+ describe "#exists?" do
57
+ it "sends an exists command and returns true" do
58
+ payload = { cmd: "exists", key: "test_key", namespace: nil }
59
+ response = { ok: true, value: true }.to_json
60
+
61
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
62
+ expect(mock_socket).to receive(:gets).and_return(response)
63
+
64
+ expect(client.exists?("test_key")).to eq(true)
65
+ end
66
+ end
67
+
68
+ describe "#fetch" do
69
+ it "fetches an existing value or writes a new one" do
70
+ read_response = { ok: true, value: nil }.to_json
71
+ write_payload = { cmd: "write", key: "test_key", value: "new_value", ttl: nil, namespace: nil }
72
+ write_response = { ok: true, value: nil }.to_json
73
+
74
+ expect(mock_socket).to receive(:puts).with({ cmd: "read", key: "test_key", namespace: nil }.to_json)
75
+ expect(mock_socket).to receive(:gets).and_return(read_response)
76
+ expect(mock_socket).to receive(:puts).with(write_payload.to_json)
77
+ expect(mock_socket).to receive(:gets).and_return(write_response)
78
+
79
+ result = client.fetch("test_key") { "new_value" } # rubocop:disable Style/RedundantFetchBlock
80
+ expect(result).to eq("new_value")
81
+ end
82
+ end
83
+
84
+ describe "#metrics" do
85
+ it "sends a metrics command and returns the metrics" do
86
+ payload = { cmd: "metrics" }
87
+ response = { ok: true, value: { reads: 10, writes: 5 } }.to_json
88
+
89
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
90
+ expect(mock_socket).to receive(:gets).and_return(response)
91
+
92
+ expect(client.metrics).to eq({ reads: 10, writes: 5 })
93
+ end
94
+ end
95
+
96
+ describe "#reset_metrics!" do
97
+ it "sends a reset_metrics command" do
98
+ payload = { cmd: "reset_metrics" }
99
+ response = { ok: true, value: nil }.to_json
100
+
101
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
102
+ expect(mock_socket).to receive(:gets).and_return(response)
103
+
104
+ expect(client.reset_metrics!).to be_nil
105
+ end
106
+ end
107
+
108
+ describe "#reset!" do
109
+ it "sends a reset command" do
110
+ payload = { cmd: "reset" }
111
+ response = { ok: true, value: nil }.to_json
112
+
113
+ expect(mock_socket).to receive(:puts).with(payload.to_json)
114
+ expect(mock_socket).to receive(:gets).and_return(response)
115
+
116
+ expect(client.reset!).to be_nil
117
+ end
118
+ end
119
+
120
+ describe "error handling" do
121
+ it "warns when the socket is missing" do
122
+ allow(UNIXSocket).to receive(:open).and_raise(Errno::ENOENT)
123
+
124
+ expect { client.read("test_key") }.to output(/Socket missing/).to_stderr
125
+ expect(client.read("test_key")).to be_nil
126
+ end
127
+
128
+ it "raises an error when the server returns an error" do
129
+ response = { ok: false, error: "Something went wrong" }.to_json
130
+
131
+ expect(mock_socket).to receive(:puts)
132
+ expect(mock_socket).to receive(:gets).and_return(response)
133
+
134
+ expect { client.read("test_key") }.to raise_error("Something went wrong")
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+
6
+ require_relative "spec_helper"
7
+
8
+ RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
9
+ let(:socket_path) { MudisServer::SOCKET_PATH }
10
+
11
+ before do
12
+ allow(Mudis).to receive(:read).and_return("mock_value")
13
+ allow(Mudis).to receive(:write)
14
+ allow(Mudis).to receive(:delete)
15
+ allow(Mudis).to receive(:exists?).and_return(true)
16
+ allow(Mudis).to receive(:fetch).and_return("mock_fetched_value")
17
+ allow(Mudis).to receive(:metrics).and_return({ reads: 1, writes: 1 })
18
+ allow(Mudis).to receive(:reset_metrics!)
19
+ allow(Mudis).to receive(:reset!)
20
+
21
+ # Start the server in a separate thread
22
+ Thread.new { MudisServer.start! }
23
+ sleep 0.1 # Allow the server to start
24
+ end
25
+
26
+ after do
27
+ File.unlink(socket_path) if File.exist?(socket_path)
28
+ end
29
+
30
+ def send_request(request)
31
+ UNIXSocket.open(socket_path) do |sock|
32
+ sock.puts(JSON.dump(request))
33
+ JSON.parse(sock.gets, symbolize_names: true)
34
+ end
35
+ end
36
+
37
+ it "handles the 'read' command" do
38
+ response = send_request({ cmd: "read", key: "test_key", namespace: "test_ns" })
39
+ expect(response).to eq({ ok: true, value: "mock_value" })
40
+ expect(Mudis).to have_received(:read).with("test_key", namespace: "test_ns")
41
+ end
42
+
43
+ it "handles the 'write' command" do
44
+ response = send_request({ cmd: "write", key: "test_key", value: "test_value", ttl: 60, namespace: "test_ns" })
45
+ expect(response).to eq({ ok: true })
46
+ expect(Mudis).to have_received(:write).with("test_key", "test_value", expires_in: 60, namespace: "test_ns")
47
+ end
48
+
49
+ it "handles the 'delete' command" do
50
+ response = send_request({ cmd: "delete", key: "test_key", namespace: "test_ns" })
51
+ expect(response).to eq({ ok: true })
52
+ expect(Mudis).to have_received(:delete).with("test_key", namespace: "test_ns")
53
+ end
54
+
55
+ it "handles the 'exists' command" do
56
+ response = send_request({ cmd: "exists", key: "test_key", namespace: "test_ns" })
57
+ expect(response).to eq({ ok: true, value: true })
58
+ expect(Mudis).to have_received(:exists?).with("test_key", namespace: "test_ns")
59
+ end
60
+
61
+ it "handles the 'fetch' command" do
62
+ response = send_request({ cmd: "fetch", key: "test_key", ttl: 60, namespace: "test_ns",
63
+ fallback: "fallback_value" })
64
+ expect(response).to eq({ ok: true, value: "mock_fetched_value" })
65
+ expect(Mudis).to have_received(:fetch).with("test_key", expires_in: 60, namespace: "test_ns")
66
+ end
67
+
68
+ it "handles the 'metrics' command" do
69
+ response = send_request({ cmd: "metrics" })
70
+ expect(response).to eq({ ok: true, value: { reads: 1, writes: 1 } })
71
+ expect(Mudis).to have_received(:metrics)
72
+ end
73
+
74
+ it "handles the 'reset_metrics' command" do
75
+ response = send_request({ cmd: "reset_metrics" })
76
+ expect(response).to eq({ ok: true })
77
+ expect(Mudis).to have_received(:reset_metrics!)
78
+ end
79
+
80
+ it "handles the 'reset' command" do
81
+ response = send_request({ cmd: "reset" })
82
+ expect(response).to eq({ ok: true })
83
+ expect(Mudis).to have_received(:reset!)
84
+ end
85
+
86
+ it "handles unknown commands" do
87
+ response = send_request({ cmd: "unknown_command" })
88
+ expect(response).to eq({ ok: false, error: "unknown command: unknown_command" })
89
+ end
90
+ end