Files
PaperClipAI/server/src/services/object-run-log-store.test.ts
T
Nicky LeachandClaude Opus 5.5 0e3fdebea8 perf: remove repeated reads and checks from run preparation (#20358)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before each run starts, Core prepares the run: it opens the run log,
resolves skills, and prepares the AI runtime
> - Every run pays this preparation time before the agent can begin, for
a new task or a follow-up, on a cold or a warm computer
> - A trace on a hosted test deployment showed some preparation steps
that repeat a read or a check that a later step does again
> - This pull request removes three of these repeated steps and keeps
the check that each one duplicated
> - The benefit is that each run starts sooner, with no new mechanism
and no change to what a run can see or write

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Run preparation (`task.prepare`). On a hosted test deployment it takes
about 2.8 to 3.6 s. Skills preparation has a P50 of 275 ms (1,161 ms on
a deployment with more skills and projects).
`heartbeat.prepare.run_output` takes 386 to 724 ms.

**Subsystem affected**
Run logs in object storage (`object-byte-journal`,
`object-run-log-store`) and runtime skill delivery (`company-skills`,
`skill-execution-content`).

**Current behavior**
- An object journal claim is preceded by a head read. Its only use is to
give the current owner to `claim()`. `claim()` then reads the head
again, and it rejects the claim only when the two reads differ. The run
log, provider trace capture, and run evidence migration all do this.
- Run log `begin` reads the source-prefix proof object for every claim.
For a new run log, no proof can exist, so the read always returns
"missing".
- Each delivered skill version calls `withVersion`, which authorizes the
skill again for the run's actor. The skill row was read with the same
actor's visibility condition in the same call. With an agent actor this
repeats the company source and project permission reads once per
version.

**Proposed behavior**
- `claim()` reads the head once. It still refuses a deleted journal. The
conditional head write still rejects any change after that read, and a
stale writer is still fenced.
- `begin` does not read a proof for a first claim (generation 1). A
resumed log still reads and validates its proof, so a changed or
malformed proof still fails closed.
- Runtime skill delivery authorizes each skill once, when it reads the
selected rows. Other callers of `withVersion` keep their own check.

**Reason and benefit**
Each removed step protects nothing that a later step does not protect
again:
- Head read: the claim builds on its own head read, and the conditional
write enforces it.
- Proof read: a proof is written only by a writer that holds this log's
head. The journal never removes a head (deletion leaves a tombstone that
`claim()` refuses). So a first claim cannot find a proof.
- Skill check: every caller of `materializeVersionSnapshot` passes a row
that it read through the same actor's visibility condition
(`readFullSkillRows`, `getById`, `listFull`).

Expected saving per run: two object-storage GETs on `run_output` (about
100 ms each on the trace), and the per-version visibility reads in
skills preparation (4 versions per run on the test deployment, about 388
ms per version on a heavier deployment).

**Breaking changes**
None. `claim()` no longer takes `expectedOwner`. It is an internal API
with three callers in this repository, all updated.

Refs #20284 (it made run-log tail reads parallel and cached; this change
covers the begin path).

## What Changed

- `object-byte-journal`: remove the `expectedOwner` input and comparison
from `claim()`. Remove the extra head read in its three callers (run log
`begin`, provider trace capture, run evidence migration).
- `object-run-log-store`: `begin` reads the source-prefix proof only
when the claimed head has `generation > 1`.
- `skill-execution-content`: split `withVersion` into the access check
and `withReadableVersion`. `withVersion` keeps the check for its other
callers.
- `company-skills`: `materializeVersionSnapshot` uses
`withReadableVersion`, because each of its callers already read the
skill row under the actor's visibility condition.
- Tests:
- A new run log `begin` reads `head.json` once and reads no proof. A
resumed log reads `head.json` once and reads its proof once.
- Runtime skill delivery for an agent actor does no per-version source
permission reads.

## Verification

