chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
39
third_party/pi/packages/durable/src/testing/assertions.ts
vendored
Normal file
39
third_party/pi/packages/durable/src/testing/assertions.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { StorageConformanceAssertions } from "./types.ts";
|
||||
|
||||
export type ExpectLike = (actual: unknown, message?: string) => unknown;
|
||||
|
||||
type ExpectResult = {
|
||||
toBe(expected: unknown): unknown;
|
||||
toBeGreaterThan(expected: number): unknown;
|
||||
toEqual(expected: unknown): unknown;
|
||||
toMatchObject(expected: unknown): unknown;
|
||||
toBeTruthy(): unknown;
|
||||
readonly rejects: {
|
||||
toThrow(expected?: string | RegExp): unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/** Adapts a Vitest/Jest-compatible `expect` function without importing either runner. */
|
||||
export function createExpectAssertions(expect: ExpectLike): StorageConformanceAssertions {
|
||||
const result = (actual: unknown, message?: string): ExpectResult => expect(actual, message) as ExpectResult;
|
||||
return {
|
||||
ok(value, message) {
|
||||
result(value, message).toBeTruthy();
|
||||
},
|
||||
strictEqual(actual, expected) {
|
||||
result(actual).toBe(expected);
|
||||
},
|
||||
deepEqual(actual, expected) {
|
||||
result(actual).toEqual(expected);
|
||||
},
|
||||
partialDeepEqual(actual, expected) {
|
||||
result(actual).toMatchObject(expected);
|
||||
},
|
||||
greaterThan(actual, expected) {
|
||||
result(actual).toBeGreaterThan(expected);
|
||||
},
|
||||
async rejects(operation, messageIncludes) {
|
||||
await result(operation).rejects.toThrow(messageIncludes);
|
||||
},
|
||||
};
|
||||
}
|
||||
633
third_party/pi/packages/durable/src/testing/env-conformance.ts
vendored
Normal file
633
third_party/pi/packages/durable/src/testing/env-conformance.ts
vendored
Normal file
@@ -0,0 +1,633 @@
|
||||
import type { Context } from "@earendil-works/chord";
|
||||
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
||||
import {
|
||||
type ExecutionEnv,
|
||||
type FileInfo,
|
||||
getOrThrow,
|
||||
type ListFilesOptions,
|
||||
type Result,
|
||||
type ShellOutputInfo,
|
||||
type WatchChange,
|
||||
type WatchTarget,
|
||||
} from "../env/index.ts";
|
||||
import type { EnvConformanceCase, EnvConformanceOptions } from "./types.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
const decoder = new TextDecoder();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
type EnvTest = (env: ExecutionEnv) => Promise<void>;
|
||||
|
||||
function abortedContext(): Context {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
return withAbortSignal(controller.signal, BACKGROUND_CONTEXT);
|
||||
}
|
||||
|
||||
function errorCode(result: Result<unknown, { code: string }>): string | undefined {
|
||||
return result.ok ? undefined : result.error.code;
|
||||
}
|
||||
|
||||
async function readAll(
|
||||
env: ExecutionEnv,
|
||||
path: string,
|
||||
maxEntries: number,
|
||||
): Promise<{ pages: FileInfo[][]; done: boolean }> {
|
||||
const reader = getOrThrow(await env.openDirReader(path, context));
|
||||
const pages: FileInfo[][] = [];
|
||||
try {
|
||||
for (let page = 0; page < 1000; page++) {
|
||||
const next = getOrThrow(await reader.next(maxEntries, context));
|
||||
pages.push(next.entries);
|
||||
if (next.done) return { pages, done: true };
|
||||
}
|
||||
return { pages, done: false };
|
||||
} finally {
|
||||
await reader.close(context);
|
||||
}
|
||||
}
|
||||
|
||||
/** A listing as `<kind letter> <path>` lines (`f`, `d`, `l`), with its truncation. */
|
||||
async function listed(
|
||||
env: ExecutionEnv,
|
||||
path: string,
|
||||
options?: ListFilesOptions,
|
||||
): Promise<{ entries: string[]; truncated: boolean }> {
|
||||
if (env.listFiles === undefined) throw new Error("The environment does not implement listFiles");
|
||||
const listing = getOrThrow(await env.listFiles(path, options, context));
|
||||
const letter = { file: "f", directory: "d", symlink: "l" } as const;
|
||||
return {
|
||||
entries: listing.entries.map((entry) => `${letter[entry.kind]} ${entry.path}`),
|
||||
truncated: listing.truncated,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a change reports `path`: an overflow, or a reported path at or above it. */
|
||||
function covers(change: WatchChange, path: string): boolean {
|
||||
if ("overflow" in change) return true;
|
||||
if (!("paths" in change)) return false;
|
||||
return change.paths.some(
|
||||
(reported) => path === reported || path.startsWith(`${reported}/`) || path.startsWith(`${reported}\\`),
|
||||
);
|
||||
}
|
||||
|
||||
/** Watch `targets` while `run` changes files, with a helper that waits for a change reporting a path. */
|
||||
async function watching(
|
||||
env: ExecutionEnv,
|
||||
targets: readonly WatchTarget[],
|
||||
run: (helpers: {
|
||||
readonly changes: WatchChange[];
|
||||
/** Wait up to three seconds for a change, after the call, that reports `path`. */
|
||||
expectChange(path: string, change: () => Promise<void>): Promise<void>;
|
||||
absolute(path: string): Promise<string>;
|
||||
}) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const changes: WatchChange[] = [];
|
||||
const watcher = getOrThrow(await env.watch(targets, (change) => changes.push(change), context));
|
||||
try {
|
||||
const absolute = async (path: string) => getOrThrow(await env.absolutePath(path, context));
|
||||
await run({
|
||||
changes,
|
||||
absolute,
|
||||
async expectChange(path, change) {
|
||||
const target = await absolute(path);
|
||||
const from = changes.length;
|
||||
await change();
|
||||
const deadline = Date.now() + 3000;
|
||||
while (!changes.slice(from).some((entry) => covers(entry, target))) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`No change reported ${target}; got ${JSON.stringify(changes.slice(from))}`);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await watcher.close(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates runner-independent cases for an `ExecutionEnv`. `withEnv` must call and await its callback exactly once per
|
||||
* case with an environment whose `cwd` is a fresh, empty, writable directory.
|
||||
*/
|
||||
export function createEnvConformance(options: EnvConformanceOptions): readonly EnvConformanceCase[] {
|
||||
const assert = options.assertions;
|
||||
const shell = options.shell ?? ["sh", "-c"];
|
||||
const symlinks = options.symlinks ?? true;
|
||||
const createCase = (name: string, test: EnvTest, timeoutMs?: number): EnvConformanceCase => ({
|
||||
name,
|
||||
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
||||
run: () => options.withEnv(test),
|
||||
});
|
||||
// Watch cases wait up to three seconds per step, longer than test runners allow by default.
|
||||
const watchCase = (name: string, test: EnvTest): EnvConformanceCase => createCase(name, test, 30_000);
|
||||
const execCollect = async (env: ExecutionEnv, command: string | readonly string[], cwd?: string) => {
|
||||
const output: Record<ShellOutputInfo["stream"], string> = { stdout: "", stderr: "" };
|
||||
const result = await env.exec(
|
||||
command,
|
||||
{
|
||||
...(cwd === undefined ? {} : { cwd }),
|
||||
onOutput: (text, _context, info) => {
|
||||
output[info.stream] += text;
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
return { result, ...output };
|
||||
};
|
||||
|
||||
const cases: EnvConformanceCase[] = [
|
||||
createCase("binary reader reads byte ranges of the opened file", async (env) => {
|
||||
getOrThrow(await env.writeFile("data.txt", "hello world", context));
|
||||
const reader = getOrThrow(await env.openBinaryReader("data.txt", undefined, context));
|
||||
const info = getOrThrow(await reader.info(context));
|
||||
assert.partialDeepEqual(info, { name: "data.txt", kind: "file", size: 11 });
|
||||
assert.strictEqual(decoder.decode(getOrThrow(await reader.read(0, 5, context))), "hello");
|
||||
assert.strictEqual(decoder.decode(getOrThrow(await reader.read(6, 100, context))), "world");
|
||||
assert.strictEqual(getOrThrow(await reader.read(11, 4, context)).length, 0);
|
||||
assert.strictEqual(getOrThrow(await reader.read(50, 1, context)).length, 0);
|
||||
assert.strictEqual(getOrThrow(await reader.read(3, 0, context)).length, 0);
|
||||
assert.strictEqual(errorCode(await reader.read(-1, 1, context)), "invalid");
|
||||
assert.strictEqual(errorCode(await reader.read(0, 1.5, context)), "invalid");
|
||||
assert.strictEqual(errorCode(await reader.read(0, 1, abortedContext())), "aborted");
|
||||
await reader.close(context);
|
||||
await reader.close(context);
|
||||
assert.strictEqual(errorCode(await reader.read(0, 1, context)), "invalid");
|
||||
assert.strictEqual(errorCode(await reader.info(context)), "invalid");
|
||||
}),
|
||||
|
||||
createCase("binary reader scans lines like decoding the whole file", async (env) => {
|
||||
// A byte-order mark, an invalid sequence before a newline, an empty line, a later U+FEFF, and no final newline.
|
||||
const bytes = Uint8Array.from([
|
||||
0xef, 0xbb, 0xbf, 0x61, 0x0a, 0xe2, 0x82, 0x0a, 0x0a, 0xef, 0xbb, 0xbf, 0x62, 0x0a, 0xc3, 0xa9,
|
||||
]);
|
||||
getOrThrow(await env.writeFile("lines.txt", bytes, context));
|
||||
const lines = new TextDecoder().decode(bytes).split("\n");
|
||||
const reader = getOrThrow(await env.openBinaryReader("lines.txt", undefined, context));
|
||||
try {
|
||||
const ranges: [number, number | undefined][] = [
|
||||
[0, undefined],
|
||||
[0, 1],
|
||||
[1, 3],
|
||||
[2, 3],
|
||||
[3, undefined],
|
||||
[4, 9],
|
||||
];
|
||||
for (const [startLine, endLine] of ranges) {
|
||||
const scan = getOrThrow(
|
||||
await reader.scanLines({ startLine, ...(endLine === undefined ? {} : { endLine }) }, context),
|
||||
);
|
||||
const selected = lines.slice(startLine, endLine);
|
||||
const range = (from: number, to: number) =>
|
||||
new TextDecoder("utf-8", { ignoreBOM: from > 0 }).decode(bytes.subarray(from, to));
|
||||
assert.strictEqual(scan.newlines, lines.length - 1);
|
||||
assert.strictEqual(range(scan.start, scan.end), selected.join("\n"));
|
||||
assert.strictEqual(scan.selectedBytes, encoder.encode(selected.join("\n")).length);
|
||||
assert.strictEqual(range(scan.start, scan.firstLineEnd), lines[startLine]);
|
||||
assert.strictEqual(scan.firstLineBytes, encoder.encode(lines[startLine]).length);
|
||||
}
|
||||
assert.partialDeepEqual(getOrThrow(await reader.scanLines({ startLine: 9 }, context)), {
|
||||
start: bytes.length,
|
||||
end: bytes.length,
|
||||
selectedBytes: 0,
|
||||
});
|
||||
assert.strictEqual(errorCode(await reader.scanLines({ startLine: 2, endLine: 2 }, context)), "invalid");
|
||||
} finally {
|
||||
await reader.close(context);
|
||||
}
|
||||
}),
|
||||
|
||||
createCase("binary reader keeps reading the file it opened after a rename", async (env) => {
|
||||
getOrThrow(await env.writeFile("a.txt", "one", context));
|
||||
const reader = getOrThrow(await env.openBinaryReader("a.txt", undefined, context));
|
||||
try {
|
||||
getOrThrow(await env.renameFile("a.txt", "b.txt", context));
|
||||
getOrThrow(await env.writeFile("a.txt", "two", context));
|
||||
assert.strictEqual(decoder.decode(getOrThrow(await reader.read(0, 10, context))), "one");
|
||||
} finally {
|
||||
await reader.close(context);
|
||||
}
|
||||
}),
|
||||
|
||||
createCase("binary reader refuses directories, missing files and aborted opens", async (env) => {
|
||||
getOrThrow(await env.createDir("dir", undefined, context));
|
||||
getOrThrow(await env.writeFile("file.txt", "x", context));
|
||||
assert.strictEqual(errorCode(await env.openBinaryReader("dir", undefined, context)), "is_directory");
|
||||
assert.strictEqual(errorCode(await env.openBinaryReader("missing.txt", undefined, context)), "not_found");
|
||||
assert.strictEqual(errorCode(await env.openBinaryReader("file.txt", undefined, abortedContext())), "aborted");
|
||||
}),
|
||||
|
||||
createCase("directory reader pages every entry exactly once", async (env) => {
|
||||
const names = ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"];
|
||||
for (const name of names) getOrThrow(await env.writeFile(name, name, context));
|
||||
getOrThrow(await env.createDir("sub", undefined, context));
|
||||
const { pages, done } = await readAll(env, ".", 2);
|
||||
assert.ok(done, "directory reader reached the end");
|
||||
for (const page of pages) assert.ok(page.length <= 2, "page within maxEntries");
|
||||
const entries = pages.flat();
|
||||
assert.deepEqual(entries.map((entry) => entry.name).sort(), [...names, "sub"].sort());
|
||||
assert.strictEqual(entries.find((entry) => entry.name === "sub")?.kind, "directory");
|
||||
assert.partialDeepEqual(
|
||||
entries.find((entry) => entry.name === "a.txt"),
|
||||
{ kind: "file", size: 5 },
|
||||
);
|
||||
}),
|
||||
|
||||
createCase("directory reader reports the end and refuses use after close", async (env) => {
|
||||
getOrThrow(await env.createDir("empty", undefined, context));
|
||||
const reader = getOrThrow(await env.openDirReader("empty", context));
|
||||
assert.deepEqual(getOrThrow(await reader.next(10, context)), { entries: [], done: true });
|
||||
assert.deepEqual(getOrThrow(await reader.next(10, context)), { entries: [], done: true });
|
||||
assert.strictEqual(errorCode(await reader.next(0, context)), "invalid");
|
||||
assert.strictEqual(errorCode(await reader.next(1, abortedContext())), "aborted");
|
||||
await reader.close(context);
|
||||
await reader.close(context);
|
||||
assert.strictEqual(errorCode(await reader.next(1, context)), "invalid");
|
||||
}),
|
||||
|
||||
createCase("directory reader refuses missing paths and files", async (env) => {
|
||||
getOrThrow(await env.writeFile("file.txt", "x", context));
|
||||
assert.strictEqual(errorCode(await env.openDirReader("missing", context)), "not_found");
|
||||
const file = await env.openDirReader("file.txt", context);
|
||||
assert.strictEqual(errorCode(file), "not_directory");
|
||||
assert.strictEqual(errorCode(await env.openDirReader(".", abortedContext())), "aborted");
|
||||
}),
|
||||
|
||||
createCase("directory reader skips entries removed during enumeration", async (env) => {
|
||||
getOrThrow(await env.createDir("dir", undefined, context));
|
||||
for (const name of ["x", "y", "z"]) getOrThrow(await env.writeFile(`dir/${name}`, name, context));
|
||||
const reader = getOrThrow(await env.openDirReader("dir", context));
|
||||
try {
|
||||
for (const name of ["x", "y", "z"]) getOrThrow(await env.remove(`dir/${name}`, undefined, context));
|
||||
const entries: FileInfo[] = [];
|
||||
for (let page = 0; page < 10; page++) {
|
||||
const next = getOrThrow(await reader.next(10, context));
|
||||
entries.push(...next.entries);
|
||||
if (next.done) break;
|
||||
}
|
||||
assert.deepEqual(entries, []);
|
||||
} finally {
|
||||
await reader.close(context);
|
||||
}
|
||||
}),
|
||||
|
||||
watchCase("watch reports a missing file's creation, changes, replacement and removal", async (env) => {
|
||||
await watching(env, [{ path: "AGENTS.md" }], async ({ expectChange }) => {
|
||||
await expectChange("AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("AGENTS.md", "one", context));
|
||||
});
|
||||
await expectChange("AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("AGENTS.md", "two!", context));
|
||||
});
|
||||
// Editors replace a file by renaming a new one over it.
|
||||
await expectChange("AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("AGENTS.md.tmp", "three", context));
|
||||
getOrThrow(await env.renameFile("AGENTS.md.tmp", "AGENTS.md", context));
|
||||
});
|
||||
await expectChange("AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("AGENTS.md", "four", context));
|
||||
});
|
||||
await expectChange("AGENTS.md", async () => {
|
||||
getOrThrow(await env.remove("AGENTS.md", undefined, context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch reports a missing target whose ancestors are created", async (env) => {
|
||||
await watching(env, [{ path: "a/b/c/AGENTS.md" }], async ({ expectChange }) => {
|
||||
await expectChange("a/b/c/AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("a/b/c/AGENTS.md", "x", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch follows directories created together with their contents", async (env) => {
|
||||
getOrThrow(await env.createDir("skills", undefined, context));
|
||||
await watching(env, [{ path: "skills", recursive: true }], async ({ expectChange }) => {
|
||||
// Written before any watcher on the new directories can exist.
|
||||
await expectChange("skills/a/b/SKILL.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/b/SKILL.md", "one", context));
|
||||
});
|
||||
await expectChange("skills/a/b/SKILL.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/b/SKILL.md", "two!", context));
|
||||
});
|
||||
await expectChange("skills/a/b/c/SKILL.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/b/c/SKILL.md", "deeper", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch keeps watching a path whose parent is renamed and recreated", async (env) => {
|
||||
getOrThrow(await env.writeFile("proj/.pi/skills/x.md", "x", context));
|
||||
await watching(env, [{ path: "proj/.pi/skills", recursive: true }], async ({ expectChange }) => {
|
||||
await expectChange("proj/.pi/skills", async () => {
|
||||
getOrThrow(await env.renameFile("proj/.pi", "proj/old", context));
|
||||
});
|
||||
await expectChange("proj/.pi/skills/y.md", async () => {
|
||||
getOrThrow(await env.writeFile("proj/.pi/skills/y.md", "y", context));
|
||||
});
|
||||
await expectChange("proj/.pi/skills/y.md", async () => {
|
||||
getOrThrow(await env.writeFile("proj/.pi/skills/y.md", "yy", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch skips excluded entries and reports a rename out of them", async (env) => {
|
||||
getOrThrow(await env.createDir("skills", undefined, context));
|
||||
const targets: WatchTarget[] = [
|
||||
{ path: "skills", recursive: true, exclude: { hidden: true, names: ["node_modules"] } },
|
||||
];
|
||||
await watching(env, targets, async ({ changes, expectChange, absolute }) => {
|
||||
getOrThrow(await env.writeFile("skills/node_modules/dep/SKILL.md", "dep", context));
|
||||
getOrThrow(await env.writeFile("skills/.SKILL.md.tmp", "draft", context));
|
||||
await expectChange("skills/SKILL.md", async () => {
|
||||
getOrThrow(await env.renameFile("skills/.SKILL.md.tmp", "skills/SKILL.md", context));
|
||||
});
|
||||
const hidden = [await absolute("skills/node_modules"), await absolute("skills/.SKILL.md.tmp")];
|
||||
for (const change of changes) {
|
||||
if (!("paths" in change)) continue;
|
||||
for (const path of change.paths) {
|
||||
assert.ok(
|
||||
!hidden.some((excluded) => path === excluded || path.startsWith(excluded)),
|
||||
`excluded ${path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch keeps recursive coverage where a non-recursive target overlaps", async (env) => {
|
||||
getOrThrow(await env.writeFile("skills/a/one.md", "one", context));
|
||||
const targets: WatchTarget[] = [{ path: "skills" }, { path: "skills", recursive: true }];
|
||||
await watching(env, targets, async ({ expectChange }) => {
|
||||
await expectChange("skills/a/two.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/two.md", "two", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch follows a directory replaced at the same path", async (env) => {
|
||||
getOrThrow(await env.writeFile("skills/a/x.md", "x", context));
|
||||
await watching(env, [{ path: "skills", recursive: true }], async ({ expectChange }) => {
|
||||
await expectChange("skills/a", async () => {
|
||||
getOrThrow(await env.renameFile("skills/a", "skills-old", context));
|
||||
getOrThrow(await env.createDir("skills/a", undefined, context));
|
||||
});
|
||||
await expectChange("skills/a/y.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/y.md", "y", context));
|
||||
});
|
||||
await expectChange("skills/a/y.md", async () => {
|
||||
getOrThrow(await env.writeFile("skills/a/y.md", "yy", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
watchCase("watch stops reporting once closed", async (env) => {
|
||||
const changes: WatchChange[] = [];
|
||||
const watcher = getOrThrow(await env.watch([{ path: "file.txt" }], (change) => changes.push(change), context));
|
||||
assert.ok(watcher.mode === "native" || watcher.mode === "polling", "watcher reports its mode");
|
||||
await watcher.close(context);
|
||||
await watcher.close(context);
|
||||
getOrThrow(await env.writeFile("file.txt", "x", context));
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
assert.deepEqual(changes, []);
|
||||
}),
|
||||
|
||||
createCase("argv exec passes arguments to the program without shell parsing", async (env) => {
|
||||
const hostile = "it's $(touch pwned) `touch pwned` *; touch pwned";
|
||||
const { result, stdout } = await execCollect(env, [
|
||||
...shell,
|
||||
'printf "%s|%s" "$1" "$2"',
|
||||
"argv0",
|
||||
hostile,
|
||||
"a b",
|
||||
]);
|
||||
assert.strictEqual(getOrThrow(result).exitCode, 0);
|
||||
assert.strictEqual(stdout, `${hostile}|a b`);
|
||||
assert.strictEqual(getOrThrow(await env.exists("pwned", context)), false);
|
||||
}),
|
||||
|
||||
createCase("exec reports the stream of every chunk in both forms", async (env) => {
|
||||
const script = "printf out; printf err >&2; printf more";
|
||||
const argv = await execCollect(env, [...shell, script]);
|
||||
assert.strictEqual(getOrThrow(argv.result).exitCode, 0);
|
||||
assert.strictEqual(argv.stdout, "outmore");
|
||||
assert.strictEqual(argv.stderr, "err");
|
||||
const string = await execCollect(env, script);
|
||||
assert.strictEqual(getOrThrow(string.result).exitCode, 0);
|
||||
assert.strictEqual(string.stdout, "outmore");
|
||||
assert.strictEqual(string.stderr, "err");
|
||||
}),
|
||||
|
||||
createCase("argv exec honors cwd and exit codes", async (env) => {
|
||||
getOrThrow(await env.createDir("sub", undefined, context));
|
||||
const made = await execCollect(env, [...shell, "printf x > made.txt; exit 3"], "sub");
|
||||
assert.strictEqual(getOrThrow(made.result).exitCode, 3);
|
||||
assert.strictEqual(getOrThrow(await env.readTextFile("sub/made.txt", context)), "x");
|
||||
}),
|
||||
|
||||
createCase("argv exec reports missing programs and empty argv as spawn errors", async (env) => {
|
||||
assert.strictEqual(
|
||||
errorCode(await env.exec(["pi-durable-conformance-missing-program"], undefined, context)),
|
||||
"spawn_error",
|
||||
);
|
||||
assert.strictEqual(errorCode(await env.exec([], undefined, context)), "spawn_error");
|
||||
}),
|
||||
|
||||
createCase("windowed exec keeps the exact tail and counts what it skips", async (env) => {
|
||||
const lines = 2000;
|
||||
const window = { maxBytes: 200, maxLines: 5, minIntervalMs: 0, bytesPerSecond: 1_000_000_000 };
|
||||
let bytes = 0;
|
||||
let newlines = 0;
|
||||
let tail = "";
|
||||
const result = await env.exec(
|
||||
[...shell, `i=0; while [ $i -lt ${lines} ]; do echo line-$i; i=$((i+1)); done`],
|
||||
{
|
||||
window,
|
||||
onOutput: (text, _context, info) => {
|
||||
if (info.skipped !== undefined) {
|
||||
bytes += info.skipped.bytes;
|
||||
newlines += info.skipped.newlines;
|
||||
const after = encoder.encode(text).length;
|
||||
const afterNewlines = text.split("\n").length - 1;
|
||||
assert.ok(
|
||||
after > window.maxBytes || afterNewlines > window.maxLines,
|
||||
"a skip is followed by more than the window",
|
||||
);
|
||||
tail = "";
|
||||
}
|
||||
bytes += encoder.encode(text).length;
|
||||
newlines += text.split("\n").length - 1;
|
||||
tail += text;
|
||||
},
|
||||
},
|
||||
context,
|
||||
);
|
||||
assert.strictEqual(getOrThrow(result).exitCode, 0);
|
||||
const expected = Array.from({ length: lines }, (_, index) => `line-${index}\n`);
|
||||
assert.strictEqual(bytes, expected.join("").length);
|
||||
assert.strictEqual(newlines, lines);
|
||||
assert.ok(tail.endsWith(expected.slice(-window.maxLines).join("")), "the delivered output ends with the tail");
|
||||
}),
|
||||
|
||||
createCase("argv exec distinguishes timeout from abort", async (env) => {
|
||||
const timedOut = await env.exec([...shell, "sleep 2"], { timeout: 0.1 }, context);
|
||||
assert.strictEqual(errorCode(timedOut), "timeout");
|
||||
const controller = new AbortController();
|
||||
const running = env.exec([...shell, "sleep 2"], undefined, withAbortSignal(controller.signal, context));
|
||||
setTimeout(() => controller.abort(), 100);
|
||||
assert.strictEqual(errorCode(await running), "aborted");
|
||||
}),
|
||||
];
|
||||
|
||||
if (options.listFiles) {
|
||||
const writeAll = async (env: ExecutionEnv, files: readonly string[]) => {
|
||||
for (const file of files) getOrThrow(await env.writeFile(file, "", context));
|
||||
};
|
||||
cases.push(
|
||||
createCase("listFiles lists files and directories depth first in name order", async (env) => {
|
||||
await writeAll(env, ["b.txt", "a/z.txt", "a/b/c.txt", ".hidden/x", "A.txt"]);
|
||||
getOrThrow(await env.createDir("empty", undefined, context));
|
||||
assert.deepEqual(await listed(env, "."), {
|
||||
entries: [
|
||||
"d .hidden",
|
||||
"f .hidden/x",
|
||||
"f A.txt",
|
||||
"d a",
|
||||
"d a/b",
|
||||
"f a/b/c.txt",
|
||||
"f a/z.txt",
|
||||
"f b.txt",
|
||||
"d empty",
|
||||
],
|
||||
truncated: false,
|
||||
});
|
||||
assert.deepEqual((await listed(env, ".", { hidden: false })).entries, [
|
||||
"f A.txt",
|
||||
"d a",
|
||||
"d a/b",
|
||||
"f a/b/c.txt",
|
||||
"f a/z.txt",
|
||||
"f b.txt",
|
||||
"d empty",
|
||||
]);
|
||||
const expected = { entries: ["d b", "f b/c.txt", "f z.txt"], truncated: false };
|
||||
assert.deepEqual(await listed(env, "a"), expected);
|
||||
assert.deepEqual(await listed(env, getOrThrow(await env.absolutePath("a", context))), expected);
|
||||
assert.deepEqual(await listed(env, "empty"), { entries: [], truncated: false });
|
||||
}),
|
||||
|
||||
createCase("listFiles skips what git ignores in work trees, .git, and excluded names", async (env) => {
|
||||
await writeAll(env, ["keep.txt", "out/x.js", "sub/keep.txt", "sub/drop.log", "node_modules/m/i.js"]);
|
||||
getOrThrow(await env.writeFile(".gitignore", "out/\n*.log\n", context));
|
||||
getOrThrow(await env.writeFile("sub/.gitignore", "!keep.txt\n", context));
|
||||
// Not a work tree yet: `.gitignore` files have no effect.
|
||||
assert.ok((await listed(env, ".")).entries.includes("f out/x.js"));
|
||||
getOrThrow(await env.writeFile(".git/HEAD", "ref: refs/heads/main\n", context));
|
||||
getOrThrow(await env.writeFile(".git/info/exclude", "keep.txt\n", context));
|
||||
assert.deepEqual((await listed(env, ".", { exclude: ["node_modules"] })).entries, [
|
||||
"f .gitignore",
|
||||
"d sub",
|
||||
"f sub/.gitignore",
|
||||
"f sub/keep.txt",
|
||||
]);
|
||||
// A directory inside the work tree applies its parents' rules.
|
||||
assert.deepEqual((await listed(env, "sub")).entries, ["f .gitignore", "f keep.txt"]);
|
||||
const all = (await listed(env, ".", { gitignore: false })).entries;
|
||||
assert.ok(
|
||||
all.includes("f out/x.js") && all.includes("f node_modules/m/i.js") && all.includes("f keep.txt"),
|
||||
);
|
||||
assert.ok(!all.some((entry) => entry.includes(".git/") || entry === "d .git"));
|
||||
}),
|
||||
|
||||
createCase("listFiles stops at its bounds and reports truncated", async (env) => {
|
||||
await writeAll(env, ["a", "b", "c"]);
|
||||
assert.deepEqual(await listed(env, ".", { maxEntries: 2 }), { entries: ["f a", "f b"], truncated: true });
|
||||
assert.deepEqual(await listed(env, ".", { maxEntries: 3 }), {
|
||||
entries: ["f a", "f b", "f c"],
|
||||
truncated: false,
|
||||
});
|
||||
// Each entry counts its path plus two bytes.
|
||||
assert.deepEqual(await listed(env, ".", { maxBytes: 6 }), { entries: ["f a", "f b"], truncated: true });
|
||||
assert.deepEqual(await listed(env, ".", { maxEntries: 0 }), { entries: [], truncated: true });
|
||||
assert.strictEqual(errorCode(await env.listFiles!(".", { maxEntries: -1 }, context)), "invalid");
|
||||
assert.strictEqual(errorCode(await env.listFiles!(".", { timeoutMs: 1.5 }, context)), "invalid");
|
||||
}),
|
||||
|
||||
createCase("listFiles fails on missing directories, files, and aborts", async (env) => {
|
||||
getOrThrow(await env.writeFile("file.txt", "", context));
|
||||
assert.strictEqual(errorCode(await env.listFiles!("missing", undefined, context)), "not_found");
|
||||
assert.strictEqual(errorCode(await env.listFiles!("file.txt", undefined, context)), "not_directory");
|
||||
assert.strictEqual(errorCode(await env.listFiles!(".", undefined, abortedContext())), "aborted");
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (symlinks && options.listFiles) {
|
||||
cases.push(
|
||||
createCase("listFiles lists symbolic links without following them", async (env) => {
|
||||
getOrThrow(await env.writeFile("dir/inner.txt", "", context));
|
||||
const linked = await env.exec(
|
||||
[...shell, "ln -s dir link && ln -s missing dangling && ln -s .. dir/loop"],
|
||||
undefined,
|
||||
context,
|
||||
);
|
||||
assert.strictEqual(getOrThrow(linked).exitCode, 0);
|
||||
assert.deepEqual((await listed(env, ".")).entries, [
|
||||
"l dangling",
|
||||
"d dir",
|
||||
"f dir/inner.txt",
|
||||
"l dir/loop",
|
||||
"l link",
|
||||
]);
|
||||
// The listed directory itself may be a link.
|
||||
assert.deepEqual((await listed(env, "link")).entries, ["f inner.txt", "l loop"]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (symlinks) {
|
||||
cases.push(
|
||||
createCase("binary reader follows symlinks unless noFollow refuses the final one", async (env) => {
|
||||
getOrThrow(await env.writeFile("target.txt", "target", context));
|
||||
getOrThrow(await env.createDir("sub", undefined, context));
|
||||
getOrThrow(await env.writeFile("sub/inner.txt", "inner", context));
|
||||
const linked = await env.exec(
|
||||
[...shell, "ln -s target.txt link.txt && ln -s sub dirlink"],
|
||||
undefined,
|
||||
context,
|
||||
);
|
||||
assert.strictEqual(getOrThrow(linked).exitCode, 0);
|
||||
|
||||
const followed = getOrThrow(await env.openBinaryReader("link.txt", undefined, context));
|
||||
assert.strictEqual(decoder.decode(getOrThrow(await followed.read(0, 10, context))), "target");
|
||||
await followed.close(context);
|
||||
|
||||
assert.strictEqual(
|
||||
errorCode(await env.openBinaryReader("link.txt", { noFollow: true }, context)),
|
||||
"invalid",
|
||||
);
|
||||
|
||||
// Only the final component is refused; earlier symlinked directories still resolve.
|
||||
const inner = getOrThrow(await env.openBinaryReader("dirlink/inner.txt", { noFollow: true }, context));
|
||||
assert.strictEqual(decoder.decode(getOrThrow(await inner.read(0, 10, context))), "inner");
|
||||
await inner.close(context);
|
||||
}),
|
||||
|
||||
watchCase("watch reports changes to the file a watched symbolic link points to", async (env) => {
|
||||
getOrThrow(await env.writeFile("data/real.md", "one", context));
|
||||
getOrThrow(await env.createDir("config", undefined, context));
|
||||
const linked = await env.exec([...shell, "ln -s ../data/real.md config/AGENTS.md"], undefined, context);
|
||||
assert.strictEqual(getOrThrow(linked).exitCode, 0);
|
||||
await watching(env, [{ path: "config/AGENTS.md" }], async ({ expectChange }) => {
|
||||
await expectChange("config/AGENTS.md", async () => {
|
||||
getOrThrow(await env.writeFile("data/real.md", "two!", context));
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return cases;
|
||||
}
|
||||
27
third_party/pi/packages/durable/src/testing/index.ts
vendored
Normal file
27
third_party/pi/packages/durable/src/testing/index.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
export { createExpectAssertions, type ExpectLike } from "./assertions.ts";
|
||||
export { createEnvConformance } from "./env-conformance.ts";
|
||||
export { registerEnvConformance, registerStorageConformance, type StorageConformanceRunner } from "./runner.ts";
|
||||
export {
|
||||
STORAGE_MEMORY_SCALES,
|
||||
STORAGE_READ_BENCHMARKS,
|
||||
STORAGE_WRITE_BENCHMARKS,
|
||||
type StorageBenchmarkDataset,
|
||||
type StorageBenchmarkScale,
|
||||
type StorageReadBenchmark,
|
||||
type StorageWriteBenchmark,
|
||||
seedStorageBenchmark,
|
||||
seedStorageWriteBenchmark,
|
||||
storageBenchmarkPrimaryRecordCount,
|
||||
TIMING_SCALE,
|
||||
} from "./storage-benchmark.ts";
|
||||
export { createStorageConformance } from "./storage-conformance.ts";
|
||||
export type {
|
||||
EnvConformanceAssertions,
|
||||
EnvConformanceCase,
|
||||
EnvConformanceOptions,
|
||||
EnvConformanceProvider,
|
||||
StorageConformanceAssertions,
|
||||
StorageConformanceCase,
|
||||
StorageConformanceOptions,
|
||||
StorageConformanceProvider,
|
||||
} from "./types.ts";
|
||||
38
third_party/pi/packages/durable/src/testing/runner.ts
vendored
Normal file
38
third_party/pi/packages/durable/src/testing/runner.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createExpectAssertions, type ExpectLike } from "./assertions.ts";
|
||||
import { createEnvConformance } from "./env-conformance.ts";
|
||||
import { createStorageConformance } from "./storage-conformance.ts";
|
||||
import type { EnvConformanceOptions, EnvConformanceProvider, StorageConformanceProvider } from "./types.ts";
|
||||
|
||||
export interface StorageConformanceRunner {
|
||||
readonly describe: (name: string, suite: () => void) => unknown;
|
||||
readonly expect: ExpectLike;
|
||||
readonly it: (name: string, test: () => Promise<void>, timeoutMs?: number) => unknown;
|
||||
}
|
||||
|
||||
/** Registers the runner-independent cases with a Vitest/Jest-compatible test runner. */
|
||||
export function registerStorageConformance(
|
||||
runner: StorageConformanceRunner,
|
||||
name: string,
|
||||
withStorage: StorageConformanceProvider,
|
||||
): void {
|
||||
const cases = createStorageConformance({
|
||||
assertions: createExpectAssertions(runner.expect),
|
||||
withStorage,
|
||||
});
|
||||
runner.describe(name, () => {
|
||||
for (const testCase of cases) runner.it(testCase.name, testCase.run);
|
||||
});
|
||||
}
|
||||
|
||||
/** Registers the runner-independent `ExecutionEnv` cases with a Vitest/Jest-compatible test runner. */
|
||||
export function registerEnvConformance(
|
||||
runner: StorageConformanceRunner,
|
||||
name: string,
|
||||
withEnv: EnvConformanceProvider,
|
||||
options: Pick<EnvConformanceOptions, "shell" | "symlinks" | "listFiles"> = {},
|
||||
): void {
|
||||
const cases = createEnvConformance({ ...options, assertions: createExpectAssertions(runner.expect), withEnv });
|
||||
runner.describe(name, () => {
|
||||
for (const testCase of cases) runner.it(testCase.name, testCase.run, testCase.timeoutMs);
|
||||
});
|
||||
}
|
||||
489
third_party/pi/packages/durable/src/testing/storage-benchmark.ts
vendored
Normal file
489
third_party/pi/packages/durable/src/testing/storage-benchmark.ts
vendored
Normal file
@@ -0,0 +1,489 @@
|
||||
import type { JsonValue } from "@earendil-works/chord";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import type {
|
||||
ConversationId,
|
||||
DocumentCreate,
|
||||
DocumentId,
|
||||
EntryId,
|
||||
Seq,
|
||||
Storage,
|
||||
StorageWrite,
|
||||
SubmissionId,
|
||||
TaskId,
|
||||
TaskRecord,
|
||||
} from "../types.ts";
|
||||
import { ROOT_CONVERSATION_ID } from "../types.ts";
|
||||
|
||||
export type StorageBenchmarkScale = {
|
||||
readonly name: string;
|
||||
readonly entryCount: number;
|
||||
readonly taskCount: number;
|
||||
readonly documentCount: number;
|
||||
};
|
||||
|
||||
export const STORAGE_MEMORY_SCALES: readonly StorageBenchmarkScale[] = [
|
||||
{ name: "1k", entryCount: 1_000, taskCount: 200, documentCount: 200 },
|
||||
{ name: "10k", entryCount: 10_000, taskCount: 2_000, documentCount: 2_000 },
|
||||
];
|
||||
|
||||
export const TIMING_SCALE: StorageBenchmarkScale = {
|
||||
name: "timing",
|
||||
entryCount: 1_000,
|
||||
taskCount: 300,
|
||||
documentCount: 300,
|
||||
};
|
||||
|
||||
const REPLAY_TAILS = [0, 16, 128, 1_024] as const;
|
||||
const HISTORY_SEGMENT_LENGTH = 128;
|
||||
const FORK_DEPTH = 8;
|
||||
const ENTRIES_PER_FORK = 32;
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export function storageBenchmarkPrimaryRecordCount(scale: StorageBenchmarkScale): number {
|
||||
return (
|
||||
1 +
|
||||
scale.entryCount +
|
||||
scale.taskCount +
|
||||
scale.documentCount +
|
||||
REPLAY_TAILS.length +
|
||||
1 +
|
||||
FORK_DEPTH * (1 + ENTRIES_PER_FORK)
|
||||
);
|
||||
}
|
||||
|
||||
type StoredTask = TaskRecord<JsonValue, JsonValue, JsonValue>;
|
||||
|
||||
export type StorageBenchmarkDataset = {
|
||||
readonly firstEntryId: EntryId;
|
||||
readonly filteredTaskCount: number;
|
||||
readonly exactDocumentId: DocumentId;
|
||||
readonly exactDocumentKey: string;
|
||||
readonly replayDocumentIds: Readonly<Record<(typeof REPLAY_TAILS)[number], DocumentId>>;
|
||||
readonly historicalDocumentId: DocumentId;
|
||||
readonly ancientAt: Seq;
|
||||
readonly recentAt: Seq;
|
||||
readonly deepestConversationId: ConversationId;
|
||||
readonly ancestorHeadEntryId: EntryId;
|
||||
};
|
||||
|
||||
function task(id: TaskId<JsonValue>, index: number): StoredTask {
|
||||
const statuses = ["pending", "running", "terminal"] as const;
|
||||
const status = statuses[index % statuses.length];
|
||||
const common = {
|
||||
id,
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
kind: index % 4 === 0 ? "benchmark.filtered" : "benchmark.other",
|
||||
version: 1,
|
||||
input: { index },
|
||||
background: index % 5 === 0,
|
||||
abortRequested: index % 7 === 0,
|
||||
};
|
||||
if (status === "terminal") {
|
||||
return { ...common, state: { status, outcome: { status: "completed", result: { index } } } };
|
||||
}
|
||||
return { ...common, state: { status, checkpoint: { index, payload: "x".repeat(64) } } };
|
||||
}
|
||||
|
||||
/** Seed deterministic representative data through only the public `Storage` contract. */
|
||||
export async function seedStorageBenchmark(
|
||||
storage: Storage,
|
||||
scale: StorageBenchmarkScale = TIMING_SCALE,
|
||||
): Promise<StorageBenchmarkDataset> {
|
||||
await storage.commit([{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], BACKGROUND_CONTEXT);
|
||||
|
||||
let firstEntryId: EntryId | undefined;
|
||||
for (let start = 0; start < scale.entryCount; start += BATCH_SIZE) {
|
||||
const writes: StorageWrite[] = [];
|
||||
for (let index = start; index < Math.min(start + BATCH_SIZE, scale.entryCount); index++) {
|
||||
const id = await storage.mintId<EntryId>();
|
||||
if (index === 0) firstEntryId = id;
|
||||
writes.push({
|
||||
type: "entry",
|
||||
value: {
|
||||
id,
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
kind: "benchmark.entry",
|
||||
...(index === 0 ? { head: id } : {}),
|
||||
data: { index, text: `entry-${index}-${"x".repeat(96)}` },
|
||||
},
|
||||
});
|
||||
}
|
||||
await storage.commit(writes, BACKGROUND_CONTEXT);
|
||||
}
|
||||
|
||||
for (let start = 0; start < scale.taskCount; start += BATCH_SIZE) {
|
||||
const writes: StorageWrite[] = [];
|
||||
for (let index = start; index < Math.min(start + BATCH_SIZE, scale.taskCount); index++) {
|
||||
writes.push({ type: "task", value: task(await storage.mintId<TaskId<JsonValue>>(), index) });
|
||||
}
|
||||
await storage.commit(writes, BACKGROUND_CONTEXT);
|
||||
}
|
||||
|
||||
let exactDocumentId: DocumentId | undefined;
|
||||
for (let start = 0; start < scale.documentCount; start += BATCH_SIZE) {
|
||||
const writes: StorageWrite[] = [];
|
||||
for (let index = start; index < Math.min(start + BATCH_SIZE, scale.documentCount); index++) {
|
||||
const id = await storage.mintId<DocumentId>();
|
||||
exactDocumentId = id;
|
||||
writes.push({
|
||||
type: "document.create",
|
||||
record: { id, kind: "benchmark.family", key: `key-${index}`, scope: { kind: "session" } },
|
||||
content: { kind: "base", version: 1, value: { index, text: "x".repeat(128) } },
|
||||
});
|
||||
}
|
||||
await storage.commit(writes, BACKGROUND_CONTEXT);
|
||||
}
|
||||
|
||||
const replayEntries = await Promise.all(
|
||||
REPLAY_TAILS.map(async (tail) => {
|
||||
const id = await storage.mintId<DocumentId>();
|
||||
return { tail, id };
|
||||
}),
|
||||
);
|
||||
await storage.commit(
|
||||
replayEntries.map(({ tail, id }) => ({
|
||||
type: "document.create" as const,
|
||||
record: {
|
||||
id,
|
||||
kind: "benchmark.replay",
|
||||
key: String(tail),
|
||||
scope: { kind: "conversation" as const, conversationId: ROOT_CONVERSATION_ID },
|
||||
history: "rewindable" as const,
|
||||
fork: "asOf" as const,
|
||||
},
|
||||
content: { kind: "base" as const, version: 1, value: { count: 0, text: "x".repeat(64) } },
|
||||
})),
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
for (let count = 1; count <= REPLAY_TAILS.at(-1)!; count++) {
|
||||
await storage.commit(
|
||||
replayEntries
|
||||
.filter(({ tail }) => count <= tail)
|
||||
.map(({ id }) => ({
|
||||
type: "document.change" as const,
|
||||
id,
|
||||
content: { kind: "delta" as const, version: 1, ops: [["s", ["count"], count] as const] },
|
||||
})),
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
const historicalDocumentId = await storage.mintId<DocumentId>();
|
||||
const historicalRecord = {
|
||||
id: historicalDocumentId,
|
||||
kind: "benchmark.history",
|
||||
scope: { kind: "conversation", conversationId: ROOT_CONVERSATION_ID },
|
||||
history: "rewindable",
|
||||
fork: "asOf",
|
||||
} satisfies DocumentCreate;
|
||||
await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "document.create",
|
||||
record: historicalRecord,
|
||||
content: { kind: "base", version: 1, value: { count: 0 } },
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
let ancientAt: Seq | undefined;
|
||||
for (let count = 1; count <= HISTORY_SEGMENT_LENGTH; count++) {
|
||||
ancientAt = await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "document.change",
|
||||
id: historicalDocumentId,
|
||||
content: { kind: "delta", version: 1, ops: [["s", ["count"], count]] },
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
}
|
||||
await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "document.change",
|
||||
id: historicalDocumentId,
|
||||
content: { kind: "base", version: 1, value: { count: HISTORY_SEGMENT_LENGTH } },
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
if (ancientAt === undefined) throw new Error("Benchmark history seed produced no commits");
|
||||
let recentAt = ancientAt;
|
||||
for (let count = HISTORY_SEGMENT_LENGTH + 1; count <= HISTORY_SEGMENT_LENGTH * 2; count++) {
|
||||
recentAt = await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "document.change",
|
||||
id: historicalDocumentId,
|
||||
content: { kind: "delta", version: 1, ops: [["s", ["count"], count]] },
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
if (firstEntryId === undefined) throw new Error("Benchmark scale must create entries");
|
||||
let parentConversationId = ROOT_CONVERSATION_ID;
|
||||
let parentAt = firstEntryId;
|
||||
let deepestConversationId = ROOT_CONVERSATION_ID;
|
||||
for (let depth = 0; depth < FORK_DEPTH; depth++) {
|
||||
const conversationId = await storage.mintId<ConversationId>();
|
||||
await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "conversation",
|
||||
value: { id: conversationId, parent: { conversationId: parentConversationId, at: parentAt } },
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
const ids = await Promise.all(Array.from({ length: ENTRIES_PER_FORK }, () => storage.mintId<EntryId>()));
|
||||
await storage.commit(
|
||||
ids.map((id, index) => ({
|
||||
type: "entry" as const,
|
||||
value: {
|
||||
id,
|
||||
conversationId,
|
||||
kind: "benchmark.fork",
|
||||
data: { depth, index },
|
||||
},
|
||||
})),
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
parentConversationId = conversationId;
|
||||
parentAt = ids.at(-1)!;
|
||||
deepestConversationId = conversationId;
|
||||
}
|
||||
|
||||
if (exactDocumentId === undefined) throw new Error("Benchmark scale must create documents");
|
||||
return {
|
||||
firstEntryId,
|
||||
filteredTaskCount: Math.min(50, Math.ceil(scale.taskCount / 60)),
|
||||
exactDocumentId,
|
||||
exactDocumentKey: `key-${scale.documentCount - 1}`,
|
||||
replayDocumentIds: Object.fromEntries(replayEntries.map(({ tail, id }) => [tail, id])) as Record<
|
||||
(typeof REPLAY_TAILS)[number],
|
||||
DocumentId
|
||||
>,
|
||||
historicalDocumentId,
|
||||
ancientAt,
|
||||
recentAt,
|
||||
deepestConversationId,
|
||||
ancestorHeadEntryId: firstEntryId,
|
||||
};
|
||||
}
|
||||
|
||||
export type StorageReadBenchmark = {
|
||||
readonly name: string;
|
||||
run(storage: Storage, dataset: StorageBenchmarkDataset): Promise<number>;
|
||||
expected(dataset: StorageBenchmarkDataset): number;
|
||||
};
|
||||
|
||||
export const STORAGE_READ_BENCHMARKS: readonly StorageReadBenchmark[] = [
|
||||
{
|
||||
name: "exact entry lookup",
|
||||
async run(storage, dataset) {
|
||||
return (await storage.entry(dataset.firstEntryId, BACKGROUND_CONTEXT))?.entry.id ?? -1;
|
||||
},
|
||||
expected: ({ firstEntryId }) => firstEntryId,
|
||||
},
|
||||
{
|
||||
name: "entry page scan (100)",
|
||||
async run(storage) {
|
||||
return (
|
||||
await storage.scanEntries({ conversationId: ROOT_CONVERSATION_ID }, 100, undefined, BACKGROUND_CONTEXT)
|
||||
).items.length;
|
||||
},
|
||||
expected: () => 100,
|
||||
},
|
||||
{
|
||||
name: "filtered task scan (50)",
|
||||
async run(storage) {
|
||||
return (
|
||||
await storage.scanTasks(
|
||||
{ kind: "benchmark.filtered", status: "pending", background: true },
|
||||
50,
|
||||
undefined,
|
||||
BACKGROUND_CONTEXT,
|
||||
)
|
||||
).items.length;
|
||||
},
|
||||
expected: ({ filteredTaskCount }) => filteredTaskCount,
|
||||
},
|
||||
{
|
||||
name: "exact document address among many",
|
||||
async run(storage, dataset) {
|
||||
return (
|
||||
(
|
||||
await storage.findDocument(
|
||||
{ kind: "benchmark.family", key: dataset.exactDocumentKey, scope: { kind: "session" } },
|
||||
"current",
|
||||
BACKGROUND_CONTEXT,
|
||||
)
|
||||
)?.id ?? -1
|
||||
);
|
||||
},
|
||||
expected: ({ exactDocumentId }) => exactDocumentId,
|
||||
},
|
||||
...REPLAY_TAILS.map(
|
||||
(tail): StorageReadBenchmark => ({
|
||||
name: `document replay tail (${tail})`,
|
||||
async run(storage, dataset) {
|
||||
return Number(
|
||||
(await storage.document(dataset.replayDocumentIds[tail], "current", BACKGROUND_CONTEXT))?.value.count,
|
||||
);
|
||||
},
|
||||
expected: () => tail,
|
||||
}),
|
||||
),
|
||||
{
|
||||
name: "ancient historical read before newer base",
|
||||
async run(storage, dataset) {
|
||||
return Number(
|
||||
(await storage.document(dataset.historicalDocumentId, dataset.ancientAt, BACKGROUND_CONTEXT))?.value.count,
|
||||
);
|
||||
},
|
||||
expected: () => HISTORY_SEGMENT_LENGTH,
|
||||
},
|
||||
{
|
||||
name: "recent historical read after newer base",
|
||||
async run(storage, dataset) {
|
||||
return Number(
|
||||
(await storage.document(dataset.historicalDocumentId, dataset.recentAt, BACKGROUND_CONTEXT))?.value.count,
|
||||
);
|
||||
},
|
||||
expected: () => HISTORY_SEGMENT_LENGTH * 2,
|
||||
},
|
||||
{
|
||||
name: "fork-depth history scan (100)",
|
||||
async run(storage, dataset) {
|
||||
return (
|
||||
await storage.scanEntries(
|
||||
{ conversationId: dataset.deepestConversationId },
|
||||
100,
|
||||
undefined,
|
||||
BACKGROUND_CONTEXT,
|
||||
)
|
||||
).items.length;
|
||||
},
|
||||
expected: () => 100,
|
||||
},
|
||||
{
|
||||
name: "fork-depth head lookup",
|
||||
async run(storage, dataset) {
|
||||
return (
|
||||
(await storage.findLatestHeadMarker(dataset.deepestConversationId, undefined, BACKGROUND_CONTEXT))?.id ?? -1
|
||||
);
|
||||
},
|
||||
expected: ({ ancestorHeadEntryId }) => ancestorHeadEntryId,
|
||||
},
|
||||
];
|
||||
|
||||
export type StorageWriteBenchmark = {
|
||||
readonly name: string;
|
||||
readonly expected: number;
|
||||
run(storage: Storage): Promise<number>;
|
||||
};
|
||||
|
||||
/** Seed the common state expected by every write benchmark sample. */
|
||||
export async function seedStorageWriteBenchmark(storage: Storage): Promise<void> {
|
||||
await storage.commit([{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], BACKGROUND_CONTEXT);
|
||||
await storage.commit(
|
||||
await Promise.all(
|
||||
Array.from({ length: 100 }, async (_, index) => ({
|
||||
type: "entry" as const,
|
||||
value: {
|
||||
id: await storage.mintId<EntryId>(),
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
kind: "benchmark.baseline",
|
||||
data: { index },
|
||||
},
|
||||
})),
|
||||
),
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
export const STORAGE_WRITE_BENCHMARKS: readonly StorageWriteBenchmark[] = [
|
||||
{
|
||||
name: "commit one entry",
|
||||
expected: 1,
|
||||
async run(storage) {
|
||||
const id = await storage.mintId<EntryId>();
|
||||
await storage.commit(
|
||||
[
|
||||
{
|
||||
type: "entry",
|
||||
value: {
|
||||
id,
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
kind: "benchmark.write",
|
||||
data: { text: "x".repeat(128) },
|
||||
},
|
||||
},
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "commit 100 entries",
|
||||
expected: 100,
|
||||
async run(storage) {
|
||||
const writes = await Promise.all(
|
||||
Array.from(
|
||||
{ length: 100 },
|
||||
async (_, index): Promise<StorageWrite> => ({
|
||||
type: "entry",
|
||||
value: {
|
||||
id: await storage.mintId<EntryId>(),
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
kind: "benchmark.write",
|
||||
data: { index, text: "x".repeat(128) },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
await storage.commit(writes, BACKGROUND_CONTEXT);
|
||||
return writes.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "commit mixed entry/task/submission/document",
|
||||
expected: 4,
|
||||
async run(storage) {
|
||||
const entryId = await storage.mintId<EntryId>();
|
||||
const taskId = await storage.mintId<TaskId<JsonValue>>();
|
||||
const submissionId = await storage.mintId<SubmissionId>();
|
||||
const documentId = await storage.mintId<DocumentId>();
|
||||
const writes: readonly StorageWrite[] = [
|
||||
{
|
||||
type: "entry",
|
||||
value: { id: entryId, conversationId: ROOT_CONVERSATION_ID, kind: "benchmark.mixed" },
|
||||
},
|
||||
{ type: "task", value: task(taskId, taskId) },
|
||||
{
|
||||
type: "submission",
|
||||
value: {
|
||||
id: submissionId,
|
||||
conversationId: ROOT_CONVERSATION_ID,
|
||||
requestId: `benchmark-${submissionId}`,
|
||||
type: "write",
|
||||
status: "done",
|
||||
entry: entryId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "document.create",
|
||||
record: { id: documentId, kind: "benchmark.mixed", key: String(documentId), scope: { kind: "session" } },
|
||||
content: { kind: "base", version: 1, value: { entryId, taskId } },
|
||||
},
|
||||
];
|
||||
await storage.commit(writes, BACKGROUND_CONTEXT);
|
||||
return writes.length;
|
||||
},
|
||||
},
|
||||
];
|
||||
1663
third_party/pi/packages/durable/src/testing/storage-conformance.ts
vendored
Normal file
1663
third_party/pi/packages/durable/src/testing/storage-conformance.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
46
third_party/pi/packages/durable/src/testing/types.ts
vendored
Normal file
46
third_party/pi/packages/durable/src/testing/types.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ExecutionEnv } from "../env/index.ts";
|
||||
import type { Storage } from "../types.ts";
|
||||
|
||||
export interface StorageConformanceAssertions {
|
||||
ok(value: unknown, message?: string): void;
|
||||
strictEqual(actual: unknown, expected: unknown): void;
|
||||
deepEqual(actual: unknown, expected: unknown): void;
|
||||
partialDeepEqual(actual: unknown, expected: unknown): void;
|
||||
greaterThan(actual: number, expected: number): void;
|
||||
rejects(operation: Promise<unknown>, messageIncludes: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type StorageConformanceProvider = (use: (storage: Storage) => Promise<void>) => Promise<void>;
|
||||
|
||||
export interface StorageConformanceOptions {
|
||||
readonly assertions: StorageConformanceAssertions;
|
||||
readonly withStorage: StorageConformanceProvider;
|
||||
}
|
||||
|
||||
export interface StorageConformanceCase {
|
||||
readonly name: string;
|
||||
run(): Promise<void>;
|
||||
}
|
||||
|
||||
export type EnvConformanceAssertions = StorageConformanceAssertions;
|
||||
|
||||
/** Calls `use` once with an environment whose `cwd` is a fresh, empty, writable directory, then cleans up. */
|
||||
export type EnvConformanceProvider = (use: (env: ExecutionEnv) => Promise<void>) => Promise<void>;
|
||||
|
||||
export interface EnvConformanceOptions {
|
||||
readonly assertions: EnvConformanceAssertions;
|
||||
readonly withEnv: EnvConformanceProvider;
|
||||
/** Program and flag that run a POSIX shell script from the next argument; default `["sh", "-c"]`. */
|
||||
readonly shell?: readonly string[];
|
||||
/** Whether the shell's `ln -s` creates symbolic links; default true. */
|
||||
readonly symlinks?: boolean;
|
||||
/** Whether the environment implements the optional `listFiles`, which the suite then checks; default false. */
|
||||
readonly listFiles?: boolean;
|
||||
}
|
||||
|
||||
export interface EnvConformanceCase {
|
||||
readonly name: string;
|
||||
/** Cases that wait for an environment's watch latency need longer than a test runner's default timeout. */
|
||||
readonly timeoutMs?: number;
|
||||
run(): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user