blob: 90abef83072ab4686b9fe6cb135a8993c61a7b38 [file]
/**
* Tests for withFileWriteLock — per-key serialization that makes file-mutating
* tools race-free when the AI SDK runs a step's tool calls concurrently.
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { withFileWriteLock } from '../file-write-lock.js';
const tick = () => new Promise<void>((r) => setImmediate(r));
describe('withFileWriteLock', () => {
test('serializes tasks sharing a key, in submission order', async () => {
const events: string[] = [];
const task = (id: string) =>
withFileWriteLock('serialize', async () => {
events.push(`${id}:start`);
await tick();
await tick();
events.push(`${id}:end`);
});
await Promise.all([task('a'), task('b'), task('c')]);
// No interleaving: each task fully completes before the next starts.
assert.deepEqual(events, ['a:start', 'a:end', 'b:start', 'b:end', 'c:start', 'c:end']);
});
test('different keys run concurrently', async () => {
const events: string[] = [];
const task = (key: string, id: string) =>
withFileWriteLock(key, async () => {
events.push(`${id}:start`);
await tick();
events.push(`${id}:end`);
});
await Promise.all([task('concurrent-x', 'a'), task('concurrent-y', 'b')]);
// Both start before either ends — they did not serialize against each other.
assert.equal(events[0], 'a:start');
assert.equal(events[1], 'b:start');
assert.deepEqual(events.slice(2).sort(), ['a:end', 'b:end']);
});
test('no lost update: serialized read-modify-write over a shared cell', async () => {
const cell = { value: 0 };
// Each task reads, yields, then writes read+1. Unserialized, concurrent tasks
// would all read the same value and the final count would be < N.
const bump = () =>
withFileWriteLock('cell', async () => {
const seen = cell.value;
await tick();
cell.value = seen + 1;
});
await Promise.all(Array.from({ length: 20 }, bump));
assert.equal(cell.value, 20);
});
test('a rejecting task does not wedge the key', async () => {
const order: string[] = [];
const failing = withFileWriteLock('wedge', async () => {
order.push('fail');
throw new Error('boom');
});
await assert.rejects(failing, /boom/);
const after = await withFileWriteLock('wedge', async () => {
order.push('after');
return 'ok';
});
assert.equal(after, 'ok');
assert.deepEqual(order, ['fail', 'after']);
});
test('returns the task result and propagates its rejection to the caller', async () => {
assert.equal(await withFileWriteLock('result', async () => 42), 42);
await assert.rejects(
withFileWriteLock('result', async () => {
throw new Error('nope');
}),
/nope/,
);
});
});