@rip-lang/server 1.3.126 → 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.
Files changed (69) hide show
  1. package/{docs/READ_VALIDATORS.md → API.md} +41 -119
  2. package/CONFIG.md +408 -0
  3. package/README.md +246 -1109
  4. package/acme/crypto.rip +0 -2
  5. package/browse.rip +62 -0
  6. package/control/cli.rip +89 -34
  7. package/control/lifecycle.rip +67 -1
  8. package/control/manager.rip +250 -0
  9. package/control/mdns.rip +3 -0
  10. package/middleware.rip +1 -1
  11. package/package.json +14 -11
  12. package/server.rip +189 -673
  13. package/serving/config.rip +766 -0
  14. package/{edge → serving}/forwarding.rip +2 -2
  15. package/serving/logging.rip +101 -0
  16. package/{edge → serving}/metrics.rip +29 -1
  17. package/serving/proxy.rip +99 -0
  18. package/{edge → serving}/queue.rip +1 -1
  19. package/{edge → serving}/ratelimit.rip +1 -1
  20. package/{edge → serving}/realtime.rip +71 -2
  21. package/{edge → serving}/registry.rip +1 -1
  22. package/{edge → serving}/router.rip +3 -3
  23. package/{edge → serving}/runtime.rip +18 -16
  24. package/{edge → serving}/security.rip +1 -1
  25. package/serving/static.rip +393 -0
  26. package/{edge → serving}/tls.rip +3 -7
  27. package/{edge → serving}/upstream.rip +4 -4
  28. package/{edge → serving}/verify.rip +16 -16
  29. package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
  30. package/streams/config.rip +8 -8
  31. package/streams/index.rip +5 -5
  32. package/streams/router.rip +2 -2
  33. package/tests/acme.rip +1 -1
  34. package/tests/config.rip +215 -0
  35. package/tests/control.rip +1 -1
  36. package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
  37. package/tests/extracted.rip +118 -0
  38. package/tests/helpers.rip +4 -4
  39. package/tests/metrics.rip +3 -3
  40. package/tests/proxy.rip +9 -8
  41. package/tests/read.rip +1 -1
  42. package/tests/realtime.rip +3 -3
  43. package/tests/registry.rip +4 -4
  44. package/tests/router.rip +27 -27
  45. package/tests/runner.rip +70 -0
  46. package/tests/security.rip +4 -4
  47. package/tests/servers.rip +102 -136
  48. package/tests/static.rip +2 -2
  49. package/tests/streams_clienthello.rip +2 -2
  50. package/tests/streams_index.rip +4 -4
  51. package/tests/streams_pipe.rip +1 -1
  52. package/tests/streams_router.rip +10 -10
  53. package/tests/streams_runtime.rip +4 -4
  54. package/tests/streams_upstream.rip +1 -1
  55. package/tests/upstream.rip +2 -2
  56. package/tests/verify.rip +18 -18
  57. package/tests/watchers.rip +4 -4
  58. package/default.rip +0 -435
  59. package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
  60. package/docs/edge/CONTRACTS.md +0 -137
  61. package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
  62. package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
  63. package/docs/edge/SCHEDULER.md +0 -46
  64. package/docs/logo.png +0 -0
  65. package/docs/logo.svg +0 -13
  66. package/docs/social.png +0 -0
  67. package/edge/config.rip +0 -607
  68. package/edge/static.rip +0 -69
  69. package/tests/edgefile.rip +0 -165
@@ -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,4 +1,4 @@
1
- import { test, eq, ok } from '../test.rip'
1
+ import { test, eq, ok } from './runner.rip'
2
2
  import { handleReloadControl } from '../control/control.rip'
3
3
 
4
4
  test "handleReloadControl ignores non-POST requests", ->
@@ -1,12 +1,16 @@
1
- import { test, eq, ok } from '../test.rip'
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 { createEdgeRuntime, createReloadHistoryEntry } from '../edge/runtime.rip'
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', '--edgefile=./Edgefile.rip', 'packages/server/default.rip'])
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.edgefilePath.endsWith('Edgefile.rip')
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', 'packages/server/default.rip@ola'])
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 "edge runtime helpers load and create summary entries", ->
34
- runtime = createEdgeRuntime()
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
- # test-helpers.rip — edge/helpers and forwarding response factory tests
1
+ # helpers.rip — serving/forwarding response factory tests
2
2
 
3
- import { test, eq, ok } from '../test.rip'
4
- import { formatPort, maybeAddSecurityHeaders, buildStatusBody, buildRipdevUrl } from '../edge/forwarding.rip'
5
- import { watchUnavailableResponse, serverBusyResponse, serviceUnavailableResponse, gatewayTimeoutResponse, queueTimeoutResponse } from '../edge/forwarding.rip'
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 — edge/metrics.rip tests
2
+ # test-metrics.rip — serving/metrics.rip tests
3
3
  # ==============================================================================