- `cd server && npx vitest run src/services/object-byte-journal.test.ts
src/services/object-run-log-store.test.ts
src/services/object-run-log-store-tail-read.test.ts
src/services/run-log-store.test.ts
src/services/provider-trace-object-store.test.ts`
- `cd server && npx vitest run
src/__tests__/company-skills-runtime-selection.test.ts
src/__tests__/company-skills-runtime-concurrency.test.ts
src/__tests__/heartbeat-runtime-skills.test.ts
src/__tests__/company-skills-service.test.ts
src/__tests__/managed-skill-content.test.ts` (embedded Postgres)
- `cd server && pnpm run typecheck`
- Each new test fails on the base commit: the run log test sees 2 head
reads and 1 proof read; the skill test sees 1 source permission read per
delivered version.
- Existing tests still pass for: stale-writer fencing after a takeover,
a deleted journal that refuses a claim, a malformed proof that fails
closed, and a denied project-scoped skill that is not delivered.
- I did not run the full `pnpm test:run`. CI covers it.

## Risks

- Low. Two concurrent claims of the same journal: before, the second
claim failed when the first published between its two head reads. Now
the later claim takes over, and the earlier writer's next append fails
with a conflict. This is the same result as when the later claim starts
after the first publish. Writer authority (`assertAuthority`) is
unchanged.
- A skill whose visibility is revoked in the moment between its row read
and its materialization in the same call is still delivered for that
run. This was already true for a revocation just after the old
per-version check.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, test runs).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:06:12 -08:00

539 lines
25 KiB
TypeScript

