tep 0.11.2 → 0.11.4
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/Makefile +31 -1
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +8 -8
- data/examples/api_gateway/app.rb +1 -1
- data/examples/blog/app.rb +17 -17
- data/examples/chat/app.rb +12 -12
- data/examples/chatbot/README.md +2 -2
- data/examples/chatbot/app.rb +24 -24
- data/examples/llm_gateway/README.md +6 -5
- data/examples/llm_gateway/app.rb +4 -4
- data/lib/spinel_kit/hex.rb +65 -0
- data/lib/spinel_kit/json.rb +151 -0
- data/lib/spinel_kit/json_decoder.rb +396 -0
- data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
- data/lib/spinel_kit/url.rb +166 -0
- data/lib/tep/auth_bearer_token.rb +6 -6
- data/lib/tep/auth_oauth2.rb +4 -4
- data/lib/tep/events.rb +37 -37
- data/lib/tep/http.rb +3 -3
- data/lib/tep/job.rb +2 -2
- data/lib/tep/jwt.rb +4 -4
- data/lib/tep/live_view.rb +4 -4
- data/lib/tep/llm.rb +13 -45
- data/lib/tep/mcp.rb +12 -12
- data/lib/tep/multipart.rb +1 -1
- data/lib/tep/openai_server.rb +134 -93
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/presence.rb +11 -11
- data/lib/tep/proxy.rb +7 -7
- data/lib/tep/request.rb +1 -1
- data/lib/tep/response.rb +1 -1
- data/lib/tep/router.rb +1 -1
- data/lib/tep/session.rb +2 -2
- data/lib/tep/version.rb +1 -1
- data/lib/tep.rb +30 -29
- data/test/helper.rb +95 -8
- data/test/run_parallel.rb +44 -7
- data/test/test_auth.rb +17 -17
- data/test/test_auth_oauth2.rb +5 -5
- data/test/test_http_pool.rb +4 -4
- data/test/test_http_pool_send.rb +3 -3
- data/test/test_json.rb +12 -12
- data/test/test_jwt.rb +4 -4
- data/test/test_live_view.rb +3 -3
- data/test/test_llm.rb +12 -9
- data/test/test_llm_gateway.rb +2 -2
- data/test/test_logger.rb +2 -2
- data/test/test_openai_server.rb +72 -1
- data/test/test_password.rb +3 -3
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +9 -8
- data/lib/tep/json.rb +0 -572
- data/lib/tep/url.rb +0 -161
data/lib/tep.rb
CHANGED
|
@@ -36,7 +36,7 @@ if defined?(RUBY_ENGINE)
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
require_relative "tep/version"
|
|
39
|
-
require_relative "
|
|
39
|
+
require_relative "spinel_kit/url"
|
|
40
40
|
require_relative "tep/multipart"
|
|
41
41
|
require_relative "tep/net"
|
|
42
42
|
require_relative "tep/agent_delegation"
|
|
@@ -59,7 +59,7 @@ require_relative "tep/router"
|
|
|
59
59
|
require_relative "tep/app"
|
|
60
60
|
# Auth provider classes land after App so Tep::AuthFilter < Tep::Filter
|
|
61
61
|
# resolves and the install! helper can reach Tep::APP. References to
|
|
62
|
-
# Tep::Jwt /
|
|
62
|
+
# Tep::Jwt / SpinelKit::Json inside their method bodies resolve at runtime.
|
|
63
63
|
require_relative "tep/auth_bearer_token"
|
|
64
64
|
require_relative "tep/auth_session_cookie"
|
|
65
65
|
require_relative "tep/auth_oauth2"
|
|
@@ -71,9 +71,10 @@ require_relative "tep/server"
|
|
|
71
71
|
require_relative "tep/server_scheduled"
|
|
72
72
|
require_relative "tep/sqlite"
|
|
73
73
|
require_relative "tep/pg"
|
|
74
|
-
require_relative "
|
|
74
|
+
require_relative "spinel_kit/json"
|
|
75
|
+
require_relative "spinel_kit/json_decoder"
|
|
75
76
|
require_relative "tep/mcp"
|
|
76
|
-
require_relative "
|
|
77
|
+
require_relative "spinel_kit/log"
|
|
77
78
|
require_relative "tep/jwt"
|
|
78
79
|
require_relative "tep/password"
|
|
79
80
|
require_relative "tep/security"
|
|
@@ -453,27 +454,27 @@ module Tep
|
|
|
453
454
|
# NB: don't checkout/checkin against the size-0 seed pool; it'd
|
|
454
455
|
# spin until timeout. The seed has @free.length=0 forever.
|
|
455
456
|
|
|
456
|
-
#
|
|
457
|
+
# SpinelKit::Json type-seeding. Pin every public method's parameter
|
|
457
458
|
# types so an app that uses one method but not another still
|
|
458
459
|
# compiles cleanly. Calls have no side effects beyond producing
|
|
459
460
|
# discardable strings.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
SpinelKit::Json.escape("")
|
|
462
|
+
SpinelKit::Json.quote("")
|
|
463
|
+
SpinelKit::Json.encode_pair_str("", "")
|
|
464
|
+
SpinelKit::Json.encode_pair_int("", 0)
|
|
464
465
|
_tep_seed_str_h = Tep.str_hash
|
|
465
466
|
_tep_seed_str_h["k"] = "v"
|
|
466
|
-
|
|
467
|
+
SpinelKit::Json.from_str_hash(_tep_seed_str_h)
|
|
467
468
|
_tep_seed_int_h = {"" => 0}
|
|
468
469
|
_tep_seed_int_h.delete("")
|
|
469
470
|
_tep_seed_int_h["k"] = 1
|
|
470
|
-
|
|
471
|
+
SpinelKit::Json.from_int_hash(_tep_seed_int_h)
|
|
471
472
|
|
|
472
|
-
#
|
|
473
|
+
# SpinelKit::Log seed -- pin parameter types for every method even
|
|
473
474
|
# when an app uses one but not another. The level-name string
|
|
474
475
|
# ("info") and the messages ("") pin the :str shape; the file-
|
|
475
476
|
# path setter pins to_file's :str arg.
|
|
476
|
-
_tep_seed_logger =
|
|
477
|
+
_tep_seed_logger = SpinelKit::Log.new
|
|
477
478
|
_tep_seed_logger.set_level("info")
|
|
478
479
|
_tep_seed_logger.to_file("")
|
|
479
480
|
_tep_seed_logger.to_stderr
|
|
@@ -481,7 +482,7 @@ module Tep
|
|
|
481
482
|
_tep_seed_logger.info("")
|
|
482
483
|
_tep_seed_logger.warn("")
|
|
483
484
|
_tep_seed_logger.error("")
|
|
484
|
-
|
|
485
|
+
SpinelKit::Log.level_value("info")
|
|
485
486
|
|
|
486
487
|
# Tep::Jwt seed -- pin every method's :str arg types. The
|
|
487
488
|
# secret + payload are blank but the call shapes pin the FFI
|
|
@@ -543,8 +544,8 @@ module Tep
|
|
|
543
544
|
Tep::Shell.read("/etc/hostname")
|
|
544
545
|
Tep::Shell.read_limited("/etc/hostname", 64)
|
|
545
546
|
|
|
546
|
-
#
|
|
547
|
-
|
|
547
|
+
# SpinelKit::Url seed -- the new split_url has to land at compile time.
|
|
548
|
+
SpinelKit::Url.split_url("http://x/")
|
|
548
549
|
|
|
549
550
|
# Tep::Http seed -- every public method gets one canonical call so
|
|
550
551
|
# spinel pins the param types. The URL "http://127.0.0.1:1/" won't
|
|
@@ -623,10 +624,10 @@ module Tep
|
|
|
623
624
|
# Pin Sock.sphttp_sleep_ms's :int param so the backoff call site
|
|
624
625
|
# resolves (called from Tep::Proxy#handle).
|
|
625
626
|
Sock.sphttp_sleep_ms(0)
|
|
626
|
-
#
|
|
627
|
+
# SpinelKit::Json.get_float seed (#133). Pin the (String, String) -> Float
|
|
627
628
|
# surface so callers (CompletionsHandler temperature/top_p,
|
|
628
629
|
# backends that parse their own bodies) resolve cleanly.
|
|
629
|
-
|
|
630
|
+
SpinelKit::Json.get_float("{\"temperature\":0.7}", "temperature")
|
|
630
631
|
_tep_seed_retry_policy.backoff_for(0)
|
|
631
632
|
_tep_seed_retry_policy.retriable?(502)
|
|
632
633
|
_tep_seed_proxy.retry_policy(_tep_seed_proxy_req)
|
|
@@ -679,13 +680,13 @@ module Tep
|
|
|
679
680
|
_tep_seed_oai_models = Tep::Llm::OpenAI::ModelsHandler.new
|
|
680
681
|
_tep_seed_oai_models.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
|
|
681
682
|
# 7.1b /v1/completions surface.
|
|
682
|
-
|
|
683
|
+
SpinelKit::Json.get_int_array("{}", "prompt")
|
|
683
684
|
_tep_seed_oai_sampling = Tep::Llm::OpenAI::Sampling.new
|
|
684
685
|
_tep_seed_oai_sampling.max_tokens = 0
|
|
685
686
|
_tep_seed_oai_sampling.temperature = 1.0
|
|
686
687
|
_tep_seed_oai_sampling.top_p = 1.0
|
|
687
688
|
_tep_seed_oai_comp = Tep::Llm::OpenAI::Completion.new
|
|
688
|
-
_tep_seed_oai_backend.generate_from_tokens("m",
|
|
689
|
+
_tep_seed_oai_backend.generate_from_tokens("m", SpinelKit::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling)
|
|
689
690
|
_tep_seed_oai_completions = Tep::Llm::OpenAI::CompletionsHandler.new
|
|
690
691
|
_tep_seed_oai_completions.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
|
|
691
692
|
# Chat completions skeleton (POST /v1/chat/completions). Default
|
|
@@ -717,10 +718,10 @@ module Tep
|
|
|
717
718
|
# makes the underlying sphttp_write_chunk a harmless EBADF at boot.
|
|
718
719
|
_tep_seed_oai_sink.emit_token("seed")
|
|
719
720
|
_tep_seed_oai_backend.generate_stream_from_tokens(
|
|
720
|
-
"m",
|
|
721
|
+
"m", SpinelKit::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling, _tep_seed_oai_sink)
|
|
721
722
|
_tep_seed_oai_cstreamer = Tep::Llm::OpenAI::CompletionsStreamer.new
|
|
722
723
|
_tep_seed_oai_cstreamer.model = "m"
|
|
723
|
-
_tep_seed_oai_cstreamer.token_ids =
|
|
724
|
+
_tep_seed_oai_cstreamer.token_ids = SpinelKit::Json.get_int_array("{}", "prompt")
|
|
724
725
|
_tep_seed_oai_cstreamer.sampling = _tep_seed_oai_sampling
|
|
725
726
|
_tep_seed_oai_cstreamer.prompt_tokens = 0
|
|
726
727
|
_tep_seed_oai_cstreamer.t0 = 0
|
|
@@ -774,13 +775,13 @@ module Tep
|
|
|
774
775
|
Tep::Job.mark_failed(":memory:", 0)
|
|
775
776
|
_tep_seed_str_arr = [""]
|
|
776
777
|
_tep_seed_str_arr.delete_at(0)
|
|
777
|
-
|
|
778
|
+
SpinelKit::Json.from_str_array(_tep_seed_str_arr)
|
|
778
779
|
_tep_seed_int_arr = [0]
|
|
779
780
|
_tep_seed_int_arr.delete_at(0)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
781
|
+
SpinelKit::Json.from_int_array(_tep_seed_int_arr)
|
|
782
|
+
SpinelKit::Json.get_str("{}", "")
|
|
783
|
+
SpinelKit::Json.get_int("{}", "")
|
|
784
|
+
SpinelKit::Json.has_key?("{}", "")
|
|
784
785
|
|
|
785
786
|
# Tep::MCP seeds (chunk 5.1). Tools register at compile time via
|
|
786
787
|
# bin/tep's mcp_tool DSL; the runtime helpers below are the
|
|
@@ -810,7 +811,7 @@ module Tep
|
|
|
810
811
|
|
|
811
812
|
# Tep::Llm seeds. attr_accessor return types default to mrb_int
|
|
812
813
|
# if spinel sees no concrete callsite -- and Tep::Llm.build_request_body
|
|
813
|
-
# passes msg.role / msg.content into
|
|
814
|
+
# passes msg.role / msg.content into SpinelKit::Json.quote(String) which
|
|
814
815
|
# then mismatches. Pin Message + Response attrs to String, and
|
|
815
816
|
# run one full encode + parse round-trip so the static analyzer
|
|
816
817
|
# sees every public method called with concrete types.
|
|
@@ -846,7 +847,7 @@ module Tep
|
|
|
846
847
|
Tep::Llm.dechunk_leftover("")
|
|
847
848
|
Tep::Llm.dechunk_pass("")
|
|
848
849
|
Tep::Llm.drain_sse_buf("", _tep_seed_llm_stream, "")
|
|
849
|
-
|
|
850
|
+
SpinelKit::Hex.to_int("")
|
|
850
851
|
|
|
851
852
|
# Tep::WebSocket seeds. Pins frame/handshake/driver/connection
|
|
852
853
|
# surfaces to concrete typed callsites so the analyzer doesn't
|
data/test/helper.rb
CHANGED
|
@@ -30,10 +30,51 @@ module TepHarness
|
|
|
30
30
|
attr_reader :port
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Hand out the next port that's actually bindable, skipping any that's
|
|
34
|
+
# already held -- a squatter (stray server from an earlier run) or an orphan
|
|
35
|
+
# the reap missed. Without this, a class boots on a squatted port, its own
|
|
36
|
+
# bind fails, and wait_for_port connects to the SQUATTER instead -> requests
|
|
37
|
+
# routed to the wrong app (cross-talk). Probing closes the TOCTOU window to
|
|
38
|
+
# nearly nothing: under the parallel runner each worker owns a DISJOINT port
|
|
39
|
+
# range (PORT_BASE_START + idx*STEP), so no two workers probe the same port,
|
|
40
|
+
# and the range sits below the OS ephemeral range so client source ports
|
|
41
|
+
# can't race it either. Stays well within the per-file headroom (STEP=50,
|
|
42
|
+
# <=9 classes).
|
|
33
43
|
def self.next_port
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
loop do
|
|
45
|
+
p = @port
|
|
46
|
+
@port += 1
|
|
47
|
+
begin
|
|
48
|
+
TCPServer.new("127.0.0.1", p).close
|
|
49
|
+
return p
|
|
50
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Run a compile, retrying once on a non-success exit that produced NO
|
|
57
|
+
# compiler diagnostic -- i.e. the toolchain was killed/crashed rather than
|
|
58
|
+
# rejecting the source. Under the parallel runner, N concurrent Spinel
|
|
59
|
+
# compiles can transiently exhaust a resource and one gets reaped mid-build
|
|
60
|
+
# (empty error, nonzero exit); a real codegen error is deterministic and
|
|
61
|
+
# surfaces on the retry too, so this never hides genuine breakage. Also
|
|
62
|
+
# helps the serial suite shrug off a one-off hiccup.
|
|
63
|
+
def self.build_with_retry(cmd, what, tries: 2)
|
|
64
|
+
out = ""
|
|
65
|
+
tries.times do |i|
|
|
66
|
+
out = `#{cmd} 2>&1`
|
|
67
|
+
return out if $?.success?
|
|
68
|
+
# Only retry a diagnostic-free (killed/crashed) failure. A genuine
|
|
69
|
+
# rejection carries an "error:" (cc) or "undefined reference" (link).
|
|
70
|
+
# The inlined lib's "cannot resolve ... (emitting 0)" lines are
|
|
71
|
+
# "warning:" only, so they don't trip this -- otherwise every build
|
|
72
|
+
# (they all emit those) would look like a real error and never retry.
|
|
73
|
+
real_error = out.match?(/\berror:|fatal error|undefined reference/i)
|
|
74
|
+
break if real_error || i == tries - 1
|
|
75
|
+
sleep 0.5
|
|
76
|
+
end
|
|
77
|
+
raise "#{what} failed:\n#{out}"
|
|
37
78
|
end
|
|
38
79
|
|
|
39
80
|
# Compile `source` (Sinatra-classic by default) and return the bound
|
|
@@ -47,11 +88,9 @@ module TepHarness
|
|
|
47
88
|
bin = File.join(tmp, "app")
|
|
48
89
|
case mode
|
|
49
90
|
when :sinatra
|
|
50
|
-
|
|
51
|
-
raise "tep build failed:\n#{out}" unless $?.success?
|
|
91
|
+
build_with_retry("#{TEP_BIN} build #{src} -o #{bin}", "tep build")
|
|
52
92
|
when :direct
|
|
53
|
-
|
|
54
|
-
raise "spinel failed:\n#{out}" unless $?.success?
|
|
93
|
+
build_with_retry("#{SPINEL} #{src} -o #{bin}", "spinel")
|
|
55
94
|
else
|
|
56
95
|
raise "unknown mode: #{mode}"
|
|
57
96
|
end
|
|
@@ -87,6 +126,39 @@ module TepHarness
|
|
|
87
126
|
@running.delete(s)
|
|
88
127
|
end
|
|
89
128
|
|
|
129
|
+
# Like terminate, but RETURNS the Process::Status of the SIGTERM'd
|
|
130
|
+
# process (or nil if not found) so a test can assert on how it exited
|
|
131
|
+
# -- e.g. that a no-events server doesn't SIGSEGV on shutdown (the #186
|
|
132
|
+
# regression: termsig == SEGV => exit 139). Bounded: escalates to
|
|
133
|
+
# SIGKILL after `timeout` so a wedged server can't hang the suite.
|
|
134
|
+
def self.terminate_status(port, timeout: 5.0)
|
|
135
|
+
s = find_by_port(port)
|
|
136
|
+
return nil unless s
|
|
137
|
+
pid = s[:pid]
|
|
138
|
+
signal_group(pid, "TERM")
|
|
139
|
+
deadline = Time.now + timeout
|
|
140
|
+
status = nil
|
|
141
|
+
loop do
|
|
142
|
+
begin
|
|
143
|
+
got, st = Process.waitpid2(pid, Process::WNOHANG)
|
|
144
|
+
if got
|
|
145
|
+
status = st
|
|
146
|
+
break
|
|
147
|
+
end
|
|
148
|
+
rescue Errno::ECHILD
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
if Time.now > deadline
|
|
152
|
+
signal_group(pid, "KILL")
|
|
153
|
+
_, status = (Process.waitpid2(pid) rescue [nil, nil])
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
sleep 0.02
|
|
157
|
+
end
|
|
158
|
+
@running.delete(s)
|
|
159
|
+
status
|
|
160
|
+
end
|
|
161
|
+
|
|
90
162
|
def self.kill_all
|
|
91
163
|
@running.each do |s|
|
|
92
164
|
reap(s[:pid])
|
|
@@ -174,7 +246,12 @@ Minitest.after_run { TepHarness.kill_all }
|
|
|
174
246
|
|
|
175
247
|
# Kill any zombie tep test processes leaking from previous runs.
|
|
176
248
|
# Skip on hosts that don't ship `pgrep` (some slim containers don't).
|
|
177
|
-
|
|
249
|
+
# Skip under the parallel runner (TEP_PARALLEL): sibling workers each run
|
|
250
|
+
# their own tep-test binaries concurrently, so a global pgrep-kill here
|
|
251
|
+
# would tear down another worker's live servers. The parallel runner does
|
|
252
|
+
# one stale-proc sweep up front instead, and each worker reaps only its
|
|
253
|
+
# own via Minitest.after_run.
|
|
254
|
+
if !ENV["TEP_PARALLEL"] && system("which pgrep >/dev/null 2>&1")
|
|
178
255
|
`pgrep -f tep-test 2>/dev/null`.split.each do |pid|
|
|
179
256
|
Process.kill("TERM", pid.to_i) rescue nil
|
|
180
257
|
end
|
|
@@ -242,6 +319,16 @@ class TepTest < Minitest::Test
|
|
|
242
319
|
r = klass.new(uri)
|
|
243
320
|
r.body = body if body && %i[post put patch].include?(method)
|
|
244
321
|
headers.each { |k, v| r[k] = v }
|
|
322
|
+
# Ruby <= 3.x's Net::HTTP auto-set Content-Type:
|
|
323
|
+
# application/x-www-form-urlencoded whenever request.body was
|
|
324
|
+
# assigned; Ruby 4.0 dropped that default, so a bodied POST goes
|
|
325
|
+
# out with no Content-Type and tep (correctly) doesn't parse it as
|
|
326
|
+
# form params. Restore the historical default for bodied requests
|
|
327
|
+
# unless a test set its own Content-Type -- matches what real form
|
|
328
|
+
# clients send and keeps the suite Ruby-version-agnostic.
|
|
329
|
+
if r.body && !r["content-type"]
|
|
330
|
+
r["Content-Type"] = "application/x-www-form-urlencoded"
|
|
331
|
+
end
|
|
245
332
|
http.request(r)
|
|
246
333
|
end
|
|
247
334
|
end
|
data/test/run_parallel.rb
CHANGED
|
@@ -22,18 +22,51 @@ require "shellwords"
|
|
|
22
22
|
|
|
23
23
|
ROOT = File.expand_path("..", __dir__)
|
|
24
24
|
TESTS = Dir[File.join(ROOT, "test", "test_*.rb")].sort
|
|
25
|
-
|
|
25
|
+
# Default below core count: each worker's Spinel compile is CPU-bound, and the
|
|
26
|
+
# booted test servers + their HTTP clients also need cycles -- saturating every
|
|
27
|
+
# core just trades compile parallelism for boot-timeout/build-kill flakes (the
|
|
28
|
+
# build retry absorbs those, but at the cost of a re-compile). ~60% of cores is
|
|
29
|
+
# the sweet spot here (12 on a 20-core box: 0F/0E, ~7.6min vs ~30min serial).
|
|
30
|
+
PROCS = (ENV["TEP_TEST_PROCS"] || [Etc.nprocessors * 3 / 5, 2].max.to_s).to_i
|
|
26
31
|
|
|
27
|
-
# Each test class boots its own app on a port
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
# Each test class boots its own app on a port = PORT_BASE_START + idx*STEP
|
|
33
|
+
# (+1 per extra class in the file). Files have at most ~9 classes, so STEP=50
|
|
34
|
+
# is ample disjoint headroom per file. With 64 files the window spans
|
|
35
|
+
# 10000..13200 -- chosen to sit BELOW the OS ephemeral range
|
|
36
|
+
# (/proc/sys/net/ipv4/ip_local_port_range, 32768+ here): a server bound in the
|
|
37
|
+
# ephemeral range collides with another test's *outbound* HTTP client source
|
|
38
|
+
# port (EADDRINUSE bind failures). It's also clear of the legacy 4900 base.
|
|
39
|
+
#
|
|
40
|
+
# Cross-run safety (a server can outlive its worker -- tep #188 leaks one under
|
|
41
|
+
# load, and tep binds prefork listeners SO_REUSEPORT, so a surviving orphan on
|
|
42
|
+
# a reused port would be silently shared / connected-to instead of this run's
|
|
43
|
+
# server -> cross-talk) is handled by SIGKILL reaping (reap_tep_test_procs),
|
|
44
|
+
# up-front and at end, not by moving the window: SIGKILL can't be ignored, so
|
|
45
|
+
# the prior run's orphans are gone before this run binds.
|
|
46
|
+
PORT_BASE_STEP = 50
|
|
47
|
+
PORT_BASE_START = (ENV["TEP_TEST_PORT_BASE"] || "10000").to_i
|
|
32
48
|
|
|
33
49
|
queue = TESTS.each_with_index.to_a
|
|
34
50
|
mutex = Mutex.new
|
|
35
51
|
results = []
|
|
36
52
|
|
|
53
|
+
# Reap stray tep-test servers by PID with SIGKILL (SIGTERM is unreliable here,
|
|
54
|
+
# tep #188). pgrep/Process.kill, not `pkill -f tep-test` -- the latter spawns a
|
|
55
|
+
# shell whose own cmdline contains the pattern and self-matches. run_parallel's
|
|
56
|
+
# cmdline ("ruby test/run_parallel.rb") doesn't contain "tep-test", so it's
|
|
57
|
+
# never a target.
|
|
58
|
+
def reap_tep_test_procs
|
|
59
|
+
`pgrep -f tep-test 2>/dev/null`.split.map(&:to_i).each do |pid|
|
|
60
|
+
Process.kill("KILL", pid) rescue nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Up-front: clear orphans from a previous run. Workers set TEP_PARALLEL so their
|
|
65
|
+
# helper.rb skips its own global pgrep-kill (which would TERM sibling workers'
|
|
66
|
+
# live servers). Per-worker cleanup still runs via Minitest.after_run; this
|
|
67
|
+
# reap is the backstop for what #188 leaks.
|
|
68
|
+
reap_tep_test_procs
|
|
69
|
+
|
|
37
70
|
workers = PROCS.times.map do
|
|
38
71
|
Thread.new do
|
|
39
72
|
loop do
|
|
@@ -41,7 +74,7 @@ workers = PROCS.times.map do
|
|
|
41
74
|
break unless job
|
|
42
75
|
path, idx = job
|
|
43
76
|
port_base = PORT_BASE_START + idx * PORT_BASE_STEP
|
|
44
|
-
env = { "TEP_TEST_PORT_BASE" => port_base.to_s }
|
|
77
|
+
env = { "TEP_TEST_PORT_BASE" => port_base.to_s, "TEP_PARALLEL" => "1" }
|
|
45
78
|
# Honor TEP_SKIP_SPINEL_FRESH if the caller set it (Makefile
|
|
46
79
|
# handles the freshness check once; per-process would be wasteful).
|
|
47
80
|
["TEP_SKIP_SPINEL_FRESH", "SPINEL", "TEP_KEEP_TMP"].each do |k|
|
|
@@ -60,6 +93,10 @@ workers = PROCS.times.map do
|
|
|
60
93
|
end
|
|
61
94
|
workers.each(&:join)
|
|
62
95
|
|
|
96
|
+
# Reap any servers a worker leaked (tep #188) so they don't accumulate across
|
|
97
|
+
# runs or hold the random window's ports.
|
|
98
|
+
reap_tep_test_procs
|
|
99
|
+
|
|
63
100
|
# Sort so the printed order is deterministic (queue order), not
|
|
64
101
|
# completion order.
|
|
65
102
|
results.sort_by! { |r| TESTS.index(r[:path]) }
|
data/test/test_auth.rb
CHANGED
|
@@ -14,46 +14,46 @@ class TestAuth < TepTest
|
|
|
14
14
|
Tep::Auth.install!
|
|
15
15
|
|
|
16
16
|
# ---- mint endpoints (test harness uses these to get tokens) ----
|
|
17
|
-
# The payload is
|
|
17
|
+
# The payload is SpinelKit::Json-friendly flat JSON: sub, exp, caps
|
|
18
18
|
# (comma-separated), and optionally delegate (pipe-encoded).
|
|
19
19
|
|
|
20
20
|
post '/mint_human' do
|
|
21
21
|
res.headers["Content-Type"] = "text/plain"
|
|
22
|
-
sub =
|
|
23
|
-
caps =
|
|
22
|
+
sub = SpinelKit::Json.get_str(req.raw_body, "sub")
|
|
23
|
+
caps = SpinelKit::Json.get_str(req.raw_body, "caps")
|
|
24
24
|
exp = Time.now.to_i + 600
|
|
25
25
|
payload = "{" +
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
SpinelKit::Json.encode_pair_str("sub", sub) + "," +
|
|
27
|
+
SpinelKit::Json.encode_pair_int("exp", exp) + "," +
|
|
28
|
+
SpinelKit::Json.encode_pair_str("caps", caps) +
|
|
29
29
|
"}"
|
|
30
30
|
Tep::Jwt.encode_hs256(payload, SECRET)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
post '/mint_agent' do
|
|
34
34
|
res.headers["Content-Type"] = "text/plain"
|
|
35
|
-
sub =
|
|
36
|
-
caps =
|
|
37
|
-
delegate =
|
|
35
|
+
sub = SpinelKit::Json.get_str(req.raw_body, "sub")
|
|
36
|
+
caps = SpinelKit::Json.get_str(req.raw_body, "caps")
|
|
37
|
+
delegate = SpinelKit::Json.get_str(req.raw_body, "delegate")
|
|
38
38
|
exp = Time.now.to_i + 600
|
|
39
39
|
payload = "{" +
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
SpinelKit::Json.encode_pair_str("sub", sub) + "," +
|
|
41
|
+
SpinelKit::Json.encode_pair_int("exp", exp) + "," +
|
|
42
|
+
SpinelKit::Json.encode_pair_str("caps", caps) + "," +
|
|
43
|
+
SpinelKit::Json.encode_pair_str("delegate", delegate) +
|
|
44
44
|
"}"
|
|
45
45
|
Tep::Jwt.encode_hs256(payload, SECRET)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
post '/mint_expired' do
|
|
49
49
|
res.headers["Content-Type"] = "text/plain"
|
|
50
|
-
sub =
|
|
50
|
+
sub = SpinelKit::Json.get_str(req.raw_body, "sub")
|
|
51
51
|
# Issued in the past, expired in the past.
|
|
52
52
|
exp = Time.now.to_i - 60
|
|
53
53
|
payload = "{" +
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
SpinelKit::Json.encode_pair_str("sub", sub) + "," +
|
|
55
|
+
SpinelKit::Json.encode_pair_int("exp", exp) + "," +
|
|
56
|
+
SpinelKit::Json.encode_pair_str("caps", "read") +
|
|
57
57
|
"}"
|
|
58
58
|
Tep::Jwt.encode_hs256(payload, SECRET)
|
|
59
59
|
end
|
data/test/test_auth_oauth2.rb
CHANGED
|
@@ -30,17 +30,17 @@ class TestAuthOAuth2 < TepTest
|
|
|
30
30
|
# only reach here on user-approve. The test stub skips the UI.
|
|
31
31
|
|
|
32
32
|
post '/consent' do
|
|
33
|
-
principal_id =
|
|
34
|
-
client_id =
|
|
35
|
-
caps_str =
|
|
33
|
+
principal_id = SpinelKit::Json.get_str(req.raw_body, "principal_id")
|
|
34
|
+
client_id = SpinelKit::Json.get_str(req.raw_body, "client_id")
|
|
35
|
+
caps_str = SpinelKit::Json.get_str(req.raw_body, "caps")
|
|
36
36
|
Tep::AuthOAuth2.issue_code(principal_id, client_id, caps_str, 0)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# ---- token-exchange endpoint: bot redeems code for JWT.
|
|
40
40
|
|
|
41
41
|
post '/token' do
|
|
42
|
-
code =
|
|
43
|
-
client_id =
|
|
42
|
+
code = SpinelKit::Json.get_str(req.raw_body, "code")
|
|
43
|
+
client_id = SpinelKit::Json.get_str(req.raw_body, "client_id")
|
|
44
44
|
Tep::AuthOAuth2.exchange_code(code, client_id, 0)
|
|
45
45
|
end
|
|
46
46
|
|
data/test/test_http_pool.rb
CHANGED
|
@@ -51,10 +51,10 @@ class TestHttpPool < TepTest
|
|
|
51
51
|
res.headers["Content-Type"] = "application/json"
|
|
52
52
|
s = Tep::Http::Pool.stats
|
|
53
53
|
"{" +
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
SpinelKit::Json.encode_pair_int("checkouts", s["checkouts"].to_i) + "," +
|
|
55
|
+
SpinelKit::Json.encode_pair_int("checkins", s["checkins"].to_i) + "," +
|
|
56
|
+
SpinelKit::Json.encode_pair_int("hits", s["hits"].to_i) + "," +
|
|
57
|
+
SpinelKit::Json.encode_pair_int("misses", s["misses"].to_i) +
|
|
58
58
|
"}"
|
|
59
59
|
end
|
|
60
60
|
|
data/test/test_http_pool_send.rb
CHANGED
|
@@ -33,9 +33,9 @@ class TestHttpPoolSend < TepTest
|
|
|
33
33
|
r2 = Tep::Http.get(base + "/ping")
|
|
34
34
|
h1 = Tep::Http::Pool.stats["hits"].to_i
|
|
35
35
|
"{" +
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
SpinelKit::Json.encode_pair_int("hits_delta", h1 - h0) + "," +
|
|
37
|
+
SpinelKit::Json.encode_pair_str("b1", r1.body) + "," +
|
|
38
|
+
SpinelKit::Json.encode_pair_str("b2", r2.body) +
|
|
39
39
|
"}"
|
|
40
40
|
end
|
|
41
41
|
RB
|
data/test/test_json.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require_relative "helper"
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# SpinelKit::Json -- pure-Ruby JSON encode primitives + flat-key decode.
|
|
4
4
|
class TestJson < TepTest
|
|
5
5
|
app_source <<~RB
|
|
6
6
|
require 'sinatra'
|
|
@@ -8,56 +8,56 @@ class TestJson < TepTest
|
|
|
8
8
|
# ---- encode side ----
|
|
9
9
|
get '/escape' do
|
|
10
10
|
res.headers["Content-Type"] = "application/json"
|
|
11
|
-
|
|
11
|
+
SpinelKit::Json.quote("a\\"b\\nc")
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
get '/object' do
|
|
15
15
|
res.headers["Content-Type"] = "application/json"
|
|
16
|
-
"{" +
|
|
17
|
-
|
|
16
|
+
"{" + SpinelKit::Json.encode_pair_str("name", "alice") + "," +
|
|
17
|
+
SpinelKit::Json.encode_pair_int("age", 30) + "}"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
get '/array' do
|
|
21
21
|
res.headers["Content-Type"] = "application/json"
|
|
22
|
-
|
|
22
|
+
SpinelKit::Json.from_str_array(["a", "b", "c"])
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
get '/int_array' do
|
|
26
26
|
res.headers["Content-Type"] = "application/json"
|
|
27
|
-
|
|
27
|
+
SpinelKit::Json.from_int_array([1, 2, 3])
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
get '/echo_html' do
|
|
31
31
|
res.headers["Content-Type"] = "application/json"
|
|
32
|
-
"{" +
|
|
32
|
+
"{" + SpinelKit::Json.encode_pair_str("payload", "<script>alert(1)</script>") + "}"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# ---- decode side ----
|
|
36
36
|
post '/parse_str' do
|
|
37
37
|
res.headers["Content-Type"] = "text/plain"
|
|
38
|
-
|
|
38
|
+
SpinelKit::Json.get_str(req.raw_body, "name")
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
post '/parse_int' do
|
|
42
42
|
res.headers["Content-Type"] = "text/plain"
|
|
43
|
-
|
|
43
|
+
SpinelKit::Json.get_int(req.raw_body, "n").to_s
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
post '/has_key' do
|
|
47
47
|
res.headers["Content-Type"] = "text/plain"
|
|
48
|
-
|
|
48
|
+
SpinelKit::Json.has_key?(req.raw_body, "x") ? "yes" : "no"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
post '/skip_nested' do
|
|
52
52
|
# Read a top-level key past a nested object (skip_value should
|
|
53
53
|
# walk the nested object correctly).
|
|
54
54
|
res.headers["Content-Type"] = "text/plain"
|
|
55
|
-
|
|
55
|
+
SpinelKit::Json.get_str(req.raw_body, "after")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
post '/parse_float' do
|
|
59
59
|
res.headers["Content-Type"] = "text/plain"
|
|
60
|
-
|
|
60
|
+
SpinelKit::Json.get_float(req.raw_body, "x").to_s
|
|
61
61
|
end
|
|
62
62
|
RB
|
|
63
63
|
|
data/test/test_jwt.rb
CHANGED
|
@@ -10,8 +10,8 @@ class TestJwt < TepTest
|
|
|
10
10
|
|
|
11
11
|
post '/issue' do
|
|
12
12
|
res.headers["Content-Type"] = "text/plain"
|
|
13
|
-
user =
|
|
14
|
-
payload = "{" +
|
|
13
|
+
user = SpinelKit::Json.get_str(req.raw_body, "user")
|
|
14
|
+
payload = "{" + SpinelKit::Json.encode_pair_str("sub", user) + "}"
|
|
15
15
|
Tep::Jwt.encode_hs256(payload, SECRET)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -42,8 +42,8 @@ class TestJwt < TepTest
|
|
|
42
42
|
|
|
43
43
|
post '/timing_eq' do
|
|
44
44
|
res.headers["Content-Type"] = "text/plain"
|
|
45
|
-
a =
|
|
46
|
-
b =
|
|
45
|
+
a = SpinelKit::Json.get_str(req.raw_body, "a")
|
|
46
|
+
b = SpinelKit::Json.get_str(req.raw_body, "b")
|
|
47
47
|
Tep::Jwt.timing_safe_eq(a, b) ? "yes" : "no"
|
|
48
48
|
end
|
|
49
49
|
RB
|
data/test/test_live_view.rb
CHANGED
|
@@ -150,9 +150,9 @@ class TestLiveView < TepTest
|
|
|
150
150
|
"<div id='tep-live-root'>" + @last_principal + ":" + @last_state + "</div>"
|
|
151
151
|
end
|
|
152
152
|
def handle_presence_diff(diff_json)
|
|
153
|
-
@last_principal =
|
|
154
|
-
@last_kind =
|
|
155
|
-
@last_state =
|
|
153
|
+
@last_principal = SpinelKit::Json.get_str(diff_json, "principal")
|
|
154
|
+
@last_kind = SpinelKit::Json.get_str(diff_json, "kind")
|
|
155
|
+
@last_state = SpinelKit::Json.get_str(diff_json, "state")
|
|
156
156
|
0
|
|
157
157
|
end
|
|
158
158
|
end
|
data/test/test_llm.rb
CHANGED
|
@@ -82,14 +82,15 @@ class TestLlm < TepTest
|
|
|
82
82
|
# --- Phase B: chunked decode + SSE event consume ---
|
|
83
83
|
|
|
84
84
|
get "/hex_to_int_valid" do
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
SpinelKit::Hex.to_int("ff").to_s + "|" +
|
|
86
|
+
SpinelKit::Hex.to_int("a").to_s + "|" +
|
|
87
|
+
SpinelKit::Hex.to_int("100").to_s
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
get "/
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
get "/hex_to_int_leading" do
|
|
91
|
+
SpinelKit::Hex.to_int("zz").to_s + "|" + # no leading hex digit -> 0
|
|
92
|
+
SpinelKit::Hex.to_int("").to_s + "|" + # empty -> 0
|
|
93
|
+
SpinelKit::Hex.to_int("ff;ext").to_s # leading hex, stops at ext -> 255
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
# One chunked body: 5 bytes "Hello", then last-chunk 0.
|
|
@@ -212,9 +213,11 @@ class TestLlm < TepTest
|
|
|
212
213
|
assert_equal "255|10|256", res.body
|
|
213
214
|
end
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
# SpinelKit::Hex.to_int parses LEADING hex (>= 0), unlike the old strict
|
|
217
|
+
# Tep::Llm.hex_to_int that returned -1 on any malformation. See dechunk_*.
|
|
218
|
+
def test_hex_to_int_leading_parse
|
|
219
|
+
res = get("/hex_to_int_leading")
|
|
220
|
+
assert_equal "0|0|255", res.body
|
|
218
221
|
end
|
|
219
222
|
|
|
220
223
|
def test_dechunk_complete_single_chunk
|