4
4
 
5
- import { test, eq, ok, near } from '../test.rip'
6
- import { createMetrics } from '../edge/metrics.rip'
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 '../test.rip'
4
- import { normalizeAppConfig, applyConfig, applyEdgeConfig } from '../edge/config.rip'
5
- import { createAppRegistry, registerApp, resolveHost, getAppState } from '../edge/registry.rip'
6
- import { broadcastBinary, createHub, addClient } from '../edge/realtime.rip'
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 "applyEdgeConfig preserves managed app entry workers and env", ->
53
+ test "applyConfig preserves managed app entry workers and env", ->
53
54
  registry = createAppRegistry()
54
55
  config =
55
- kind: 'edge'
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 = applyEdgeConfig(config, registry, registerApp, '/srv')
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/test/tests/read.rip
10
+ # Run: ./bin/rip packages/server/tests/read.rip
11
11
  #
12
12
 
13
13
  import { read, requestContext } from '../api.rip'
@@ -1,7 +1,7 @@
1
- # realtime.rip — edge/realtime.rip unit tests
1
+ # realtime.rip — serving/realtime.rip unit tests
2
2
 
3
- import { test, eq, ok } from '../test.rip'
4
- import { createHub, generateClientId, addClient, removeClient, processResponse, handlePublish, getRealtimeStats } from '../edge/realtime.rip'
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
 
@@ -1,8 +1,8 @@
1
- # test-registry.rip — edge/registry, scheduler, and queue tests
1
+ # test-registry.rip — serving/registry, scheduler, and queue tests
2
2
 
3
- import { test, eq, ok } from '../test.rip'
4
- import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from '../edge/registry.rip'
5
- import { isCurrentVersion, getNextAvailableSocket, releaseWorker, shouldRetryBodylessBusy, drainQueueOnce } from '../edge/queue.rip'
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 '../test.rip'
2
- import { compileRouteTable, matchRoute, describeRoute } from '../edge/router.rip'
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', upstream: 'app' }
7
- { id: 'two', path: '/b', upstream: 'app' }
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: '/*', upstream: 'fallback' }
24
- { host: 'api.example.com', path: '/*', upstream: 'api' }
23
+ { path: '/*', proxy: 'fallback' }
24
+ { host: 'api.example.com', path: '/*', proxy: 'api' }
25
25
  ])
26
- eq matchRoute(table, 'api.example.com', '/users').upstream, 'api'
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: '/*', upstream: 'fallback' }
31
- { host: '*.example.com', path: '/*', upstream: 'wild' }
30
+ { path: '/*', proxy: 'fallback' }
31
+ { host: '*.example.com', path: '/*', proxy: 'wild' }
32
32
  ])
33
- eq matchRoute(table, 'api.example.com', '/users').upstream, 'wild'
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: '/*', upstream: 'wild' }
38
- { path: '/*', upstream: 'fallback' }
37
+ { host: '*.example.com', path: '/*', proxy: 'wild' }
38
+ { path: '/*', proxy: 'fallback' }
39
39
  ])
40
- eq matchRoute(table, 'a.b.example.com', '/users').upstream, 'fallback'
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/*', upstream: 'api' }
45
- { path: '/api/users', upstream: 'users' }
44
+ { path: '/api/*', proxy: 'api' }
45
+ { path: '/api/users', proxy: 'users' }
46
46
  ])
47
- eq matchRoute(table, 'example.com', '/api/users').upstream, '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/*', upstream: 'api' }
52
- { path: '/api/admin/*', upstream: 'admin' }
51
+ { path: '/api/*', proxy: 'api' }
52
+ { path: '/api/admin/*', proxy: 'admin' }
53
53
  ])
54
- eq matchRoute(table, 'example.com', '/api/admin/users').upstream, 'admin'
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/*', upstream: 'one', priority: 10 }
59
- { path: '/api/*', upstream: 'two', priority: 5 }
58
+ { path: '/api/*', proxy: 'one', priority: 10 }
59
+ { path: '/api/*', proxy: 'two', priority: 5 }
60
60
  ])
61
- eq matchRoute(table, 'example.com', '/api/x').upstream, 'two'
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', upstream: 'read', methods: ['GET'] }
66
- { path: '/submit', upstream: 'write', methods: ['POST'] }
65
+ { path: '/submit', proxy: 'read', methods: ['GET'] }
66
+ { path: '/submit', proxy: 'write', methods: ['POST'] }
67
67
  ])
68
- eq matchRoute(table, 'example.com', '/submit', 'POST').upstream, 'write'
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/*', upstream: '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/*', upstream: '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')
@@ -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