import { createHash } from "node:crypto";
import { Readable } from "node:stream";
import { describe, expect, it, vi } from "vitest";
import type { StorageProvider } from "../storage/types.js";
import { ObjectJournalBusyError } from "./object-byte-journal.js";
import { createObjectRunLogStore } from "./object-run-log-store.js";
async function readBody(body: AsyncIterable<Uint8Array>) {
const chunks: Buffer[] = [];
for await (const bytes of body) chunks.push(Buffer.from(bytes));
return Buffer.concat(chunks);
}
function memory() {
const objects = new Map<string, Buffer>();
let puts = 0,
unavailable = false;
const etag = (b: Buffer) => createHash("sha256").update(b).digest("hex");
const provider: StorageProvider = {
id: "s3",
async putObject() {
throw Error("unconditional");
},
async putObjectConditional(i) {
if (unavailable) throw Error("storage offline");
const old = objects.get(i.objectKey);
if (i.ifNoneMatch ? old : !old || etag(old) !== i.ifMatch)
throw Object.assign(Error("conflict"), { name: "PreconditionFailed" });
const b = Buffer.isBuffer(i.body) ? i.body : await readBody(i.body);
objects.set(i.objectKey, Buffer.from(b));
puts++;
return { etag: etag(b) };
},
async getObject(i) {
const b = objects.get(i.objectKey);
if (!b) throw Object.assign(Error("missing"), { name: "NoSuchKey" });
return { stream: Readable.from([b]), etag: etag(b) };
},
async headObject(i) {
return { exists: objects.has(i.objectKey) };
},
async deleteObject(i) {
objects.delete(i.objectKey);
},
async listObjects(i) {
return { keys: [...objects.keys()].filter((k) => k.startsWith(i.prefix)) };
},
};
return {
provider,
objects,
puts: () => puts,
offline: (value: boolean) => {
unavailable = value;
},
};
}
const input = { companyId: "company", agentId: "agent", runId: "run" };
const event = (seq: number) => ({
stream: "stdout" as const,
chunk: "再開",
ts: "2026-10-30T00:00:00Z",
seq,
});
describe("S3 run log store", () => {
it("awaits a successor flush started by another drain waiter before finalizing", async () => {
const m = memory(), store = createObjectRunLogStore({ provider: m.provider, keyPrefix: "logs", flushMs: 1000,
segmentBytes: 1024, assertWriterAuthority: async () => undefined });
const handle = await store.begin(input), put = m.provider.putObjectConditional!.bind(m.provider);
let firstEntered!: () => void, secondEntered!: () => void, releaseFirst!: () => void, releaseSecond!: () => void;
const firstStarted = new Promise<void>(resolve => { firstEntered = resolve; });
const secondStarted = new Promise<void>(resolve => { secondEntered = resolve; });
const firstGate = new Promise<void>(resolve => { releaseFirst = resolve; });
const secondGate = new Promise<void>(resolve => { releaseSecond = resolve; });
let publications = 0;
m.provider.putObjectConditional = async value => {
if (value.objectKey.endsWith("/head.json") && Buffer.isBuffer(value.body)) {
const head = JSON.parse(value.body.toString());
if (!head.closed && head.end > 0) {
publications++;
if (publications === 1) { firstEntered(); await firstGate; }
else if (publications === 2) { secondEntered(); await secondGate; }
}
}
return put(value);
};
const first = await store.enqueue!(handle, { ...event(1), chunk: "first ".repeat(200) });
const firstCommitted = first.committed.catch(error => error);
await firstStarted;
const second = await store.enqueue!(handle, { ...event(2), chunk: "second" });
const secondCommitted = second.committed.catch(error => error);
let finalized = false;
const finalizing = store.finalize(handle).then(result => { finalized = true; return result; });
releaseFirst();
await secondStarted;
await new Promise<void>(resolve => setImmediate(resolve));
const finalizedBeforeSecondCommit = finalized;
releaseSecond();
const summary = await finalizing;
const committed = await Promise.all([firstCommitted, secondCommitted]);
const content = (await store.read(handle)).content;
expect(finalizedBeforeSecondCommit).toBe(false);
expect(committed.every(value => typeof value === "number")).toBe(true);
expect(summary.bytes).toBe(Buffer.byteLength(content));
expect(content.trim().split("\n").map(line => JSON.parse(line).seq)).toEqual([1, 2]);
});
it("rejects outer records exceeding 8MiB without losing later bounded appends", async () => {
const m = memory(), store = createObjectRunLogStore({ provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined });
const handle = await store.begin(input);
await expect(store.append(handle, { ...event(1), chunk: "x".repeat(8 * 1024 * 1024) }))
.rejects.toThrow("Object run log record exceeds pending-byte limit");
await store.append(handle, event(1));
const content = (await store.read(handle)).content;
expect(content.trim().split("\n")).toHaveLength(1);
expect((await store.finalize(handle)).bytes).toBe(Buffer.byteLength(content));
});
it("begins a new run log with one head read and no source proof read", async () => {
const m = memory(), options = { provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined };
const get = m.provider.getObject.bind(m.provider), reads: string[] = [];
m.provider.getObject = async value => { reads.push(value.objectKey); return get(value); };
const headReads = () => reads.filter(key => key.endsWith("/head.json")).length;
const proofReads = () => reads.filter(key => key.includes("/objects/source-prefix-proof-v1-")).length;
const handle = await createObjectRunLogStore(options).begin(input);
expect(handle.initialSourcePrefixVerified).toBe(false);
expect({ head: headReads(), proof: proofReads() }).toEqual({ head: 1, proof: 0 });
reads.length = 0;
const resumed = await createObjectRunLogStore(options).begin(input);
expect(resumed.initialSourcePrefixVerified).toBe(false);
expect({ head: headReads(), proof: proofReads() }).toEqual({ head: 1, proof: 1 });
});
it("keeps live byte appends deliverable after a proof-only outage and requires cold prefix verification", async () => {
const m = memory(), options = { provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined };
const put = m.provider.putObjectConditional!.bind(m.provider);
let proofUnavailable = false, proofFailures = 0;
m.provider.putObjectConditional = async value => {
if (proofUnavailable && value.objectKey.includes("/objects/source-prefix-proof-v1-")) {
proofFailures++;
throw Object.assign(new Error("proof storage unavailable"), { $metadata: { httpStatusCode: 503 } });
}
return put(value);
};
const store = createObjectRunLogStore(options), handle = await store.begin(input);
await store.recordVerifiedSourcePrefix!(handle, 0);
proofUnavailable = true;
const first = await store.append(handle, { ...event(1), sourceCursor: 2, preservesSourceCoverage: true })
.catch(error => error);
// The existing byte/index/head publication succeeded despite the proof PUT failure.
expect((await store.read(handle)).content.trim().split("\n")).toHaveLength(1);
proofUnavailable = false;
const second = await store.append(handle, { ...event(2), sourceCursor: 5, preservesSourceCoverage: true })
.catch(error => error);
const summary = await store.finalize(handle);
const cold = createObjectRunLogStore(options), resumed = await cold.begin(input);
const content = (await cold.read(resumed)).content;
expect(resumed.initialSourceCursor).toBe(5);
// Absence selects the maintained consumer's mandatory full source-prefix scan.
expect(resumed.initialSourcePrefixVerified).toBe(false);
proofUnavailable = true;
// A complete legacy proof can advance the old max cursor before its optional
// proof PUT fails. The public recorder must retain that known committed head.
await cold.recordVerifiedSourcePrefix!(resumed, 8);
expect(resumed.initialSourcePrefixVerified).toBe(false);
proofUnavailable = false;
const third = await cold.append(resumed, { ...event(3), sourceCursor: 11, preservesSourceCoverage: true });
await cold.finalize(resumed);
const final = await store.begin(input);
const finalContent = (await store.read(final)).content;
await store.finalize(final);
expect(proofFailures).toBeGreaterThan(0);
expect(first).toBeTypeOf("number");
expect(second).toBeTypeOf("number");
expect(third).toBeTypeOf("number");
expect(summary.bytes).toBe(Buffer.byteLength(content));
expect(finalContent.trim().split("\n").map(line => JSON.parse(line).seq)).toEqual([1, 2, 3]);
expect(final.initialSourceCursor).toBe(11);
expect(final.initialSourcePrefixVerified).toBe(false);
});
it.each(["internal-deadline", "caller-abort"] as const)("distinguishes proof %s after the byte head committed", async kind => {
const m = memory(), caller = new AbortController();
const options = { provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined };
const store = createObjectRunLogStore(options), handle = await store.begin({ ...input, signal: caller.signal });
await store.recordVerifiedSourcePrefix!(handle, 0);
const put = m.provider.putObjectConditional!.bind(m.provider);
const deadline = new AbortController();
let entered!: () => void;
const proofStarted = new Promise<void>(resolve => { entered = resolve; });
m.provider.putObjectConditional = async value => {
if (!value.objectKey.includes("/objects/source-prefix-proof-v1-")) return put(value);
entered();
return new Promise((_resolve, reject) => {
if (value.signal!.aborted) reject(value.signal!.reason);
else value.signal!.addEventListener("abort", () => reject(value.signal!.reason), { once: true });
});
};
const timeout = vi.spyOn(AbortSignal, "timeout").mockReturnValue(deadline.signal);
let first: unknown;
const callerReason = new Error("caller revoked writer");
try {
const pending = store.append(handle, { ...event(1), sourceCursor: 2, preservesSourceCoverage: true })
.catch(error => error);
await proofStarted;
if (kind === "internal-deadline") deadline.abort(new DOMException("proof deadline", "TimeoutError"));
else caller.abort(callerReason);
first = await pending;
} finally { timeout.mockRestore(); m.provider.putObjectConditional = put; }
const second = await store.append(handle, { ...event(2), sourceCursor: 5, preservesSourceCoverage: true })
.catch(error => error);
const summary = await store.finalize(handle);
const cold = createObjectRunLogStore(options), resumed = await cold.begin(input);
await cold.finalize(resumed);
expect(resumed.initialSourcePrefixVerified).toBe(false);
if (kind === "internal-deadline") {
expect(first).toBeTypeOf("number");
expect(second).toBeTypeOf("number");
expect(summary.bytes).not.toBeNull();
expect(resumed.initialSourceCursor).toBe(5);
} else {
expect(first).toBe(callerReason);
expect(second).toBe(callerReason);
expect(summary.bytes).toBeNull();
expect(resumed.initialSourceCursor).toBe(2);
}
});
it("keeps an unknown head publication failure fatal", async () => {
const m = memory(), store = createObjectRunLogStore({ provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined });
const handle = await store.begin(input);
await store.recordVerifiedSourcePrefix!(handle, 0);
const put = m.provider.putObjectConditional!.bind(m.provider);
m.provider.putObjectConditional = async value => {
if (value.objectKey.endsWith("/head.json")) throw new Error("unknown head publication failure");
return put(value);
};
await expect(store.append(handle, { ...event(1), sourceCursor: 2, preservesSourceCoverage: true }))
.rejects.toThrow("unknown head publication failure");
await expect(store.append(handle, { ...event(2), sourceCursor: 5, preservesSourceCoverage: true }))
.rejects.toThrow("unknown head publication failure");
expect((await store.read(handle)).content).toBe("");
await store.finalize(handle);
});
it("binds source proof to exact immutable bytes and invalidates it on ordinary appends", async () => {
const m = memory(), options = { provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined };
const store = createObjectRunLogStore(options), handle = await store.begin(input);
expect(handle.initialSourcePrefixVerified).toBe(false);
await store.recordVerifiedSourcePrefix!(handle, 0);
await store.append(handle, { ...event(1), sourceCursor: 2, preservesSourceCoverage: true });
await store.finalize(handle);
const replacement = createObjectRunLogStore(options), verified = await replacement.begin(input);
expect(verified.initialSourceCursor).toBe(2);
expect(verified.initialSourcePrefixVerified).toBe(true);
const head = JSON.parse([...m.objects].find(([key]) => key.endsWith("/head.json"))![1].toString());
expect(Object.keys(head).sort()).toEqual([
"cleanupAfter", "closed", "deleted", "end", "generation", "owner", "root", "roots", "sequence", "sourceCursor", "v",
]);
// An older/generic caller cannot carry the attestation across new bytes,
// even when it reuses the old max cursor or calls its payload a diagnostic.
await replacement.append(verified, { ...event(2), sourceCursor: 2,
chunk: '{"type":"paperclip.prp.event","event":{"duplicate":true}}' });
await replacement.finalize(verified);
const legacy = await store.begin(input);
expect(legacy.initialSourceCursor).toBe(2);
expect(legacy.initialSourcePrefixVerified).toBe(false);
await store.finalize(legacy);
});
it("fails closed when an existing exact-prefix proof is malformed or changed", async () => {
const m = memory(), options = { provider: m.provider, keyPrefix: "logs", flushMs: 1,
assertWriterAuthority: async () => undefined };
const store = createObjectRunLogStore(options), handle = await store.begin(input);
await store.recordVerifiedSourcePrefix!(handle, 0);
await store.append(handle, { ...event(1), sourceCursor: 2, preservesSourceCoverage: true });
await store.finalize(handle);
const proofs = [...m.objects.keys()].filter(key => key.includes("/objects/source-prefix-proof-v1-"));
expect(proofs.length).toBeGreaterThan(0);
for (const key of proofs) m.objects.set(key, Buffer.from('{"schema":"changed-proof"}'));
await expect(createObjectRunLogStore(options).begin(input))
.rejects.toThrow("object_journal_source_prefix_proof_invalid");
});
it("batches concurrent appends, confirms durability, and resumes offsets/sequence on a replacement", async () => {
const m = memory(),
options = {
provider: m.provider,
keyPrefix: "logs",
flushMs: 10,
assertWriterAuthority: async () => undefined,
};
const a = createObjectRunLogStore(options),
handle = await a.begin(input),
before = m.puts();
const sizes = await Promise.all(
Array.from({ length: 100 }, (_, i) => a.append(handle, event(i + 1))),
);
expect(m.puts() - before).toBe(2);
const b = createObjectRunLogStore(options);
const read = await b.read(handle);
expect(read.content.trim().split("\n")).toHaveLength(100);
const successor = await b.begin(input);
expect(successor.initialSequence).toBe(100);
expect(successor.initialBytes).toBe(sizes.reduce((sum, size) => sum + size, 0));
await b.append(successor, event(101));
const summary = await b.finalize(successor);
expect(summary.bytes).toBeGreaterThan(successor.initialBytes!);
expect(summary.sha256).toBe(
createHash("sha256")
.update((await b.read(successor)).content)
.digest("hex"),
);
await expect(a.append(handle, event(102))).rejects.toThrow("another writer");
});
it("finalization flushes already accepted writes and rejects later append admission", async () => {
const m = memory(),
store = createObjectRunLogStore({
provider: m.provider,
keyPrefix: "logs",
flushMs: 1000,
assertWriterAuthority: async () => undefined,
});
const h = await store.begin(input),
pending = store.append(h, event(1));
const finalizing = store.finalize(h);
await expect(store.append(h, event(2))).rejects.toThrow("closed");
const [size, result] = await Promise.all([pending, finalizing]);
expect(result.bytes).toBe(size);
});
it.each(["commit", "storage failure", "Stop"] as const)("settles pre-close bounded admission through %s and fences new producers", async mode => {
const m = memory(), original = m.provider.putObjectConditional!;
let release!: () => void, entered = false;
const gate = new Promise<void>(resolve => { release = resolve; });
const cancellation = new AbortController();
m.provider.putObjectConditional = async value => {
if (value.objectKey.includes("/objects/") && !entered) {
entered = true;
await gate;
value.signal?.throwIfAborted();
if (mode === "storage failure") throw new Error("controlled upload failure");
}
return original(value);
};
const store = createObjectRunLogStore({ provider: m.provider, keyPrefix: "logs", assertWriterAuthority: async () => undefined });
const handle = await store.begin({ ...input, signal: cancellation.signal });
const first = await store.enqueue!(handle, { ...event(1), chunk: "a".repeat(7 * 1024 * 1024) });
const committed = first.committed.catch(error => error);
await vi.waitFor(() => expect(entered).toBe(true));
let admitted = false;
const waiting = store.enqueue!(handle, { ...event(2), chunk: "b".repeat(2 * 1024 * 1024) })
.then(receipt => { admitted = true; return receipt.committed; }).catch(error => error);
let finalized = false;
const finalizing = store.finalize(handle).then(value => { finalized = true; return value; });
try {
await expect(store.enqueue!(handle, event(3))).rejects.toThrow("closed");
const peer = await store.begin({ ...input, runId: "peer" });
await store.append(peer, { ...event(1), chunk: "peer output" });
expect((await store.finalize(peer)).bytes).toBeGreaterThan(0);
expect(admitted).toBe(false);
expect(finalized).toBe(false);
if (mode === "Stop") cancellation.abort(new Error("controlled Stop"));
} finally { release(); }
const [firstResult, secondResult, summary] = await Promise.all([committed, waiting, finalizing]);
if (mode === "commit") {
expect(admitted).toBe(true);
expect(summary.bytes).toBe(Number(firstResult) + Number(secondResult));
const hash = createHash("sha256");
let offset = 0, chunks = 0;
for (;;) {
const page = await store.read(handle, { offset, limitBytes: 1024 * 1024 });
hash.update(page.content);
chunks += (page.content.match(/"chunk":/g) ?? []).length;
if (page.nextOffset === undefined) break;
offset = page.nextOffset;
}
expect(chunks).toBe(2);
expect(summary.sha256).toBe(hash.digest("hex"));
} else {
expect(firstResult).toBeInstanceOf(Error);
expect(secondResult).toBeInstanceOf(Error);
expect(admitted).toBe(false);
expect(summary).toMatchObject({ bytes: null, sha256: null });
}
});
it("drains an admission waiting on global capacity while other writers flush", async () => {
const m = memory(), original = m.provider.putObjectConditional!;
let release!: () => void, uploads = 0;
const gate = new Promise<void>(resolve => { release = resolve; });
m.provider.putObjectConditional = async value => {
if (value.objectKey.includes("/objects/")) { uploads++; await gate; }
return original(value);
};
const store = createObjectRunLogStore({ provider: m.provider, keyPrefix: "logs", assertWriterAuthority: async () => undefined });
const handles = [], committed = [];
for (let index = 0; index < 9; index++) {
const handle = await store.begin({ ...input, runId: `full-${index}` });
handles.push(handle);
committed.push((await store.enqueue!(handle, { ...event(1), chunk: "x".repeat(7 * 1024 * 1024) })).committed);
}
await vi.waitFor(() => expect(uploads).toBe(4));
const target = await store.begin({ ...input, runId: "waiting" });
let admitted = false, finalized = false;
const waiting = store.enqueue!(target, { ...event(1), chunk: "y".repeat(2 * 1024 * 1024) })
.then(receipt => { admitted = true; return receipt.committed; });
const finalizing = store.finalize(target).then(value => { finalized = true; return value; });
try {
await expect(store.enqueue!(target, event(2))).rejects.toThrow("closed");
expect(admitted).toBe(false);
expect(finalized).toBe(false);
} finally { release(); }
const [bytes, summary] = await Promise.all([waiting, finalizing]);
expect(admitted).toBe(true);
expect(summary.bytes).toBe(bytes);
await Promise.all(committed);
await Promise.all(handles.map(handle => store.finalize(handle)));
});
it("does not acknowledge a batch when storage fails", async () => {
const m = memory(),
store = createObjectRunLogStore({
provider: m.provider,
keyPrefix: "logs",
flushMs: 1,
assertWriterAuthority: async () => undefined,
});
const h = await store.begin(input);
m.offline(true);
await expect(store.append(h, event(1))).rejects.toThrow("storage offline");
m.offline(false);
expect((await store.read(h)).content).toBe("");
});
it("preserves UTF-8 and byte offsets across tiny pages and aligned tails", async () => {
const m = memory(),
store = createObjectRunLogStore({
provider: m.provider,
keyPrefix: "logs",
flushMs: 1,
assertWriterAuthority: async () => undefined,
});
const h = await store.begin(input);
await store.append(h, { ...event(1), chunk: "再開😀" });
await store.append(h, event(2));
const full = Buffer.from((await store.read(h)).content);
let offset = 0,
text = "";
for (;;) {
const page = await store.read(h, { offset, limitBytes: 7 });
text += page.content;
if (page.nextOffset === undefined) break;
expect(page.nextOffset).toBeGreaterThan(offset);
offset = page.nextOffset;
}
expect(Buffer.from(text)).toEqual(full);
const start = full.indexOf(Buffer.from("再")) + 1;
const tail = await store.read(h, { offset: start, limitBytes: 1000, alignStartToRecord: true });
expect(Buffer.from(tail.content)).toEqual(full);
});
it("backpressures admission at capacity without poisoning the writer or losing accepted output", async () => {
const m = memory(),
original = m.provider.putObjectConditional!;
let release!: () => void,
entered = false;
const blocked = new Promise<void>((resolve) => {
release = resolve;
});
m.provider.putObjectConditional = async (input) => {
if (input.objectKey.includes("/objects/") && !entered) {
entered = true;
await blocked;
}
return original(input);
};
const store = createObjectRunLogStore({
provider: m.provider,
keyPrefix: "logs",
assertWriterAuthority: async () => undefined,
});
const h = await store.begin(input);
const first = await store.enqueue!(h, { ...event(1), chunk: "a".repeat(7 * 1024 * 1024) });
await vi.waitFor(() => expect(entered).toBe(true));
let admitted = false;
const second = store.enqueue!(h, { ...event(2), chunk: "b".repeat(2 * 1024 * 1024) }).then(
(receipt) => {
admitted = true;
return receipt.committed;
},
);
await Promise.resolve();
expect(admitted).toBe(false);
release();
await Promise.all([first.committed, second]);
await store.append(h, event(3));
expect((await store.finalize(h)).bytes).toBeGreaterThan(9 * 1024 * 1024);
});
it("bounds ordinary reads while explicit internal reads keep queuing", async () => {
const m = memory(),
store = createObjectRunLogStore({
provider: m.provider,
keyPrefix: "logs",
assertWriterAuthority: async () => undefined,
});
const handle = await store.begin(input);
await store.append(handle, event(1));
await store.finalize(handle);
const get = m.provider.getObject;
let open!: () => void;
const gate = new Promise<void>((resolve) => {
open = resolve;
});
m.provider.getObject = async (i) => {
await gate;
return get(i);
};
const polls = Array.from({ length: 40 }, () => store.read(handle));
const excess = store.read(handle);
let rejected: unknown;
void excess.catch((error) => { rejected = error; });
let internal: ReturnType<typeof store.read> | undefined;
try {
await vi.waitFor(() => expect(rejected).toBeInstanceOf(ObjectJournalBusyError));
internal = store.read(handle, { priority: "background" });
open();
for (const poll of await Promise.allSettled(polls))
if (poll.status === "rejected") expect(poll.reason).toBeInstanceOf(ObjectJournalBusyError);
expect((await internal).content.trim().split("\n")).toHaveLength(1);
} finally {
open();
await Promise.allSettled([...polls, excess, ...(internal ? [internal] : [])]);
}
});
});