tep 0.11.3 → 0.11.5
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 +42 -2
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +47 -10
- 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/app.rb +4 -4
- data/examples/pg_hello.rb +11 -1
- 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/broadcast.rb +18 -80
- 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/net.rb +8 -3
- data/lib/tep/openai_server.rb +102 -94
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/pg.rb +468 -14
- data/lib/tep/presence.rb +33 -329
- 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 +57 -137
- data/spinel-ext.json +6 -0
- 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_broadcast_pg.rb +1 -0
- 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 +10 -1
- data/test/test_password.rb +3 -3
- data/test/test_pg.rb +1 -0
- data/test/test_presence_pg.rb +1 -0
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +23 -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"
|
|
@@ -70,10 +70,14 @@ require_relative "tep/live_view"
|
|
|
70
70
|
require_relative "tep/server"
|
|
71
71
|
require_relative "tep/server_scheduled"
|
|
72
72
|
require_relative "tep/sqlite"
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# tep/pg is OPT-IN (#216): NOT required here. An app that needs
|
|
74
|
+
# PostgreSQL does `require "tep/pg"`, which bin/tep splices in at build
|
|
75
|
+
# time (and the test suite requires explicitly). Keeping it out of the
|
|
76
|
+
# core require tree is what lets a non-PG app DCE the libpq closure.
|
|
77
|
+
require_relative "spinel_kit/json"
|
|
78
|
+
require_relative "spinel_kit/json_decoder"
|
|
75
79
|
require_relative "tep/mcp"
|
|
76
|
-
require_relative "
|
|
80
|
+
require_relative "spinel_kit/log"
|
|
77
81
|
require_relative "tep/jwt"
|
|
78
82
|
require_relative "tep/password"
|
|
79
83
|
require_relative "tep/security"
|
|
@@ -212,13 +216,7 @@ module Tep
|
|
|
212
216
|
APP.set_after(Filter.new)
|
|
213
217
|
APP.set_auth_filter(Filter.new)
|
|
214
218
|
APP.set_auth_bearer_secret("")
|
|
215
|
-
#
|
|
216
|
-
# these via set_broadcast_pg_conn / _channel / _enabled when a
|
|
217
|
-
# connect succeeds; the empty-conninfo seed below short-circuits
|
|
218
|
-
# before getting there, so we exercise the setters directly.
|
|
219
|
-
APP.set_broadcast_pg_enabled(0)
|
|
220
|
-
APP.set_broadcast_pg_channel("")
|
|
221
|
-
APP.set_broadcast_pg_conn(PG::Connection.new(""))
|
|
219
|
+
# (broadcast_pg setter seeds relocated to lib/tep/pg.rb -- #216)
|
|
222
220
|
APP.set_not_found(Handler.new)
|
|
223
221
|
# Type-seeding: methods that may not be called by a given user app
|
|
224
222
|
# would otherwise default their param C types to mrb_int and
|
|
@@ -274,19 +272,10 @@ module Tep
|
|
|
274
272
|
Tep::Broadcast.subscriber_count
|
|
275
273
|
Tep::Broadcast.clear
|
|
276
274
|
|
|
277
|
-
# Broadcast PG-backend seeds. enable_pg_backend("", "") tries to
|
|
278
|
-
# open a PG connection -- empty conninfo behaves the same as the
|
|
279
|
-
# PG::Connection.new("") seed above: connect fails, returns -1.
|
|
280
|
-
# The point is to pin parameter types on every cmeth.
|
|
281
|
-
Tep::Broadcast.enable_pg_backend("", "")
|
|
282
|
-
Tep::Broadcast.poll_pg_once(0)
|
|
283
|
-
Tep::Broadcast.disable_pg_backend
|
|
284
|
-
Tep::Broadcast.encode_wire("", "")
|
|
285
|
-
Tep::Broadcast.deliver_wire_local("0:")
|
|
286
275
|
Tep::Broadcast.publish_local_only("_seed", "")
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
#
|
|
276
|
+
# (Broadcast PG-backend seeds -- enable_pg_backend / poll_pg_once /
|
|
277
|
+
# disable_pg_backend / encode_wire / deliver_wire_local -- relocated
|
|
278
|
+
# to lib/tep/pg.rb, #216.)
|
|
290
279
|
|
|
291
280
|
# Presence type-seeding. Same pattern as Broadcast: pin every
|
|
292
281
|
# cmeth's param C types so compile units that don't otherwise
|
|
@@ -316,25 +305,10 @@ module Tep
|
|
|
316
305
|
Tep::Presence.encode_diff("join", _tep_seed_presence_entry)
|
|
317
306
|
Tep::Presence.publish_diff("join", _tep_seed_presence_entry)
|
|
318
307
|
Tep::Presence.sweep_expired_status
|
|
319
|
-
# PG mirror seeds
|
|
320
|
-
#
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
Tep::Presence.mirror_insert(_tep_seed_presence_entry)
|
|
324
|
-
Tep::Presence.mirror_delete("_seed", -1)
|
|
325
|
-
Tep::Presence.mirror_status("_seed", -1, :available, "", 0)
|
|
326
|
-
Tep::Presence.list_global("_seed")
|
|
327
|
-
Tep::Presence.count_global("_seed")
|
|
328
|
-
Tep::Presence.worker_schema_sql
|
|
329
|
-
Tep::Presence.heartbeat
|
|
330
|
-
Tep::Presence.prune_stale_workers(90)
|
|
331
|
-
Tep::Presence.disable_pg_mirror
|
|
332
|
-
# Same APP-setter-via-constant pattern as the broadcast_pg_conn
|
|
333
|
-
# seed: PG::Connection.new can't run inside App#initialize
|
|
334
|
-
# (Tep::APP is mid-construction; sched_current read segfaults).
|
|
335
|
-
APP.set_presence_pg_enabled(0)
|
|
336
|
-
APP.set_presence_pg_worker_id("")
|
|
337
|
-
APP.set_presence_pg_conn(PG::Connection.new(""))
|
|
308
|
+
# (Presence PG mirror seeds + presence_pg setter seeds relocated to
|
|
309
|
+
# lib/tep/pg.rb -- #216. The core mirror_insert/delete/status no-op
|
|
310
|
+
# stubs are already seeded by the track/set_status/untrack calls
|
|
311
|
+
# above.)
|
|
338
312
|
|
|
339
313
|
# LiveView type-seeding (chunk 4.1). The render_page + dispatch_event
|
|
340
314
|
# cmeths get pinned via top-level calls; the base-class mount /
|
|
@@ -386,94 +360,31 @@ module Tep
|
|
|
386
360
|
_tep_seed_db.close
|
|
387
361
|
end
|
|
388
362
|
|
|
389
|
-
# PG type-seeding
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
|
|
393
|
-
#
|
|
394
|
-
# one method still compile cleanly.
|
|
395
|
-
_tep_seed_pg_conn = PG::Connection.new("")
|
|
396
|
-
_tep_seed_pg_conn.connected?
|
|
397
|
-
_tep_seed_pg_conn.status
|
|
398
|
-
_tep_seed_pg_conn.transaction_status
|
|
399
|
-
_tep_seed_pg_conn.server_version
|
|
400
|
-
_tep_seed_pg_conn.error_message
|
|
401
|
-
_tep_seed_pg_conn.escape_string("")
|
|
402
|
-
_tep_seed_pg_conn.escape_identifier("")
|
|
403
|
-
_tep_seed_pg_conn.escape_literal("")
|
|
404
|
-
_tep_seed_pg_conn.last_sqlstate = ""
|
|
405
|
-
_tep_seed_pg_conn.last_error_message = ""
|
|
406
|
-
_tep_seed_pg_conn.last_result_rh = -1
|
|
407
|
-
# Async surface seed -- calling these on a failed-conn instance
|
|
408
|
-
# is harmless (the C shim short-circuits on conn slot < 1).
|
|
409
|
-
_tep_seed_pg_conn.async_exec("")
|
|
410
|
-
_tep_seed_pg_seed_arr = [""]
|
|
411
|
-
_tep_seed_pg_seed_arr.delete_at(0)
|
|
412
|
-
_tep_seed_pg_conn.async_exec_params("", _tep_seed_pg_seed_arr)
|
|
413
|
-
# Async connect cmeth. Returns -1 for empty conninfo from a
|
|
414
|
-
# non-scheduled context (the shim's PQconnectStart-then-FAILED
|
|
415
|
-
# path), which is type-equivalent to the success path.
|
|
416
|
-
PG::Connection.async_connect("")
|
|
417
|
-
# LISTEN / NOTIFY surface (Tep::Broadcast PG backend lands here).
|
|
418
|
-
_tep_seed_pg_conn.listen("_seed")
|
|
419
|
-
_tep_seed_pg_conn.unlisten("_seed")
|
|
420
|
-
_tep_seed_pg_conn.notify("_seed", "")
|
|
421
|
-
_tep_seed_pg_conn.poll_notification(0)
|
|
422
|
-
_tep_seed_pg_conn.last_notify_channel
|
|
423
|
-
_tep_seed_pg_conn.last_notify_payload
|
|
424
|
-
_tep_seed_pg_res = PG::Result.new(-1)
|
|
425
|
-
_tep_seed_pg_res.ntuples
|
|
426
|
-
_tep_seed_pg_res.nfields
|
|
427
|
-
_tep_seed_pg_res.fname(0)
|
|
428
|
-
_tep_seed_pg_res.fnumber("")
|
|
429
|
-
_tep_seed_pg_res.ftype(0)
|
|
430
|
-
_tep_seed_pg_res.fformat(0)
|
|
431
|
-
_tep_seed_pg_res.fmod(0)
|
|
432
|
-
_tep_seed_pg_res.getvalue(0, 0)
|
|
433
|
-
_tep_seed_pg_res.getisnull(0, 0)
|
|
434
|
-
_tep_seed_pg_res.getlength(0, 0)
|
|
435
|
-
_tep_seed_pg_res.value(0, 0)
|
|
436
|
-
_tep_seed_pg_res.error_field(67)
|
|
437
|
-
_tep_seed_pg_res.cmd_status
|
|
438
|
-
_tep_seed_pg_res.cmd_tuples
|
|
439
|
-
_tep_seed_pg_res.error_message
|
|
440
|
-
_tep_seed_pg_res.sql_state
|
|
441
|
-
_tep_seed_pg_res.fields
|
|
442
|
-
_tep_seed_pg_res.values
|
|
443
|
-
_tep_seed_pg_res.column_values(0)
|
|
444
|
-
_tep_seed_pg_res.clear
|
|
445
|
-
_tep_seed_pg_conn.close
|
|
446
|
-
# Pool seed -- size 0 so we don't try to open real conns at load.
|
|
447
|
-
_tep_seed_pg_pool = PG::Pool.new("", 0)
|
|
448
|
-
_tep_seed_pg_pool.healthy?
|
|
449
|
-
_tep_seed_pg_pool.available
|
|
450
|
-
_tep_seed_pg_pool.size
|
|
451
|
-
_tep_seed_pg_pool.set_checkout_timeout_ms(0)
|
|
452
|
-
_tep_seed_pg_pool.close_all
|
|
453
|
-
# NB: don't checkout/checkin against the size-0 seed pool; it'd
|
|
454
|
-
# spin until timeout. The seed has @free.length=0 forever.
|
|
455
|
-
|
|
456
|
-
# Tep::Json type-seeding. Pin every public method's parameter
|
|
363
|
+
# (PG::Connection / Result / Pool type-seeding relocated to
|
|
364
|
+
# lib/tep/pg.rb -- #216. PG.Connection.new("") is a failed-conn
|
|
365
|
+
# instance, not a raise, so the seeds stay safe at module load.)
|
|
366
|
+
|
|
367
|
+
# SpinelKit::Json type-seeding. Pin every public method's parameter
|
|
457
368
|
# types so an app that uses one method but not another still
|
|
458
369
|
# compiles cleanly. Calls have no side effects beyond producing
|
|
459
370
|
# discardable strings.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
371
|
+
SpinelKit::Json.escape("")
|
|
372
|
+
SpinelKit::Json.quote("")
|
|
373
|
+
SpinelKit::Json.encode_pair_str("", "")
|
|
374
|
+
SpinelKit::Json.encode_pair_int("", 0)
|
|
464
375
|
_tep_seed_str_h = Tep.str_hash
|
|
465
376
|
_tep_seed_str_h["k"] = "v"
|
|
466
|
-
|
|
377
|
+
SpinelKit::Json.from_str_hash(_tep_seed_str_h)
|
|
467
378
|
_tep_seed_int_h = {"" => 0}
|
|
468
379
|
_tep_seed_int_h.delete("")
|
|
469
380
|
_tep_seed_int_h["k"] = 1
|
|
470
|
-
|
|
381
|
+
SpinelKit::Json.from_int_hash(_tep_seed_int_h)
|
|
471
382
|
|
|
472
|
-
#
|
|
383
|
+
# SpinelKit::Log seed -- pin parameter types for every method even
|
|
473
384
|
# when an app uses one but not another. The level-name string
|
|
474
385
|
# ("info") and the messages ("") pin the :str shape; the file-
|
|
475
386
|
# path setter pins to_file's :str arg.
|
|
476
|
-
_tep_seed_logger =
|
|
387
|
+
_tep_seed_logger = SpinelKit::Log.new
|
|
477
388
|
_tep_seed_logger.set_level("info")
|
|
478
389
|
_tep_seed_logger.to_file("")
|
|
479
390
|
_tep_seed_logger.to_stderr
|
|
@@ -481,7 +392,7 @@ module Tep
|
|
|
481
392
|
_tep_seed_logger.info("")
|
|
482
393
|
_tep_seed_logger.warn("")
|
|
483
394
|
_tep_seed_logger.error("")
|
|
484
|
-
|
|
395
|
+
SpinelKit::Log.level_value("info")
|
|
485
396
|
|
|
486
397
|
# Tep::Jwt seed -- pin every method's :str arg types. The
|
|
487
398
|
# secret + payload are blank but the call shapes pin the FFI
|
|
@@ -538,13 +449,22 @@ module Tep
|
|
|
538
449
|
Tep::Scheduler.clear
|
|
539
450
|
|
|
540
451
|
# Tep::Shell seed -- pin :str args at the FFI boundary.
|
|
452
|
+
#
|
|
453
|
+
# These run at MODULE LOAD, so the paths must be readable in EVERY
|
|
454
|
+
# deploy environment, not just gx10. `/etc/hostname` is absent in a
|
|
455
|
+
# bare container (e.g. Upsun); under the engine's now-correct
|
|
456
|
+
# ENOENT-raising File.read it threw at boot and 502'd the native
|
|
457
|
+
# serve_bin (tep#199 boot-hazard report). `/dev/null` exists on every
|
|
458
|
+
# POSIX target (Linux containers, macOS) and reads as empty, so it
|
|
459
|
+
# pins the same :str param type without the missing-file crash. The
|
|
460
|
+
# full fix -- no boot-time seed I/O at all -- is tep#199 (--rbs sig).
|
|
541
461
|
Tep::Shell.run(":")
|
|
542
462
|
Tep::Shell.run_limited(":", 1)
|
|
543
|
-
Tep::Shell.read("/
|
|
544
|
-
Tep::Shell.read_limited("/
|
|
463
|
+
Tep::Shell.read("/dev/null")
|
|
464
|
+
Tep::Shell.read_limited("/dev/null", 64)
|
|
545
465
|
|
|
546
|
-
#
|
|
547
|
-
|
|
466
|
+
# SpinelKit::Url seed -- the new split_url has to land at compile time.
|
|
467
|
+
SpinelKit::Url.split_url("http://x/")
|
|
548
468
|
|
|
549
469
|
# Tep::Http seed -- every public method gets one canonical call so
|
|
550
470
|
# spinel pins the param types. The URL "http://127.0.0.1:1/" won't
|
|
@@ -623,10 +543,10 @@ module Tep
|
|
|
623
543
|
# Pin Sock.sphttp_sleep_ms's :int param so the backoff call site
|
|
624
544
|
# resolves (called from Tep::Proxy#handle).
|
|
625
545
|
Sock.sphttp_sleep_ms(0)
|
|
626
|
-
#
|
|
546
|
+
# SpinelKit::Json.get_float seed (#133). Pin the (String, String) -> Float
|
|
627
547
|
# surface so callers (CompletionsHandler temperature/top_p,
|
|
628
548
|
# backends that parse their own bodies) resolve cleanly.
|
|
629
|
-
|
|
549
|
+
SpinelKit::Json.get_float("{\"temperature\":0.7}", "temperature")
|
|
630
550
|
_tep_seed_retry_policy.backoff_for(0)
|
|
631
551
|
_tep_seed_retry_policy.retriable?(502)
|
|
632
552
|
_tep_seed_proxy.retry_policy(_tep_seed_proxy_req)
|
|
@@ -679,13 +599,13 @@ module Tep
|
|
|
679
599
|
_tep_seed_oai_models = Tep::Llm::OpenAI::ModelsHandler.new
|
|
680
600
|
_tep_seed_oai_models.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
|
|
681
601
|
# 7.1b /v1/completions surface.
|
|
682
|
-
|
|
602
|
+
SpinelKit::Json.get_int_array("{}", "prompt")
|
|
683
603
|
_tep_seed_oai_sampling = Tep::Llm::OpenAI::Sampling.new
|
|
684
604
|
_tep_seed_oai_sampling.max_tokens = 0
|
|
685
605
|
_tep_seed_oai_sampling.temperature = 1.0
|
|
686
606
|
_tep_seed_oai_sampling.top_p = 1.0
|
|
687
607
|
_tep_seed_oai_comp = Tep::Llm::OpenAI::Completion.new
|
|
688
|
-
_tep_seed_oai_backend.generate_from_tokens("m",
|
|
608
|
+
_tep_seed_oai_backend.generate_from_tokens("m", SpinelKit::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling)
|
|
689
609
|
_tep_seed_oai_completions = Tep::Llm::OpenAI::CompletionsHandler.new
|
|
690
610
|
_tep_seed_oai_completions.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
|
|
691
611
|
# Chat completions skeleton (POST /v1/chat/completions). Default
|
|
@@ -717,10 +637,10 @@ module Tep
|
|
|
717
637
|
# makes the underlying sphttp_write_chunk a harmless EBADF at boot.
|
|
718
638
|
_tep_seed_oai_sink.emit_token("seed")
|
|
719
639
|
_tep_seed_oai_backend.generate_stream_from_tokens(
|
|
720
|
-
"m",
|
|
640
|
+
"m", SpinelKit::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling, _tep_seed_oai_sink)
|
|
721
641
|
_tep_seed_oai_cstreamer = Tep::Llm::OpenAI::CompletionsStreamer.new
|
|
722
642
|
_tep_seed_oai_cstreamer.model = "m"
|
|
723
|
-
_tep_seed_oai_cstreamer.token_ids =
|
|
643
|
+
_tep_seed_oai_cstreamer.token_ids = SpinelKit::Json.get_int_array("{}", "prompt")
|
|
724
644
|
_tep_seed_oai_cstreamer.sampling = _tep_seed_oai_sampling
|
|
725
645
|
_tep_seed_oai_cstreamer.prompt_tokens = 0
|
|
726
646
|
_tep_seed_oai_cstreamer.t0 = 0
|
|
@@ -774,13 +694,13 @@ module Tep
|
|
|
774
694
|
Tep::Job.mark_failed(":memory:", 0)
|
|
775
695
|
_tep_seed_str_arr = [""]
|
|
776
696
|
_tep_seed_str_arr.delete_at(0)
|
|
777
|
-
|
|
697
|
+
SpinelKit::Json.from_str_array(_tep_seed_str_arr)
|
|
778
698
|
_tep_seed_int_arr = [0]
|
|
779
699
|
_tep_seed_int_arr.delete_at(0)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
700
|
+
SpinelKit::Json.from_int_array(_tep_seed_int_arr)
|
|
701
|
+
SpinelKit::Json.get_str("{}", "")
|
|
702
|
+
SpinelKit::Json.get_int("{}", "")
|
|
703
|
+
SpinelKit::Json.has_key?("{}", "")
|
|
784
704
|
|
|
785
705
|
# Tep::MCP seeds (chunk 5.1). Tools register at compile time via
|
|
786
706
|
# bin/tep's mcp_tool DSL; the runtime helpers below are the
|
|
@@ -810,7 +730,7 @@ module Tep
|
|
|
810
730
|
|
|
811
731
|
# Tep::Llm seeds. attr_accessor return types default to mrb_int
|
|
812
732
|
# if spinel sees no concrete callsite -- and Tep::Llm.build_request_body
|
|
813
|
-
# passes msg.role / msg.content into
|
|
733
|
+
# passes msg.role / msg.content into SpinelKit::Json.quote(String) which
|
|
814
734
|
# then mismatches. Pin Message + Response attrs to String, and
|
|
815
735
|
# run one full encode + parse round-trip so the static analyzer
|
|
816
736
|
# sees every public method called with concrete types.
|
|
@@ -846,7 +766,7 @@ module Tep
|
|
|
846
766
|
Tep::Llm.dechunk_leftover("")
|
|
847
767
|
Tep::Llm.dechunk_pass("")
|
|
848
768
|
Tep::Llm.drain_sse_buf("", _tep_seed_llm_stream, "")
|
|
849
|
-
|
|
769
|
+
SpinelKit::Hex.to_int("")
|
|
850
770
|
|
|
851
771
|
# Tep::WebSocket seeds. Pins frame/handshake/driver/connection
|
|
852
772
|
# surfaces to concrete typed callsites so the analyzer doesn't
|
data/spinel-ext.json
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
"source": "lib/tep/sphttp.c",
|
|
6
6
|
"cflags": ["-O2"]
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"name": "sphttp",
|
|
10
|
+
"placeholder": "@TEP_SPHTTP_CFLAGS@",
|
|
11
|
+
"pkg_config": "openssl",
|
|
12
|
+
"pkg_config_fallback": "-lssl -lcrypto"
|
|
13
|
+
},
|
|
8
14
|
{
|
|
9
15
|
"name": "sqlite",
|
|
10
16
|
"placeholder": "@TEP_SQLITE_O@",
|
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
|
|