@rip-lang/server 1.3.125 → 1.4.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.
- package/{docs/READ_VALIDATORS.md → API.md} +41 -119
- package/CONFIG.md +408 -0
- package/README.md +246 -1109
- package/acme/crypto.rip +0 -2
- package/browse.rip +62 -0
- package/control/cli.rip +95 -36
- package/control/lifecycle.rip +67 -1
- package/control/manager.rip +250 -0
- package/control/mdns.rip +3 -0
- package/middleware.rip +1 -1
- package/package.json +14 -11
- package/server.rip +189 -673
- package/serving/config.rip +766 -0
- package/{edge → serving}/forwarding.rip +2 -2
- package/serving/logging.rip +101 -0
- package/{edge → serving}/metrics.rip +29 -1
- package/serving/proxy.rip +99 -0
- package/{edge → serving}/queue.rip +1 -1
- package/{edge → serving}/ratelimit.rip +1 -1
- package/{edge → serving}/realtime.rip +71 -2
- package/{edge → serving}/registry.rip +1 -1
- package/{edge → serving}/router.rip +3 -3
- package/{edge → serving}/runtime.rip +18 -16
- package/{edge → serving}/security.rip +1 -1
- package/serving/static.rip +393 -0
- package/{edge → serving}/tls.rip +3 -7
- package/{edge → serving}/upstream.rip +4 -4
- package/{edge → serving}/verify.rip +16 -16
- package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
- package/streams/config.rip +8 -8
- package/streams/index.rip +5 -5
- package/streams/router.rip +2 -2
- package/tests/acme.rip +1 -1
- package/tests/config.rip +215 -0
- package/tests/control.rip +1 -1
- package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
- package/tests/extracted.rip +118 -0
- package/tests/helpers.rip +4 -4
- package/tests/metrics.rip +3 -3
- package/tests/proxy.rip +9 -8
- package/tests/read.rip +1 -1
- package/tests/realtime.rip +3 -3
- package/tests/registry.rip +4 -4
- package/tests/router.rip +27 -27
- package/tests/runner.rip +70 -0
- package/tests/security.rip +4 -4
- package/tests/servers.rip +102 -136
- package/tests/static.rip +2 -2
- package/tests/streams_clienthello.rip +2 -2
- package/tests/streams_index.rip +4 -4
- package/tests/streams_pipe.rip +1 -1
- package/tests/streams_router.rip +10 -10
- package/tests/streams_runtime.rip +4 -4
- package/tests/streams_upstream.rip +1 -1
- package/tests/upstream.rip +2 -2
- package/tests/verify.rip +18 -18
- package/tests/watchers.rip +4 -4
- package/default.rip +0 -435
- package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
- package/docs/edge/CONTRACTS.md +0 -137
- package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
- package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
- package/docs/edge/SCHEDULER.md +0 -46
- package/docs/logo.png +0 -0
- package/docs/logo.svg +0 -13
- package/docs/social.png +0 -0
- package/edge/config.rip +0 -607
- package/edge/static.rip +0 -69
- package/tests/edgefile.rip +0 -165
package/tests/config.rip
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { test, eq, ok } from './runner.rip'
|
|
2
|
+
import { normalizeConfig, summarizeConfig, formatConfigErrors } from '../serving/config.rip'
|
|
3
|
+
|
|
4
|
+
getErrors = (fn) ->
|
|
5
|
+
try
|
|
6
|
+
fn()
|
|
7
|
+
[]
|
|
8
|
+
catch e
|
|
9
|
+
e.validationErrors or []
|
|
10
|
+
|
|
11
|
+
test "normalizeConfig accepts composable groups rules and certs", ->
|
|
12
|
+
normalized = normalizeConfig(
|
|
13
|
+
certs:
|
|
14
|
+
trusthealth: '/ssl/trusthealth.com'
|
|
15
|
+
rules:
|
|
16
|
+
web: [
|
|
17
|
+
{ path: '/*', app: 'web' }
|
|
18
|
+
]
|
|
19
|
+
groups:
|
|
20
|
+
publicWeb: ['example.com', 'www.example.com']
|
|
21
|
+
apps:
|
|
22
|
+
web:
|
|
23
|
+
entry: './web/index.rip'
|
|
24
|
+
hosts:
|
|
25
|
+
publicWeb:
|
|
26
|
+
hosts: 'publicWeb'
|
|
27
|
+
cert: 'trusthealth'
|
|
28
|
+
rules: 'web'
|
|
29
|
+
, '/srv')
|
|
30
|
+
eq normalized.kind, 'serve'
|
|
31
|
+
eq Object.keys(normalized.sites).length, 2
|
|
32
|
+
eq normalized.sites['example.com'].routes[0].app, 'web'
|
|
33
|
+
eq normalized.sites['www.example.com'].routes[0].app, 'web'
|
|
34
|
+
ok normalized.resolvedCerts['example.com']
|
|
35
|
+
ok normalized.resolvedCerts['www.example.com']
|
|
36
|
+
|
|
37
|
+
test "normalizeConfig accepts map-literal style grouped host keys", ->
|
|
38
|
+
normalized = normalizeConfig(
|
|
39
|
+
certs:
|
|
40
|
+
shared: '/ssl/shared'
|
|
41
|
+
rules:
|
|
42
|
+
web: [
|
|
43
|
+
{ path: '/*', app: 'web' }
|
|
44
|
+
]
|
|
45
|
+
apps:
|
|
46
|
+
web:
|
|
47
|
+
entry: './web/index.rip'
|
|
48
|
+
hosts: *{
|
|
49
|
+
['example.com', 'foo.bar.com', 'larry.me.org']:
|
|
50
|
+
cert: 'shared'
|
|
51
|
+
rules: 'web'
|
|
52
|
+
}
|
|
53
|
+
, '/srv')
|
|
54
|
+
eq Object.keys(normalized.sites).length, 3
|
|
55
|
+
eq normalized.sites['foo.bar.com'].routes[0].app, 'web'
|
|
56
|
+
ok normalized.resolvedCerts['larry.me.org']
|
|
57
|
+
|
|
58
|
+
test "normalizeConfig allows rule-set refs mixed with inline rules", ->
|
|
59
|
+
normalized = normalizeConfig(
|
|
60
|
+
rules:
|
|
61
|
+
web: [
|
|
62
|
+
{ path: '/*', app: 'web' }
|
|
63
|
+
]
|
|
64
|
+
proxies:
|
|
65
|
+
api:
|
|
66
|
+
hosts: ['http://127.0.0.1:4000']
|
|
67
|
+
apps:
|
|
68
|
+
web:
|
|
69
|
+
entry: './web/index.rip'
|
|
70
|
+
hosts:
|
|
71
|
+
'example.com':
|
|
72
|
+
rules: [
|
|
73
|
+
{ path: '/api/*', proxy: 'api' }
|
|
74
|
+
'web'
|
|
75
|
+
]
|
|
76
|
+
, '/srv')
|
|
77
|
+
eq normalized.sites['example.com'].routes.length, 2
|
|
78
|
+
eq normalized.sites['example.com'].routes[0].proxy, 'api'
|
|
79
|
+
eq normalized.sites['example.com'].routes[1].app, 'web'
|
|
80
|
+
|
|
81
|
+
test "normalizeConfig preserves verify policy", ->
|
|
82
|
+
normalized = normalizeConfig(
|
|
83
|
+
edge:
|
|
84
|
+
verify:
|
|
85
|
+
requireHealthyProxies: false
|
|
86
|
+
requireReadyApps: true
|
|
87
|
+
includeUnroutedManagedApps: false
|
|
88
|
+
minHealthyTargetsPerProxy: 2
|
|
89
|
+
hosts:
|
|
90
|
+
'*.example.com':
|
|
91
|
+
root: '/mnt/site'
|
|
92
|
+
, '/srv')
|
|
93
|
+
eq normalized.server.verify.requireHealthyProxies, false
|
|
94
|
+
eq normalized.server.verify.includeUnroutedManagedApps, false
|
|
95
|
+
eq normalized.server.verify.minHealthyTargetsPerProxy, 2
|
|
96
|
+
|
|
97
|
+
test "normalizeConfig accepts TCP proxies and streams", ->
|
|
98
|
+
normalized = normalizeConfig(
|
|
99
|
+
proxies:
|
|
100
|
+
incus:
|
|
101
|
+
hosts: ['tcp://127.0.0.1:8443']
|
|
102
|
+
hosts:
|
|
103
|
+
'*.example.com':
|
|
104
|
+
root: '/mnt/site'
|
|
105
|
+
streams: [
|
|
106
|
+
{ listen: 8443, sni: ['incus.example.com'], proxy: 'incus' }
|
|
107
|
+
]
|
|
108
|
+
, '/srv')
|
|
109
|
+
ok normalized.streamUpstreams['incus']
|
|
110
|
+
eq normalized.streams.length, 1
|
|
111
|
+
|
|
112
|
+
test "normalizeConfig rejects wildcard ACME domains", ->
|
|
113
|
+
errors = getErrors ->
|
|
114
|
+
normalizeConfig(
|
|
115
|
+
server:
|
|
116
|
+
acme: ['*.example.com']
|
|
117
|
+
hosts:
|
|
118
|
+
'*.example.com':
|
|
119
|
+
root: '/mnt/site'
|
|
120
|
+
, '/srv')
|
|
121
|
+
ok errors.some((err) -> err.code is 'E_ACME_WILDCARD')
|
|
122
|
+
|
|
123
|
+
test "normalizeConfig rejects unknown rule-set references", ->
|
|
124
|
+
errors = getErrors ->
|
|
125
|
+
normalizeConfig(
|
|
126
|
+
hosts:
|
|
127
|
+
'*.example.com':
|
|
128
|
+
rules: 'missing'
|
|
129
|
+
, '/srv')
|
|
130
|
+
ok errors.some((err) -> err.code is 'E_RULE_UNKNOWN')
|
|
131
|
+
|
|
132
|
+
test "normalizeConfig rejects inline app definitions on host blocks", ->
|
|
133
|
+
errors = getErrors ->
|
|
134
|
+
normalizeConfig(
|
|
135
|
+
hosts:
|
|
136
|
+
'api.example.com':
|
|
137
|
+
app:
|
|
138
|
+
entry: './api/index.rip'
|
|
139
|
+
, '/srv')
|
|
140
|
+
ok errors.some((err) -> err.code is 'E_SERVER_APP_INLINE')
|
|
141
|
+
|
|
142
|
+
test "normalizeConfig rejects duplicate hosts after expansion", ->
|
|
143
|
+
errors = getErrors ->
|
|
144
|
+
normalizeConfig(
|
|
145
|
+
groups:
|
|
146
|
+
publicWeb: ['example.com']
|
|
147
|
+
hosts:
|
|
148
|
+
publicWeb:
|
|
149
|
+
hosts: 'publicWeb'
|
|
150
|
+
root: '/srv/site'
|
|
151
|
+
'example.com':
|
|
152
|
+
root: '/srv/other'
|
|
153
|
+
, '/srv')
|
|
154
|
+
ok errors.some((err) -> err.code is 'E_HOST_DUPLICATE')
|
|
155
|
+
|
|
156
|
+
test "normalizeConfig infers HTTP and TCP proxy kinds from hosts", ->
|
|
157
|
+
normalized = normalizeConfig(
|
|
158
|
+
proxies:
|
|
159
|
+
api:
|
|
160
|
+
hosts: ['http://127.0.0.1:4000']
|
|
161
|
+
check:
|
|
162
|
+
path: '/health'
|
|
163
|
+
incus:
|
|
164
|
+
hosts: ['tcp://127.0.0.1:8443']
|
|
165
|
+
hosts:
|
|
166
|
+
'example.com':
|
|
167
|
+
rules: [{ path: '/api/*', proxy: 'api' }]
|
|
168
|
+
'incus.example.com':
|
|
169
|
+
proxy: 'incus'
|
|
170
|
+
, '/srv')
|
|
171
|
+
eq normalized.proxies.api.kind, 'http'
|
|
172
|
+
eq normalized.proxies.incus.kind, 'tcp'
|
|
173
|
+
eq normalized.streams[0].proxy, 'incus'
|
|
174
|
+
|
|
175
|
+
test "normalizeConfig rejects mixed proxy host kinds", ->
|
|
176
|
+
errors = getErrors ->
|
|
177
|
+
normalizeConfig(
|
|
178
|
+
proxies:
|
|
179
|
+
bad:
|
|
180
|
+
hosts: ['http://127.0.0.1:4000', 'tcp://127.0.0.1:8443']
|
|
181
|
+
hosts:
|
|
182
|
+
'example.com':
|
|
183
|
+
root: '/srv/site'
|
|
184
|
+
, '/srv')
|
|
185
|
+
ok errors.some((err) -> err.code is 'E_PROXY_KIND_MIX')
|
|
186
|
+
|
|
187
|
+
test "formatConfigErrors includes code path and hint", ->
|
|
188
|
+
output = formatConfigErrors('serve.rip', [
|
|
189
|
+
code: 'E_ROUTE_ACTION'
|
|
190
|
+
path: 'hosts.publicWeb.rules[0]'
|
|
191
|
+
message: 'route must define exactly one action'
|
|
192
|
+
hint: 'Choose one action field per route.'
|
|
193
|
+
])
|
|
194
|
+
ok output.includes('[E_ROUTE_ACTION]')
|
|
195
|
+
ok output.includes('hosts.publicWeb.rules[0]')
|
|
196
|
+
ok output.includes('Hint:')
|
|
197
|
+
|
|
198
|
+
test "summarizeConfig reports canonical counts", ->
|
|
199
|
+
summary = summarizeConfig('/srv/serve.rip',
|
|
200
|
+
kind: 'serve'
|
|
201
|
+
version: 1
|
|
202
|
+
server: {}
|
|
203
|
+
proxies: { app: { id: 'app', kind: 'http', hosts: ['http://127.0.0.1:3000'] } }
|
|
204
|
+
apps: { web: { entry: '/srv/web/index.rip' } }
|
|
205
|
+
sites:
|
|
206
|
+
'example.com':
|
|
207
|
+
routes: [{ path: '/*', app: 'web' }]
|
|
208
|
+
'www.example.com':
|
|
209
|
+
routes: [{ path: '/*', app: 'web' }]
|
|
210
|
+
streams: []
|
|
211
|
+
)
|
|
212
|
+
eq summary.counts.proxies, 1
|
|
213
|
+
eq summary.counts.apps, 1
|
|
214
|
+
eq summary.counts.hosts, 2
|
|
215
|
+
eq summary.counts.routes, 2
|
package/tests/control.rip
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { test, eq, ok } from '
|
|
1
|
+
import { test, eq, ok } from './runner.rip'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
2
3
|
import { coerceInt, computeSocketPrefix, parseFlags } from '../control/cli.rip'
|
|
3
|
-
import {
|
|
4
|
+
import { createServingRuntime, createReloadHistoryEntry } from '../serving/runtime.rip'
|
|
5
|
+
|
|
6
|
+
serverDir = dirname(import.meta.dir)
|
|
7
|
+
defaultRip = join(serverDir, 'browse.rip')
|
|
4
8
|
|
|
5
9
|
test "control cli helpers load and parse basic flags", ->
|
|
6
|
-
flags = parseFlags(['bun', 'server', '--https-port=9445', '--
|
|
10
|
+
flags = parseFlags(['bun', 'server', '--https-port=9445', '--file=./serve.rip', defaultRip])
|
|
7
11
|
eq coerceInt('42', 0), 42
|
|
8
12
|
eq flags.httpsPort, 9445
|
|
9
|
-
ok flags.
|
|
13
|
+
ok flags.configPath.endsWith('serve.rip')
|
|
10
14
|
|
|
11
15
|
test "control cli uses bare app token as true app name override", ->
|
|
12
16
|
flags = parseFlags(['bun', 'server', 'ola'])
|
|
@@ -25,13 +29,13 @@ test "control cli uses bare app token as true app name override", ->
|
|
|
25
29
|
eq socketPrefix, 'rip_ola'
|
|
26
30
|
|
|
27
31
|
test "control cli supports explicit path with custom app name", ->
|
|
28
|
-
flags = parseFlags(['bun', 'server',
|
|
32
|
+
flags = parseFlags(['bun', 'server', "#{defaultRip}@ola"])
|
|
29
33
|
eq flags.appName, 'ola'
|
|
30
34
|
eq flags.appAliases[0], 'ola'
|
|
31
35
|
eq flags.socketPrefix, 'rip_ola'
|
|
32
36
|
|
|
33
|
-
test "
|
|
34
|
-
runtime =
|
|
37
|
+
test "serving runtime helpers load and create summary entries", ->
|
|
38
|
+
runtime = createServingRuntime()
|
|
35
39
|
entry = createReloadHistoryEntry('reload-1', 'test', null, 1, 'applied')
|
|
36
40
|
eq runtime.configInfo.kind, 'none'
|
|
37
41
|
eq entry.id, 'reload-1'
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# extracted.rip — tests for modules extracted during the server rebuild
|
|
2
|
+
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
|
+
import { nowMs, isDebug, applyFlagSideEffects, stripInternalHeaders } from '../serving/logging.rip'
|
|
5
|
+
import { proxyRouteToUpstream, upgradeProxyWebSocket } from '../serving/proxy.rip'
|
|
6
|
+
import { createMetrics, buildDiagnosticsBody } from '../serving/metrics.rip'
|
|
7
|
+
import { proxyRealtimeToWorker, buildWebSocketHandlers, createHub } from '../serving/realtime.rip'
|
|
8
|
+
import { Manager } from '../control/manager.rip'
|
|
9
|
+
import { createUpstreamPool } from '../serving/upstream.rip'
|
|
10
|
+
import { createServingRuntime } from '../serving/runtime.rip'
|
|
11
|
+
|
|
12
|
+
# --- serving/logging.rip ---
|
|
13
|
+
|
|
14
|
+
test "nowMs returns a number", ->
|
|
15
|
+
ok typeof nowMs() is 'number'
|
|
16
|
+
ok nowMs() > 0
|
|
17
|
+
|
|
18
|
+
test "isDebug returns false by default", ->
|
|
19
|
+
eq isDebug(), false
|
|
20
|
+
|
|
21
|
+
test "applyFlagSideEffects sets debug mode", ->
|
|
22
|
+
applyFlagSideEffects({ debug: true })
|
|
23
|
+
ok isDebug()
|
|
24
|
+
applyFlagSideEffects({ debug: false })
|
|
25
|
+
|
|
26
|
+
test "stripInternalHeaders removes rip headers and preserves others", ->
|
|
27
|
+
h = new Headers()
|
|
28
|
+
h.set('content-type', 'text/plain')
|
|
29
|
+
h.set('rip-worker-busy', '1')
|
|
30
|
+
h.set('rip-worker-id', 'w-0')
|
|
31
|
+
h.set('rip-no-log', '1')
|
|
32
|
+
h.set('x-custom', 'keep')
|
|
33
|
+
stripped = stripInternalHeaders(h)
|
|
34
|
+
ok stripped.has('content-type')
|
|
35
|
+
ok stripped.has('x-custom')
|
|
36
|
+
eq stripped.has('rip-worker-busy'), false
|
|
37
|
+
eq stripped.has('rip-worker-id'), false
|
|
38
|
+
eq stripped.has('rip-no-log'), false
|
|
39
|
+
|
|
40
|
+
# --- serving/proxy.rip ---
|
|
41
|
+
|
|
42
|
+
test "proxyRouteToUpstream returns 503 when no matching proxy", ->
|
|
43
|
+
pool = createUpstreamPool()
|
|
44
|
+
runtime = createServingRuntime(null, pool)
|
|
45
|
+
metrics = createMetrics()
|
|
46
|
+
route = { proxy: 'nonexistent', timeouts: {} }
|
|
47
|
+
req = new Request('http://localhost/test')
|
|
48
|
+
res = await proxyRouteToUpstream(req, route, 'req-1', '127.0.0.1', runtime, metrics, {}, (->), (->))
|
|
49
|
+
eq res.status, 503
|
|
50
|
+
|
|
51
|
+
test "upgradeProxyWebSocket returns 503 when no matching proxy", ->
|
|
52
|
+
pool = createUpstreamPool()
|
|
53
|
+
runtime = createServingRuntime(null, pool)
|
|
54
|
+
route = { proxy: 'nonexistent' }
|
|
55
|
+
res = upgradeProxyWebSocket(null, null, route, 'req-1', runtime)
|
|
56
|
+
eq res.status, 503
|
|
57
|
+
|
|
58
|
+
# --- serving/metrics.rip ---
|
|
59
|
+
|
|
60
|
+
test "buildDiagnosticsBody returns valid JSON with expected keys", ->
|
|
61
|
+
metrics = createMetrics()
|
|
62
|
+
runtime = createServingRuntime()
|
|
63
|
+
body = buildDiagnosticsBody
|
|
64
|
+
metrics: metrics
|
|
65
|
+
startedAt: Date.now()
|
|
66
|
+
appRegistry: { apps: new Map(), hostIndex: new Map() }
|
|
67
|
+
servingRuntime: runtime
|
|
68
|
+
serverVersion: '1.0.0'
|
|
69
|
+
ripVersion: '3.0.0'
|
|
70
|
+
configInfo: { kind: 'none' }
|
|
71
|
+
realtimeStats: { clients: 0, groups: 0 }
|
|
72
|
+
hosts: []
|
|
73
|
+
streamDiagnostics: {}
|
|
74
|
+
multiplexer: { enabled: false }
|
|
75
|
+
activeRuntime: { id: 'test', inflight: 0, wsConnections: 0 }
|
|
76
|
+
retiredRuntimes: []
|
|
77
|
+
activeStreamRuntime: { id: 'test', inflight: 0, listeners: [] }
|
|
78
|
+
retiredStreamRuntimes: []
|
|
79
|
+
parsed = JSON.parse(body)
|
|
80
|
+
ok parsed.status
|
|
81
|
+
ok parsed.version
|
|
82
|
+
ok parsed.metrics
|
|
83
|
+
ok parsed.gauges
|
|
84
|
+
eq parsed.version.server, '1.0.0'
|
|
85
|
+
|
|
86
|
+
# --- serving/realtime.rip ---
|
|
87
|
+
|
|
88
|
+
test "proxyRealtimeToWorker returns null when no socket available", ->
|
|
89
|
+
result = await proxyRealtimeToWorker(new Headers(), 'open', '', (-> null), (->))
|
|
90
|
+
eq result, null
|
|
91
|
+
|
|
92
|
+
test "buildWebSocketHandlers returns object with websocket handlers", ->
|
|
93
|
+
hub = createHub()
|
|
94
|
+
serverStub =
|
|
95
|
+
metrics: createMetrics()
|
|
96
|
+
retainRuntimeWs: (->)
|
|
97
|
+
releaseRuntimeWs: (->)
|
|
98
|
+
createWsPassthrough: (->)
|
|
99
|
+
releaseProxyTarget: (->)
|
|
100
|
+
logEvent: (->)
|
|
101
|
+
proxyRealtimeToWorker: (->)
|
|
102
|
+
handlers = buildWebSocketHandlers(hub, serverStub)
|
|
103
|
+
ok handlers.websocket
|
|
104
|
+
ok typeof handlers.websocket.open is 'function'
|
|
105
|
+
ok typeof handlers.websocket.message is 'function'
|
|
106
|
+
ok typeof handlers.websocket.close is 'function'
|
|
107
|
+
|
|
108
|
+
# --- control/manager.rip ---
|
|
109
|
+
|
|
110
|
+
test "Manager constructor initializes expected state", ->
|
|
111
|
+
flags = { appName: 'test', workers: 2 }
|
|
112
|
+
mgr = new Manager(flags, '/tmp/server.rip')
|
|
113
|
+
eq mgr.appWorkers.size, 0
|
|
114
|
+
eq mgr.currentVersion, 1
|
|
115
|
+
eq mgr.shuttingDown, false
|
|
116
|
+
eq mgr.defaultAppId, 'test'
|
|
117
|
+
ok mgr.restartBudgets instanceof Map
|
|
118
|
+
ok mgr.deferredDeaths instanceof Set
|
package/tests/helpers.rip
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# helpers.rip — serving/forwarding response factory tests
|
|
2
2
|
|
|
3
|
-
import { test, eq, ok } from '
|
|
4
|
-
import { formatPort, maybeAddSecurityHeaders, buildStatusBody, buildRipdevUrl } from '../
|
|
5
|
-
import { watchUnavailableResponse, serverBusyResponse, serviceUnavailableResponse, gatewayTimeoutResponse, queueTimeoutResponse } from '../
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
|
+
import { formatPort, maybeAddSecurityHeaders, buildStatusBody, buildRipdevUrl } from '../serving/forwarding.rip'
|
|
5
|
+
import { watchUnavailableResponse, serverBusyResponse, serviceUnavailableResponse, gatewayTimeoutResponse, queueTimeoutResponse } from '../serving/forwarding.rip'
|
|
6
6
|
|
|
7
7
|
# --- formatPort ---
|
|
8
8
|
|
package/tests/metrics.rip
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
# test-metrics.rip —
|
|
2
|
+
# test-metrics.rip — serving/metrics.rip tests
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
|
|
5
|
-
import { test, eq, ok, near } from '
|
|
6
|
-
import { createMetrics } from '../
|
|
5
|
+
import { test, eq, ok, near } from './runner.rip'
|
|
6
|
+
import { createMetrics } from '../serving/metrics.rip'
|
|
7
7
|
|
|
8
8
|
test "createMetrics returns object with counters", ->
|
|
9
9
|
m = createMetrics()
|
package/tests/proxy.rip
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# proxy.rip — reverse proxy and config loader tests
|
|
2
2
|
|
|
3
|
-
import { test, eq, ok } from '
|
|
4
|
-
import { normalizeAppConfig, applyConfig
|
|
5
|
-
import { createAppRegistry, registerApp, resolveHost, getAppState } from '../
|
|
6
|
-
import { broadcastBinary, createHub, addClient } from '../
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
|
+
import { normalizeAppConfig, applyConfig } from '../serving/config.rip'
|
|
5
|
+
import { createAppRegistry, registerApp, resolveHost, getAppState } from '../serving/registry.rip'
|
|
6
|
+
import { broadcastBinary, createHub, addClient } from '../serving/realtime.rip'
|
|
7
7
|
import { setEventJsonMode, logEvent } from '../control/lifecycle.rip'
|
|
8
8
|
|
|
9
9
|
# --- Config normalization ---
|
|
@@ -36,6 +36,7 @@ test "normalizeAppConfig keeps absolute entry path", ->
|
|
|
36
36
|
test "applyConfig registers apps in registry", ->
|
|
37
37
|
registry = createAppRegistry()
|
|
38
38
|
config =
|
|
39
|
+
kind: 'serve'
|
|
39
40
|
apps:
|
|
40
41
|
web: { hosts: ['example.com'], workers: 4 }
|
|
41
42
|
api: { hosts: ['api.example.com'], workers: 2 }
|
|
@@ -46,13 +47,13 @@ test "applyConfig registers apps in registry", ->
|
|
|
46
47
|
|
|
47
48
|
test "applyConfig handles empty apps", ->
|
|
48
49
|
registry = createAppRegistry()
|
|
49
|
-
registered = applyConfig({ apps: {} }, registry, registerApp, '/srv')
|
|
50
|
+
registered = applyConfig({ kind: 'serve', apps: {} }, registry, registerApp, '/srv')
|
|
50
51
|
eq registered.length, 0
|
|
51
52
|
|
|
52
|
-
test "
|
|
53
|
+
test "applyConfig preserves managed app entry workers and env", ->
|
|
53
54
|
registry = createAppRegistry()
|
|
54
55
|
config =
|
|
55
|
-
kind: '
|
|
56
|
+
kind: 'serve'
|
|
56
57
|
apps:
|
|
57
58
|
admin:
|
|
58
59
|
entry: '/srv/admin/index.rip'
|
|
@@ -62,7 +63,7 @@ test "applyEdgeConfig preserves managed app entry workers and env", ->
|
|
|
62
63
|
queueTimeoutMs: 1234
|
|
63
64
|
readTimeoutMs: 5678
|
|
64
65
|
env: { ADMIN_MODE: '1' }
|
|
65
|
-
registered =
|
|
66
|
+
registered = applyConfig(config, registry, registerApp, '/srv')
|
|
66
67
|
eq registered.length, 1
|
|
67
68
|
state = getAppState(registry, 'admin')
|
|
68
69
|
eq state.config.entry, '/srv/admin/index.rip'
|
package/tests/read.rip
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# Each test is self-contained: t(name, input, expected, fn)
|
|
8
8
|
# No HTTP server needed — uses requestContext.run() directly.
|
|
9
9
|
#
|
|
10
|
-
# Run: rip packages/server/
|
|
10
|
+
# Run: ./bin/rip packages/server/tests/read.rip
|
|
11
11
|
#
|
|
12
12
|
|
|
13
13
|
import { read, requestContext } from '../api.rip'
|
package/tests/realtime.rip
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# realtime.rip —
|
|
1
|
+
# realtime.rip — serving/realtime.rip unit tests
|
|
2
2
|
|
|
3
|
-
import { test, eq, ok } from '
|
|
4
|
-
import { createHub, generateClientId, addClient, removeClient, processResponse, handlePublish, getRealtimeStats } from '../
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
|
+
import { createHub, generateClientId, addClient, removeClient, processResponse, handlePublish, getRealtimeStats } from '../serving/realtime.rip'
|
|
5
5
|
|
|
6
6
|
# --- Hub lifecycle ---
|
|
7
7
|
|
package/tests/registry.rip
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# test-registry.rip —
|
|
1
|
+
# test-registry.rip — serving/registry, scheduler, and queue tests
|
|
2
2
|
|
|
3
|
-
import { test, eq, ok } from '
|
|
4
|
-
import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from '../
|
|
5
|
-
import { isCurrentVersion, getNextAvailableSocket, releaseWorker, shouldRetryBodylessBusy, drainQueueOnce } from '../
|
|
3
|
+
import { test, eq, ok } from './runner.rip'
|
|
4
|
+
import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from '../serving/registry.rip'
|
|
5
|
+
import { isCurrentVersion, getNextAvailableSocket, releaseWorker, shouldRetryBodylessBusy, drainQueueOnce } from '../serving/queue.rip'
|
|
6
6
|
|
|
7
7
|
# --- Registry ---
|
|
8
8
|
|
package/tests/router.rip
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { test, eq, ok } from '
|
|
2
|
-
import { compileRouteTable, matchRoute, describeRoute } from '../
|
|
1
|
+
import { test, eq, ok } from './runner.rip'
|
|
2
|
+
import { compileRouteTable, matchRoute, describeRoute } from '../serving/router.rip'
|
|
3
3
|
|
|
4
4
|
test "compileRouteTable keeps global route order", ->
|
|
5
5
|
table = compileRouteTable([
|
|
6
|
-
{ id: 'one', path: '/a',
|
|
7
|
-
{ id: 'two', path: '/b',
|
|
6
|
+
{ id: 'one', path: '/a', proxy: 'app' }
|
|
7
|
+
{ id: 'two', path: '/b', proxy: 'app' }
|
|
8
8
|
])
|
|
9
9
|
eq table.routes.length, 2
|
|
10
10
|
eq table.routes[0].order, 0
|
|
@@ -20,62 +20,62 @@ test "compileRouteTable merges site routes with inherited host", ->
|
|
|
20
20
|
|
|
21
21
|
test "matchRoute prefers exact host over catchall", ->
|
|
22
22
|
table = compileRouteTable([
|
|
23
|
-
{ path: '/*',
|
|
24
|
-
{ host: 'api.example.com', path: '/*',
|
|
23
|
+
{ path: '/*', proxy: 'fallback' }
|
|
24
|
+
{ host: 'api.example.com', path: '/*', proxy: 'api' }
|
|
25
25
|
])
|
|
26
|
-
eq matchRoute(table, 'api.example.com', '/users').
|
|
26
|
+
eq matchRoute(table, 'api.example.com', '/users').proxy, 'api'
|
|
27
27
|
|
|
28
28
|
test "matchRoute prefers wildcard host over catchall", ->
|
|
29
29
|
table = compileRouteTable([
|
|
30
|
-
{ path: '/*',
|
|
31
|
-
{ host: '*.example.com', path: '/*',
|
|
30
|
+
{ path: '/*', proxy: 'fallback' }
|
|
31
|
+
{ host: '*.example.com', path: '/*', proxy: 'wild' }
|
|
32
32
|
])
|
|
33
|
-
eq matchRoute(table, 'api.example.com', '/users').
|
|
33
|
+
eq matchRoute(table, 'api.example.com', '/users').proxy, 'wild'
|
|
34
34
|
|
|
35
35
|
test "matchRoute does not let wildcard span multiple labels", ->
|
|
36
36
|
table = compileRouteTable([
|
|
37
|
-
{ host: '*.example.com', path: '/*',
|
|
38
|
-
{ path: '/*',
|
|
37
|
+
{ host: '*.example.com', path: '/*', proxy: 'wild' }
|
|
38
|
+
{ path: '/*', proxy: 'fallback' }
|
|
39
39
|
])
|
|
40
|
-
eq matchRoute(table, 'a.b.example.com', '/users').
|
|
40
|
+
eq matchRoute(table, 'a.b.example.com', '/users').proxy, 'fallback'
|
|
41
41
|
|
|
42
42
|
test "matchRoute prefers exact path over prefix", ->
|
|
43
43
|
table = compileRouteTable([
|
|
44
|
-
{ path: '/api/*',
|
|
45
|
-
{ path: '/api/users',
|
|
44
|
+
{ path: '/api/*', proxy: 'api' }
|
|
45
|
+
{ path: '/api/users', proxy: 'users' }
|
|
46
46
|
])
|
|
47
|
-
eq matchRoute(table, 'example.com', '/api/users').
|
|
47
|
+
eq matchRoute(table, 'example.com', '/api/users').proxy, 'users'
|
|
48
48
|
|
|
49
49
|
test "matchRoute prefers longer prefix", ->
|
|
50
50
|
table = compileRouteTable([
|
|
51
|
-
{ path: '/api/*',
|
|
52
|
-
{ path: '/api/admin/*',
|
|
51
|
+
{ path: '/api/*', proxy: 'api' }
|
|
52
|
+
{ path: '/api/admin/*', proxy: 'admin' }
|
|
53
53
|
])
|
|
54
|
-
eq matchRoute(table, 'example.com', '/api/admin/users').
|
|
54
|
+
eq matchRoute(table, 'example.com', '/api/admin/users').proxy, 'admin'
|
|
55
55
|
|
|
56
56
|
test "matchRoute uses lower priority number before order", ->
|
|
57
57
|
table = compileRouteTable([
|
|
58
|
-
{ path: '/api/*',
|
|
59
|
-
{ path: '/api/*',
|
|
58
|
+
{ path: '/api/*', proxy: 'one', priority: 10 }
|
|
59
|
+
{ path: '/api/*', proxy: 'two', priority: 5 }
|
|
60
60
|
])
|
|
61
|
-
eq matchRoute(table, 'example.com', '/api/x').
|
|
61
|
+
eq matchRoute(table, 'example.com', '/api/x').proxy, 'two'
|
|
62
62
|
|
|
63
63
|
test "matchRoute filters by method", ->
|
|
64
64
|
table = compileRouteTable([
|
|
65
|
-
{ path: '/submit',
|
|
66
|
-
{ path: '/submit',
|
|
65
|
+
{ path: '/submit', proxy: 'read', methods: ['GET'] }
|
|
66
|
+
{ path: '/submit', proxy: 'write', methods: ['POST'] }
|
|
67
67
|
])
|
|
68
|
-
eq matchRoute(table, 'example.com', '/submit', 'POST').
|
|
68
|
+
eq matchRoute(table, 'example.com', '/submit', 'POST').proxy, 'write'
|
|
69
69
|
|
|
70
70
|
test "matchRoute returns null when nothing matches", ->
|
|
71
71
|
table = compileRouteTable([
|
|
72
|
-
{ host: 'api.example.com', path: '/api/*',
|
|
72
|
+
{ host: 'api.example.com', path: '/api/*', proxy: 'api' }
|
|
73
73
|
])
|
|
74
74
|
eq matchRoute(table, 'example.com', '/api/x'), null
|
|
75
75
|
|
|
76
76
|
test "describeRoute summarizes action", ->
|
|
77
77
|
table = compileRouteTable([
|
|
78
|
-
{ host: 'api.example.com', path: '/api/*',
|
|
78
|
+
{ host: 'api.example.com', path: '/api/*', proxy: 'api' }
|
|
79
79
|
])
|
|
80
80
|
summary = describeRoute(matchRoute(table, 'api.example.com', '/api/x'))
|
|
81
81
|
ok summary.includes('proxy:api')
|
package/tests/runner.rip
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# tests/runner.rip — server test harness
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
#
|
|
5
|
+
# Run from package dir: bun run test
|
|
6
|
+
# Run from repo root: ./bin/rip packages/server/tests/runner.rip
|
|
7
|
+
#
|
|
8
|
+
# Discovers and runs all *.rip files in tests/
|
|
9
|
+
# ==============================================================================
|
|
10
|
+
|
|
11
|
+
import { readdirSync } from 'node:fs'
|
|
12
|
+
import { join, dirname } from 'node:path'
|
|
13
|
+
|
|
14
|
+
# --- Test helpers (exported for test files to use) ---
|
|
15
|
+
|
|
16
|
+
passed = 0
|
|
17
|
+
failed = 0
|
|
18
|
+
failures = []
|
|
19
|
+
|
|
20
|
+
export test = (name, fn) ->
|
|
21
|
+
try
|
|
22
|
+
result = fn()
|
|
23
|
+
await result if result?.then
|
|
24
|
+
passed++
|
|
25
|
+
p " ✓ #{name}"
|
|
26
|
+
catch e
|
|
27
|
+
failed++
|
|
28
|
+
failures.push { name, error: e.message or String(e) }
|
|
29
|
+
p " ✗ #{name}: #{e.message or e}"
|
|
30
|
+
|
|
31
|
+
export eq = (actual, expected) ->
|
|
32
|
+
a = JSON.stringify(actual)
|
|
33
|
+
b = JSON.stringify(expected)
|
|
34
|
+
throw new Error("expected #{b}, got #{a}") unless a is b
|
|
35
|
+
|
|
36
|
+
export ok = (value, msg) ->
|
|
37
|
+
throw new Error(msg or "expected truthy, got #{JSON.stringify(value)}") unless value
|
|
38
|
+
|
|
39
|
+
export near = (actual, expected, tolerance = 0.001) ->
|
|
40
|
+
throw new Error("expected ~#{expected}, got #{actual}") unless Math.abs(actual - expected) < tolerance
|
|
41
|
+
|
|
42
|
+
# --- Runner ---
|
|
43
|
+
|
|
44
|
+
testDir = dirname(import.meta.path)
|
|
45
|
+
files = readdirSync(testDir).filter((f) -> f.endsWith('.rip') and f isnt 'runner.rip').sort()
|
|
46
|
+
|
|
47
|
+
p "rip-server tests: #{files.length} files\n"
|
|
48
|
+
|
|
49
|
+
for file in files
|
|
50
|
+
p " #{file}"
|
|
51
|
+
try
|
|
52
|
+
import!(join(testDir, file))
|
|
53
|
+
catch e
|
|
54
|
+
failed++
|
|
55
|
+
failures.push { name: "#{file} (load)", error: e.message or String(e) }
|
|
56
|
+
p " ✗ #{file} failed to load: #{e.message or e}"
|
|
57
|
+
p ''
|
|
58
|
+
|
|
59
|
+
# --- Summary ---
|
|
60
|
+
|
|
61
|
+
total = passed + failed
|
|
62
|
+
pct = if total > 0 then Math.round(passed / total * 100) else 0
|
|
63
|
+
p "✓ #{passed} passing, ✗ #{failed} failing, ★ #{pct}% passing"
|
|
64
|
+
|
|
65
|
+
if failures.length > 0
|
|
66
|
+
p "\nFailures:"
|
|
67
|
+
for f in failures[0...20]
|
|
68
|
+
p " ✗ #{f.name}: #{f.error}"
|
|
69
|
+
|
|
70
|
+
exit if failed > 0 then 1 else 0
|