AtlatestRepositoryfolio
1
import assert from 'node:assert/strict';2
import test from 'node:test';3
import { spawn } from 'node:child_process';4
import { once } from 'node:events';5
import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';6
import { createServer } from 'node:net';7
import { tmpdir } from 'node:os';8
import { join, resolve } from 'node:path';10
const binary = resolve(process.env.FOLIO_TEST_BINARY || 'build/dev/bin/folio');11
const lib = resolve(process.env.FOLIO_TEST_LIB || 'build/dev/lib');12
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));14
async function stop(child) {15
if (!child || child.exitCode !== null || child.signalCode !== null) return;16
const exited = once(child, 'exit');17
child.kill('SIGTERM');18
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);19
await exited;20
clearTimeout(timer);21
}23
async function unusedPort() {24
const listener = createServer();25
listener.listen(0, '127.0.0.1');26
await once(listener, 'listening');27
const port = listener.address().port;28
await new Promise(resolve => listener.close(resolve));29
return port;30
}32
test('a read completes while a writer is waiting for another process', { timeout: 30000 }, async t => {33
const root = await mkdtemp(join(tmpdir(), 'folio-http-concurrent-'));34
let server, holder, diagnostic = '';35
const release = join(root, 'release');36
t.after(async () => {37
await writeFile(release, 'release\n').catch(() => {});38
await stop(holder);39
await stop(server);40
await rm(root, { recursive: true, force: true });41
});42
await mkdir(join(root, 'notes'));43
await writeFile(join(root, 'notes/fast.md'), '# Fast\ncommitted before the writer\n');44
await writeFile(join(root, 'notes/writer.md'), '# Writer\n');45
const token = 'test-only-' + 'ab12'.repeat(12);46
const tokenFile = join(root, 'token');47
await writeFile(tokenFile, token + '\n', { mode: 0o600 });48
const port = await unusedPort();49
const url = `http://127.0.0.1:${port}/mcp`;50
server = spawn(binary, ['serve', '--http', String(port), '--token-file', tokenFile], {51
env: { ...process.env, FOLIO_ROOT: root }, stdio: ['ignore', 'pipe', 'pipe'],52
});53
const record = chunk => { diagnostic = (diagnostic + chunk).slice(-12000); };54
server.stdout.on('data', record);55
server.stderr.on('data', record);56
const startup = performance.now();57
for (;;) {58
assert.equal(server.exitCode, null, diagnostic);59
try {60
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });61
await response.text();62
if (response.status === 401) break;63
} catch {}64
assert.ok(performance.now() - startup < 10000, `server never ready: ${diagnostic}`);65
await pause(50);66
}67
let id = 0;68
async function call(name, args) {69
const requestId = ++id;70
const response = await fetch(url, {71
method: 'POST', signal: AbortSignal.timeout(10000),72
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json',73
'MCP-Protocol-Version': '2025-11-25' },74
body: JSON.stringify({ jsonrpc: '2.0', id: requestId, method: 'tools/call',75
params: { name, arguments: args } }),76
});77
const reply = await response.json();78
assert.equal(response.status, 200, JSON.stringify(reply));79
assert.equal(reply.id, requestId);80
assert.ok(!reply.error && !reply.result?.isError, JSON.stringify(reply));81
return reply.result.content.map(item => item.text || '').join('\n');82
}83
assert.match(await call('folio/note-read', { name: 'fast' }), /committed before the writer/);85
const ready = join(root, 'ready');86
holder = spawn('sigil', ['-L', lib, resolve('test/fixtures/hold-store-lock.sgl')], {87
cwd: process.cwd(), env: { ...process.env,88
FOLIO_LOCK_TEST_ROOT: root, FOLIO_LOCK_TEST_READY: ready, FOLIO_LOCK_TEST_RELEASE: release },89
stdio: ['ignore', 'pipe', 'pipe'],90
});91
holder.stdout.on('data', record);92
holder.stderr.on('data', record);93
const lockStart = performance.now();94
for (;;) {95
assert.equal(holder.exitCode, null, diagnostic);96
try { if ((await readFile(ready, 'utf8')) === 'held\n') break; } catch {}97
assert.ok(performance.now() - lockStart < 5000, `writer lock never held: ${diagnostic}`);98
await pause(20);99
}101
let writerFinished = false;102
const marker = 'unique committed append';103
const write = call('folio/note-edit', { name: 'writer', append: marker + '\n' })104
.finally(() => { writerFinished = true; });105
// Attach a handler immediately so a failure during the read is still reported.106
write.catch(() => {});107
await pause(100);108
assert.equal(writerFinished, false, 'the writer must actually be waiting');109
let forcedRelease = false;110
const safety = setTimeout(() => {111
forcedRelease = true;112
writeFile(release, 'release\n').catch(() => {});113
}, 2000);114
let text, latency;115
try {116
const start = performance.now();117
text = await call('folio/note-read', { name: 'fast' });118
latency = performance.now() - start;119
} finally {120
clearTimeout(safety);121
await writeFile(release, 'release\n');122
await write;123
}124
assert.match(text, /committed before the writer/);125
assert.equal(forcedRelease, false, `read waited for the writer lock (${latency} ms)`);126
assert.ok(latency < 750, `independent read took ${latency} ms`);127
const stored = await call('folio/note-read', { name: 'writer' });128
assert.equal(stored.split(marker).length - 1, 1, 'the append must commit exactly once');129
});