braid-text 0.2.77 ā 0.2.79
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/index.js +62 -60
- package/package.json +1 -1
- package/test/fuzz-test.js +12 -8
- package/test/test.js +306 -0
- package/test/tests.js +78 -26
- package/.claude/settings.local.json +0 -14
package/index.js
CHANGED
|
@@ -17,25 +17,37 @@ function create_braid_text() {
|
|
|
17
17
|
let max_encoded_key_size = 240
|
|
18
18
|
|
|
19
19
|
braid_text.sync = async (a, b, options = {}) => {
|
|
20
|
-
var unsync_cbs = []
|
|
21
|
-
options.my_unsync = () => unsync_cbs.forEach(cb => cb())
|
|
22
|
-
|
|
23
20
|
if (!options.merge_type) options.merge_type = 'dt'
|
|
24
21
|
|
|
25
22
|
if ((a instanceof URL) === (b instanceof URL)) {
|
|
23
|
+
// Both are URLs or both are local keys
|
|
24
|
+
var a_first_put, b_first_put
|
|
25
|
+
var a_first_put_promise = new Promise(done => a_first_put = done)
|
|
26
|
+
var b_first_put_promise = new Promise(done => b_first_put = done)
|
|
27
|
+
|
|
26
28
|
var a_ops = {
|
|
27
|
-
|
|
29
|
+
signal: options.signal,
|
|
30
|
+
subscribe: update => {
|
|
31
|
+
update.signal = options.signal
|
|
32
|
+
braid_text.put(b, update).then(a_first_put)
|
|
33
|
+
},
|
|
28
34
|
merge_type: options.merge_type,
|
|
29
35
|
}
|
|
30
|
-
braid_text.get(a, a_ops)
|
|
31
|
-
|
|
36
|
+
braid_text.get(a, a_ops).then(x =>
|
|
37
|
+
x || b_first_put_promise.then(() =>
|
|
38
|
+
braid_text.get(a, a_ops)))
|
|
32
39
|
|
|
33
40
|
var b_ops = {
|
|
34
|
-
|
|
41
|
+
signal: options.signal,
|
|
42
|
+
subscribe: update => {
|
|
43
|
+
update.signal = options.signal
|
|
44
|
+
braid_text.put(a, update).then(b_first_put)
|
|
45
|
+
},
|
|
35
46
|
merge_type: options.merge_type,
|
|
36
47
|
}
|
|
37
|
-
braid_text.get(b, b_ops)
|
|
38
|
-
|
|
48
|
+
braid_text.get(b, b_ops).then(x =>
|
|
49
|
+
x || a_first_put_promise.then(() =>
|
|
50
|
+
braid_text.get(b, b_ops)))
|
|
39
51
|
} else {
|
|
40
52
|
// make a=local and b=remote (swap if not)
|
|
41
53
|
if (a instanceof URL) { let swap = a; a = b; b = swap }
|
|
@@ -78,12 +90,22 @@ function create_braid_text() {
|
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
var closed
|
|
81
|
-
var disconnect
|
|
82
|
-
|
|
93
|
+
var disconnect = () => {}
|
|
94
|
+
options.signal?.addEventListener('abort', () => {
|
|
83
95
|
closed = true
|
|
84
96
|
disconnect()
|
|
85
97
|
})
|
|
86
98
|
|
|
99
|
+
var local_first_put
|
|
100
|
+
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
101
|
+
|
|
102
|
+
function handle_error(_e) {
|
|
103
|
+
if (closed) return
|
|
104
|
+
disconnect()
|
|
105
|
+
console.log(`disconnected, retrying in 1 second`)
|
|
106
|
+
setTimeout(connect, 1000)
|
|
107
|
+
}
|
|
108
|
+
|
|
87
109
|
connect()
|
|
88
110
|
async function connect() {
|
|
89
111
|
if (options.on_connect) options.on_connect()
|
|
@@ -91,9 +113,7 @@ function create_braid_text() {
|
|
|
91
113
|
if (closed) return
|
|
92
114
|
|
|
93
115
|
var ac = new AbortController()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
disconnect = () => disconnect_cbs.forEach(cb => cb())
|
|
116
|
+
disconnect = () => ac.abort()
|
|
97
117
|
|
|
98
118
|
try {
|
|
99
119
|
// fork-point
|
|
@@ -142,9 +162,11 @@ function create_braid_text() {
|
|
|
142
162
|
|
|
143
163
|
// local -> remote
|
|
144
164
|
var a_ops = {
|
|
165
|
+
signal: ac.signal,
|
|
145
166
|
subscribe: update => {
|
|
146
167
|
update.signal = ac.signal
|
|
147
168
|
braid_text.put(b, update).then((x) => {
|
|
169
|
+
local_first_put()
|
|
148
170
|
extend_fork_point(update)
|
|
149
171
|
}).catch(e => {
|
|
150
172
|
if (e.name === 'AbortError') {
|
|
@@ -155,28 +177,31 @@ function create_braid_text() {
|
|
|
155
177
|
}
|
|
156
178
|
if (resource.meta.fork_point)
|
|
157
179
|
a_ops.parents = resource.meta.fork_point
|
|
158
|
-
disconnect_cbs.push(() => braid_text.forget(a, a_ops))
|
|
159
180
|
braid_text.get(a, a_ops)
|
|
160
181
|
|
|
161
182
|
// remote -> local
|
|
162
183
|
var b_ops = {
|
|
184
|
+
signal: ac.signal,
|
|
163
185
|
dont_retry: true,
|
|
164
186
|
subscribe: async update => {
|
|
165
187
|
await braid_text.put(a, update)
|
|
166
188
|
extend_fork_point(update)
|
|
167
189
|
},
|
|
190
|
+
on_error: handle_error
|
|
191
|
+
}
|
|
192
|
+
// Handle case where remote doesn't exist yet - wait for local to create it
|
|
193
|
+
var remote_result = await braid_text.get(b, b_ops)
|
|
194
|
+
if (remote_result === null) {
|
|
195
|
+
// Remote doesn't exist yet, wait for local to put something
|
|
196
|
+
await local_first_put_promise
|
|
197
|
+
disconnect()
|
|
198
|
+
connect()
|
|
168
199
|
}
|
|
169
|
-
|
|
170
|
-
// NOTE: this should not return, but it might throw
|
|
171
|
-
await braid_text.get(b, b_ops)
|
|
200
|
+
// on_error will call handle_error when connection drops
|
|
172
201
|
} catch (e) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
disconnect()
|
|
176
|
-
console.log(`disconnected, retrying in 1 second`)
|
|
177
|
-
setTimeout(connect, 1000)
|
|
202
|
+
handle_error(e)
|
|
178
203
|
}
|
|
179
|
-
}
|
|
204
|
+
}
|
|
180
205
|
}
|
|
181
206
|
}
|
|
182
207
|
|
|
@@ -468,15 +493,9 @@ function create_braid_text() {
|
|
|
468
493
|
|
|
469
494
|
// Handle URL - make a DELETE request
|
|
470
495
|
if (key instanceof URL) {
|
|
471
|
-
options.my_abort = new AbortController()
|
|
472
|
-
if (options.signal)
|
|
473
|
-
options.signal.addEventListener('abort', () =>
|
|
474
|
-
options.my_abort.abort())
|
|
475
|
-
|
|
476
496
|
var params = {
|
|
477
497
|
method: 'DELETE',
|
|
478
|
-
signal: options.
|
|
479
|
-
retry: () => true,
|
|
498
|
+
signal: options.signal,
|
|
480
499
|
}
|
|
481
500
|
for (var x of ['headers', 'peer'])
|
|
482
501
|
if (options[x] != null) params[x] = options[x]
|
|
@@ -502,33 +521,29 @@ function create_braid_text() {
|
|
|
502
521
|
if (key instanceof URL) {
|
|
503
522
|
if (!options) options = {}
|
|
504
523
|
|
|
505
|
-
options.my_abort = new AbortController()
|
|
506
|
-
|
|
507
524
|
var params = {
|
|
508
|
-
signal: options.
|
|
525
|
+
signal: options.signal,
|
|
509
526
|
subscribe: !!options.subscribe,
|
|
510
527
|
heartbeats: 120,
|
|
511
528
|
}
|
|
512
|
-
if (!options.dont_retry)
|
|
529
|
+
if (!options.dont_retry) {
|
|
530
|
+
params.retry = (res) => res.status !== 404
|
|
531
|
+
}
|
|
513
532
|
for (var x of ['headers', 'parents', 'version', 'peer'])
|
|
514
533
|
if (options[x] != null) params[x] = options[x]
|
|
515
534
|
|
|
516
535
|
var res = await braid_fetch(key.href, params)
|
|
517
536
|
|
|
518
|
-
if (
|
|
519
|
-
if (options.dont_retry) {
|
|
520
|
-
var error_happened
|
|
521
|
-
var error_promise = new Promise((_, fail) => error_happened = fail)
|
|
522
|
-
}
|
|
537
|
+
if (res.status === 404) return null
|
|
523
538
|
|
|
539
|
+
if (options.subscribe) {
|
|
524
540
|
res.subscribe(async update => {
|
|
525
541
|
update.body = update.body_text
|
|
526
542
|
if (update.patches)
|
|
527
543
|
for (var p of update.patches) p.content = p.content_text
|
|
528
544
|
await options.subscribe(update)
|
|
529
|
-
}, e => options.
|
|
545
|
+
}, e => options.on_error?.(e))
|
|
530
546
|
|
|
531
|
-
if (options.dont_retry) return await error_promise
|
|
532
547
|
return res
|
|
533
548
|
} else return await res.text()
|
|
534
549
|
}
|
|
@@ -603,6 +618,8 @@ function create_braid_text() {
|
|
|
603
618
|
|
|
604
619
|
options.my_last_sent_version = x.version
|
|
605
620
|
resource.simpleton_clients.add(options)
|
|
621
|
+
options.signal?.addEventListener('abort', () =>
|
|
622
|
+
resource.simpleton_clients.delete(options))
|
|
606
623
|
} else {
|
|
607
624
|
|
|
608
625
|
if (options.accept_encoding?.match(/updates\s*\((.*)\)/)?.[1].split(',').map(x=>x.trim()).includes('dt')) {
|
|
@@ -650,22 +667,12 @@ function create_braid_text() {
|
|
|
650
667
|
}
|
|
651
668
|
|
|
652
669
|
resource.clients.add(options)
|
|
670
|
+
options.signal?.addEventListener('abort', () =>
|
|
671
|
+
resource.clients.delete(options))
|
|
653
672
|
}
|
|
654
673
|
}
|
|
655
674
|
}
|
|
656
675
|
|
|
657
|
-
braid_text.forget = async (key, options) => {
|
|
658
|
-
if (!options) throw new Error('options is required')
|
|
659
|
-
|
|
660
|
-
if (key instanceof URL) return options.my_abort.abort()
|
|
661
|
-
|
|
662
|
-
let resource = (typeof key == 'string') ? await get_resource(key) : key
|
|
663
|
-
|
|
664
|
-
if (options.merge_type != "dt")
|
|
665
|
-
resource.simpleton_clients.delete(options)
|
|
666
|
-
else resource.clients.delete(options)
|
|
667
|
-
}
|
|
668
|
-
|
|
669
676
|
braid_text.put = async (key, options) => {
|
|
670
677
|
if (options.version) {
|
|
671
678
|
validate_version_array(options.version)
|
|
@@ -677,14 +684,9 @@ function create_braid_text() {
|
|
|
677
684
|
}
|
|
678
685
|
|
|
679
686
|
if (key instanceof URL) {
|
|
680
|
-
options.my_abort = new AbortController()
|
|
681
|
-
if (options.signal)
|
|
682
|
-
options.signal.addEventListener('abort', () =>
|
|
683
|
-
options.my_abort.abort())
|
|
684
|
-
|
|
685
687
|
var params = {
|
|
686
688
|
method: 'PUT',
|
|
687
|
-
signal: options.
|
|
689
|
+
signal: options.signal,
|
|
688
690
|
retry: () => true,
|
|
689
691
|
}
|
|
690
692
|
for (var x of ['headers', 'parents', 'version', 'peer', 'body', 'patches'])
|
package/package.json
CHANGED
package/test/fuzz-test.js
CHANGED
|
@@ -270,16 +270,20 @@ async function main() {
|
|
|
270
270
|
|
|
271
271
|
// try getting updates from middle_doc to doc
|
|
272
272
|
if (!v_eq(middle_v, doc_v)) {
|
|
273
|
-
var
|
|
273
|
+
var ac = new AbortController()
|
|
274
274
|
await new Promise(async done => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
275
|
+
braid_text.get('doc', {
|
|
276
|
+
signal: ac.signal,
|
|
277
|
+
merge_type: 'dt',
|
|
278
|
+
parents: middle_v,
|
|
279
|
+
subscribe: async update => {
|
|
280
|
+
await braid_text.put('middle_doc', update)
|
|
281
|
+
middle_v = (await braid_text.get_resource('middle_doc')).version
|
|
282
|
+
if (v_eq(doc_v, middle_v)) done()
|
|
283
|
+
}
|
|
284
|
+
})
|
|
281
285
|
})
|
|
282
|
-
|
|
286
|
+
ac.abort()
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
if (await braid_text.get('middle_doc') != await braid_text.get('doc')) throw new Error('bad')
|
package/test/test.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Unified test runner - can run in console mode (Node.js) or browser mode (server)
|
|
4
|
+
const http = require('http')
|
|
5
|
+
const {fetch: braid_fetch} = require('braid-http')
|
|
6
|
+
const defineTests = require('./tests.js')
|
|
7
|
+
|
|
8
|
+
// Parse command line arguments
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
const mode = args.includes('--browser') || args.includes('-b') ? 'browser' : 'console'
|
|
11
|
+
const portArg = args.find(arg => arg.startsWith('--port='))?.split('=')[1]
|
|
12
|
+
|| args.find(arg => !arg.startsWith('-') && !isNaN(arg))
|
|
13
|
+
const port = portArg || 8889
|
|
14
|
+
const filterArg = args.find(arg => arg.startsWith('--filter='))?.split('=')[1]
|
|
15
|
+
|| args.find(arg => arg.startsWith('--grep='))?.split('=')[1]
|
|
16
|
+
|
|
17
|
+
// Show help if requested
|
|
18
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: node test.js [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--browser, -b Start server for browser testing (default: console mode)
|
|
24
|
+
--port=PORT Specify port number (default: 8889)
|
|
25
|
+
PORT Port number as positional argument
|
|
26
|
+
--filter=PATTERN Only run tests matching pattern (case-insensitive)
|
|
27
|
+
--grep=PATTERN Alias for --filter
|
|
28
|
+
--help, -h Show this help message
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
node test.js # Run all tests in console
|
|
32
|
+
node test.js --filter="sync" # Run only tests with "sync" in name
|
|
33
|
+
node test.js --grep="digest" # Run only tests with "digest" in name
|
|
34
|
+
node test.js --browser # Start browser test server
|
|
35
|
+
node test.js --browser --port=9000
|
|
36
|
+
node test.js -b 9000 # Short form with port
|
|
37
|
+
`)
|
|
38
|
+
process.exit(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Shared Server Code
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
function createTestServer(options = {}) {
|
|
46
|
+
const {
|
|
47
|
+
port = 8889,
|
|
48
|
+
runTests = false,
|
|
49
|
+
logRequests = false
|
|
50
|
+
} = options
|
|
51
|
+
|
|
52
|
+
const braid_text = require(`${__dirname}/../index.js`)
|
|
53
|
+
braid_text.db_folder = `${__dirname}/test_db_folder`
|
|
54
|
+
|
|
55
|
+
const braid_text2 = braid_text.create_braid_text()
|
|
56
|
+
braid_text2.db_folder = null
|
|
57
|
+
|
|
58
|
+
const server = http.createServer(async (req, res) => {
|
|
59
|
+
if (logRequests) {
|
|
60
|
+
console.log(`${req.method} ${req.url}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Free the CORS
|
|
64
|
+
braid_text.free_cors(res)
|
|
65
|
+
if (req.method === 'OPTIONS') return
|
|
66
|
+
|
|
67
|
+
if (req.url.startsWith('/have_error')) {
|
|
68
|
+
res.statusCode = 569
|
|
69
|
+
return res.end('error')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (req.url.startsWith('/404')) {
|
|
73
|
+
res.statusCode = 404
|
|
74
|
+
return res.end('Not Found')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (req.url.startsWith('/eval')) {
|
|
78
|
+
var body = await new Promise(done => {
|
|
79
|
+
var chunks = []
|
|
80
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
81
|
+
req.on('end', () => done(Buffer.concat(chunks)))
|
|
82
|
+
})
|
|
83
|
+
try {
|
|
84
|
+
eval('' + body)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
87
|
+
res.end(`Error: ${error.message}`)
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (req.url.startsWith('/test.html')) {
|
|
93
|
+
let parts = req.url.split(/[\?&=]/g)
|
|
94
|
+
|
|
95
|
+
if (parts[1] === 'check') {
|
|
96
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" })
|
|
97
|
+
return res.end(JSON.stringify({
|
|
98
|
+
checking: parts[2],
|
|
99
|
+
result: (await braid_text.get(parts[2])) != null
|
|
100
|
+
}))
|
|
101
|
+
} else if (parts[1] === 'dt_create_bytes_big_name') {
|
|
102
|
+
try {
|
|
103
|
+
braid_text.dt_create_bytes('x'.repeat(1000000) + '-0', [], 0, 0, 'hi')
|
|
104
|
+
return res.end(JSON.stringify({ ok: true }))
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return res.end(JSON.stringify({ ok: false, error: '' + e }))
|
|
107
|
+
}
|
|
108
|
+
} else if (parts[1] === 'dt_create_bytes_many_names') {
|
|
109
|
+
try {
|
|
110
|
+
braid_text.dt_create_bytes('hi-0', new Array(1000000).fill(0).map((x, i) => `x${i}-0`), 0, 0, 'hi')
|
|
111
|
+
return res.end(JSON.stringify({ ok: true }))
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return res.end(JSON.stringify({ ok: false, error: '' + e }))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" })
|
|
118
|
+
require("fs").createReadStream(`${__dirname}/test.html`).pipe(res)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Serve tests.js file for browser
|
|
123
|
+
if (req.url.startsWith('/tests.js')) {
|
|
124
|
+
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" })
|
|
125
|
+
require("fs").createReadStream(`${__dirname}/tests.js`).pipe(res)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Now serve the collaborative text!
|
|
130
|
+
braid_text.serve(req, res)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
server,
|
|
135
|
+
start: () => new Promise((resolve) => {
|
|
136
|
+
server.listen(port, 'localhost', () => {
|
|
137
|
+
if (runTests) {
|
|
138
|
+
console.log(`Test server running on http://localhost:${port}`)
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`serving: http://localhost:${port}/test.html`)
|
|
141
|
+
}
|
|
142
|
+
resolve()
|
|
143
|
+
})
|
|
144
|
+
}),
|
|
145
|
+
port
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Console Test Mode (Node.js)
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
async function runConsoleTests() {
|
|
154
|
+
// Test tracking
|
|
155
|
+
let totalTests = 0
|
|
156
|
+
let passedTests = 0
|
|
157
|
+
let failedTests = 0
|
|
158
|
+
|
|
159
|
+
// Handle unhandled rejections during tests (some tests intentionally cause errors)
|
|
160
|
+
const unhandledRejections = []
|
|
161
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
162
|
+
// Collect but don't crash - some tests intentionally trigger errors
|
|
163
|
+
unhandledRejections.push({ reason, promise })
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Node.js test runner implementation
|
|
167
|
+
// Store tests to run sequentially instead of in parallel
|
|
168
|
+
const testsToRun = []
|
|
169
|
+
|
|
170
|
+
function runTest(testName, testFunction, expectedResult) {
|
|
171
|
+
// Apply filter if specified
|
|
172
|
+
if (filterArg && !testName.toLowerCase().includes(filterArg.toLowerCase())) {
|
|
173
|
+
return // Skip this test
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
totalTests++
|
|
177
|
+
testsToRun.push({ testName, testFunction, expectedResult })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create a braid_fetch wrapper that points to localhost
|
|
181
|
+
function createBraidFetch(baseUrl) {
|
|
182
|
+
return async (url, options = {}) => {
|
|
183
|
+
const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
|
|
184
|
+
return braid_fetch(fullUrl, options)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Starting braid-text tests...\n')
|
|
189
|
+
|
|
190
|
+
// Create and start the test server
|
|
191
|
+
const testServer = createTestServer({
|
|
192
|
+
port,
|
|
193
|
+
runTests: true,
|
|
194
|
+
logRequests: false
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
await testServer.start()
|
|
198
|
+
|
|
199
|
+
// Create braid_fetch bound to test server
|
|
200
|
+
const testBraidFetch = createBraidFetch(`http://localhost:${port}`)
|
|
201
|
+
|
|
202
|
+
// Load the real diamond-types module for Node.js
|
|
203
|
+
const { Doc, OpLog } = require('@braid.org/diamond-types-node')
|
|
204
|
+
|
|
205
|
+
// Define globals needed for some tests
|
|
206
|
+
global.Doc = Doc
|
|
207
|
+
global.OpLog = OpLog
|
|
208
|
+
global.dt_p = Promise.resolve() // No initialization needed for Node.js version
|
|
209
|
+
global.fetch = testBraidFetch
|
|
210
|
+
global.AbortController = AbortController
|
|
211
|
+
global.crypto = require('crypto').webcrypto
|
|
212
|
+
|
|
213
|
+
// Run all tests
|
|
214
|
+
defineTests(runTest, testBraidFetch)
|
|
215
|
+
|
|
216
|
+
// Run tests sequentially (not in parallel) to avoid conflicts
|
|
217
|
+
for (const { testName, testFunction, expectedResult } of testsToRun) {
|
|
218
|
+
try {
|
|
219
|
+
const result = await testFunction()
|
|
220
|
+
if (result == expectedResult) {
|
|
221
|
+
passedTests++
|
|
222
|
+
console.log(`ā ${testName}`)
|
|
223
|
+
} else {
|
|
224
|
+
failedTests++
|
|
225
|
+
console.log(`ā ${testName}`)
|
|
226
|
+
console.log(` Expected: ${expectedResult}`)
|
|
227
|
+
console.log(` Got: ${result}`)
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
failedTests++
|
|
231
|
+
console.log(`ā ${testName}`)
|
|
232
|
+
console.log(` Error: ${error.message || error}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Print summary
|
|
237
|
+
console.log('\n' + '='.repeat(50))
|
|
238
|
+
console.log(`Total: ${totalTests} | Passed: ${passedTests} | Failed: ${failedTests}`)
|
|
239
|
+
console.log('='.repeat(50))
|
|
240
|
+
|
|
241
|
+
// Clean up test database folder
|
|
242
|
+
console.log('Cleaning up test database folder...')
|
|
243
|
+
const fs = require('fs')
|
|
244
|
+
const path = require('path')
|
|
245
|
+
const testDbPath = path.join(__dirname, 'test_db_folder')
|
|
246
|
+
try {
|
|
247
|
+
if (fs.existsSync(testDbPath)) {
|
|
248
|
+
fs.rmSync(testDbPath, { recursive: true, force: true })
|
|
249
|
+
console.log('Test database folder removed')
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.log(`Warning: Could not remove test database folder: ${err.message}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Force close the server and all connections
|
|
256
|
+
console.log('Closing server...')
|
|
257
|
+
testServer.server.close(() => {
|
|
258
|
+
console.log('Server closed callback - calling process.exit()')
|
|
259
|
+
process.exit(failedTests > 0 ? 1 : 0)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Also close all active connections if the method exists (Node 18.2+)
|
|
263
|
+
if (typeof testServer.server.closeAllConnections === 'function') {
|
|
264
|
+
console.log('Closing all connections...')
|
|
265
|
+
testServer.server.closeAllConnections()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback: force exit after a short delay even if server hasn't fully closed
|
|
269
|
+
console.log('Setting 200ms timeout fallback...')
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
console.log('Timeout reached - calling process.exit()')
|
|
272
|
+
process.exit(failedTests > 0 ? 1 : 0)
|
|
273
|
+
}, 200)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Browser Test Mode (Server)
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
async function runBrowserMode() {
|
|
281
|
+
const testServer = createTestServer({
|
|
282
|
+
port,
|
|
283
|
+
runTests: false,
|
|
284
|
+
logRequests: true
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
await testServer.start()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Main Entry Point
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
async function main() {
|
|
295
|
+
if (mode === 'browser') {
|
|
296
|
+
await runBrowserMode()
|
|
297
|
+
} else {
|
|
298
|
+
await runConsoleTests()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Run the appropriate mode
|
|
303
|
+
main().catch(err => {
|
|
304
|
+
console.error('Fatal error:', err)
|
|
305
|
+
process.exit(1)
|
|
306
|
+
})
|
package/test/tests.js
CHANGED
|
@@ -14,31 +14,24 @@ runTest(
|
|
|
14
14
|
})
|
|
15
15
|
if (!r.ok) return 'got: ' + r.status
|
|
16
16
|
|
|
17
|
-
var
|
|
17
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
18
18
|
method: 'PUT',
|
|
19
19
|
body: `void (async () => {
|
|
20
20
|
var x = await new Promise(done => {
|
|
21
21
|
braid_text.get(new URL('http://localhost:8889/${key}'), {
|
|
22
22
|
subscribe: update => {
|
|
23
|
-
if (update.body_text === '
|
|
23
|
+
if (update.body_text === 'hi') done(update.body_text)
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
})
|
|
27
27
|
res.end(x)
|
|
28
28
|
})()`
|
|
29
29
|
})
|
|
30
|
-
|
|
31
|
-
var r2 = await braid_fetch(`/${key}`, {
|
|
32
|
-
method: 'PUT',
|
|
33
|
-
body: 'yo'
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
var r1 = await r1p
|
|
37
30
|
if (!r1.ok) return 'got: ' + r.status
|
|
38
31
|
|
|
39
32
|
return await r1.text()
|
|
40
33
|
},
|
|
41
|
-
'
|
|
34
|
+
'hi'
|
|
42
35
|
)
|
|
43
36
|
|
|
44
37
|
runTest(
|
|
@@ -50,13 +43,14 @@ runTest(
|
|
|
50
43
|
method: 'PUT',
|
|
51
44
|
body: `void (async () => {
|
|
52
45
|
var count = 0
|
|
53
|
-
var
|
|
54
|
-
braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'),
|
|
46
|
+
var ac = new AbortController()
|
|
47
|
+
braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'), {
|
|
48
|
+
signal: ac.signal,
|
|
55
49
|
on_connect: () => {
|
|
56
50
|
count++
|
|
57
51
|
if (count === 2) {
|
|
58
52
|
res.end('it reconnected!')
|
|
59
|
-
|
|
53
|
+
ac.abort()
|
|
60
54
|
}
|
|
61
55
|
}
|
|
62
56
|
})
|
|
@@ -201,13 +195,14 @@ runTest(
|
|
|
201
195
|
method: 'PUT',
|
|
202
196
|
body: `void (async () => {
|
|
203
197
|
var count = 0
|
|
204
|
-
var
|
|
205
|
-
braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'),
|
|
198
|
+
var ac = new AbortController()
|
|
199
|
+
braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'), {
|
|
200
|
+
signal: ac.signal,
|
|
206
201
|
on_connect: () => {
|
|
207
202
|
count++
|
|
208
203
|
if (count === 2) {
|
|
209
204
|
res.end('it reconnected!')
|
|
210
|
-
|
|
205
|
+
ac.abort()
|
|
211
206
|
}
|
|
212
207
|
}
|
|
213
208
|
})
|
|
@@ -265,10 +260,10 @@ runTest(
|
|
|
265
260
|
var r = await braid_fetch(`/eval`, {
|
|
266
261
|
method: 'PUT',
|
|
267
262
|
body: `void (async () => {
|
|
268
|
-
var
|
|
269
|
-
braid_text.sync('/${key_a}', '/${key_b}',
|
|
263
|
+
var ac = new AbortController()
|
|
264
|
+
braid_text.sync('/${key_a}', '/${key_b}', {signal: ac.signal})
|
|
270
265
|
await new Promise(done => setTimeout(done, 100))
|
|
271
|
-
|
|
266
|
+
ac.abort()
|
|
272
267
|
res.end('')
|
|
273
268
|
})()`
|
|
274
269
|
})
|
|
@@ -283,10 +278,10 @@ runTest(
|
|
|
283
278
|
var r = await braid_fetch(`/eval`, {
|
|
284
279
|
method: 'PUT',
|
|
285
280
|
body: `void (async () => {
|
|
286
|
-
var
|
|
287
|
-
braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'),
|
|
281
|
+
var ac = new AbortController()
|
|
282
|
+
braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'), {signal: ac.signal})
|
|
288
283
|
await new Promise(done => setTimeout(done, 100))
|
|
289
|
-
|
|
284
|
+
ac.abort()
|
|
290
285
|
res.end('')
|
|
291
286
|
})()`
|
|
292
287
|
})
|
|
@@ -456,14 +451,15 @@ runTest(
|
|
|
456
451
|
method: 'PUT',
|
|
457
452
|
body: `void (async () => {
|
|
458
453
|
var url = new URL('http://localhost:8889/${key}')
|
|
454
|
+
var ac = new AbortController()
|
|
459
455
|
var update = await new Promise(done => {
|
|
460
|
-
|
|
456
|
+
braid_text.get(url, {
|
|
457
|
+
signal: ac.signal,
|
|
461
458
|
subscribe: update => {
|
|
462
|
-
|
|
459
|
+
ac.abort()
|
|
463
460
|
done(update)
|
|
464
461
|
}
|
|
465
|
-
}
|
|
466
|
-
braid_text.get(url, o)
|
|
462
|
+
})
|
|
467
463
|
})
|
|
468
464
|
res.end(update.body)
|
|
469
465
|
})()`
|
|
@@ -1929,6 +1925,62 @@ runTest(
|
|
|
1929
1925
|
'got: '
|
|
1930
1926
|
)
|
|
1931
1927
|
|
|
1928
|
+
runTest(
|
|
1929
|
+
"test braid_text.get(url) returns null for 404",
|
|
1930
|
+
async () => {
|
|
1931
|
+
// Use the /404 endpoint that always returns 404
|
|
1932
|
+
var r = await braid_fetch(`/eval`, {
|
|
1933
|
+
method: 'PUT',
|
|
1934
|
+
body: `void (async () => {
|
|
1935
|
+
var result = await braid_text.get(new URL('http://localhost:8889/404'))
|
|
1936
|
+
res.end(result === null ? 'null' : 'not null: ' + result)
|
|
1937
|
+
})()`
|
|
1938
|
+
})
|
|
1939
|
+
return await r.text()
|
|
1940
|
+
},
|
|
1941
|
+
'null'
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
runTest(
|
|
1945
|
+
"test braid_text.sync handles remote not existing yet",
|
|
1946
|
+
async () => {
|
|
1947
|
+
var local_key = 'test-local-' + Math.random().toString(36).slice(2)
|
|
1948
|
+
var remote_key = 'test-remote-' + Math.random().toString(36).slice(2)
|
|
1949
|
+
|
|
1950
|
+
// Start sync between a local key and a remote URL that doesn't exist yet
|
|
1951
|
+
// The sync should wait for local to create something, then push to remote
|
|
1952
|
+
var r = await braid_fetch(`/eval`, {
|
|
1953
|
+
method: 'PUT',
|
|
1954
|
+
body: `void (async () => {
|
|
1955
|
+
var ac = new AbortController()
|
|
1956
|
+
|
|
1957
|
+
// Start sync - remote doesn't exist yet
|
|
1958
|
+
braid_text.sync('/${local_key}', new URL('http://localhost:8889/${remote_key}'), {
|
|
1959
|
+
signal: ac.signal
|
|
1960
|
+
})
|
|
1961
|
+
|
|
1962
|
+
// Wait a bit then put something locally
|
|
1963
|
+
await new Promise(done => setTimeout(done, 100))
|
|
1964
|
+
await braid_text.put('/${local_key}', { body: 'created locally' })
|
|
1965
|
+
|
|
1966
|
+
// Wait for sync to propagate
|
|
1967
|
+
await new Promise(done => setTimeout(done, 200))
|
|
1968
|
+
|
|
1969
|
+
// Stop sync
|
|
1970
|
+
ac.abort()
|
|
1971
|
+
|
|
1972
|
+
res.end('done')
|
|
1973
|
+
})()`
|
|
1974
|
+
})
|
|
1975
|
+
if (!r.ok) return 'eval failed: ' + r.status
|
|
1976
|
+
|
|
1977
|
+
// Check that remote now has the content
|
|
1978
|
+
var r2 = await braid_fetch(`/${remote_key}`)
|
|
1979
|
+
return await r2.text()
|
|
1980
|
+
},
|
|
1981
|
+
'created locally'
|
|
1982
|
+
)
|
|
1983
|
+
|
|
1932
1984
|
runTest(
|
|
1933
1985
|
"test getting a binary update from a subscription",
|
|
1934
1986
|
async () => {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(node test/test.js:*)",
|
|
5
|
-
"Bash(node:*)",
|
|
6
|
-
"Bash(git add:*)",
|
|
7
|
-
"Bash(git commit -m \"$(cat <<''EOF''\n0.2.74 - updates url-file-db to 0.0.19\n\nš¤ Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
|
8
|
-
"Bash(git push)",
|
|
9
|
-
"Bash(npm publish:*)"
|
|
10
|
-
],
|
|
11
|
-
"deny": [],
|
|
12
|
-
"ask": []
|
|
13
|
-
}
|
|
14
|
-
}
|