Initial Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import {
|
||||
createStorageUriResolver,
|
||||
type UriPlugin,
|
||||
} from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveUriPlugin } from "../../src/plugins/drive/drive.js";
|
||||
|
||||
interface FileUriOptions {
|
||||
readonly rootDir: string;
|
||||
}
|
||||
|
||||
function fileUriPlugin(options: FileUriOptions): UriPlugin {
|
||||
const adapter = createNodeFsStorage({
|
||||
rootDir: options.rootDir,
|
||||
mkdirp: true,
|
||||
});
|
||||
return {
|
||||
scheme: "file",
|
||||
adapter,
|
||||
parse(uri: string) {
|
||||
// file:///some/path -> key = "some/path"
|
||||
const idx = uri.indexOf("://");
|
||||
const after = uri.slice(idx + 3);
|
||||
const pathStart = after.indexOf("/");
|
||||
const path = pathStart >= 0 ? after.slice(pathStart + 1) : after;
|
||||
return { key: path };
|
||||
},
|
||||
build(key: string) {
|
||||
return `file:///${key.replace(/^\//, "")}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("compose-with-storage-uri integration", () => {
|
||||
let rootDir: string;
|
||||
let driveRoot: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-file-"));
|
||||
driveRoot = mkdtempSync(join(tmpdir(), "drive-uri-drive-"));
|
||||
const driveStorage = Object.assign(
|
||||
createNodeFsStorage({ rootDir: driveRoot, mkdirp: true }),
|
||||
{ id: "default" }
|
||||
);
|
||||
drive = new Drive(driveStorage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
rmSync(driveRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("file:// and drive:// resolve through one StorageUriResolver", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const resolver = createStorageUriResolver([
|
||||
fileUriPlugin({ rootDir }),
|
||||
driveUriPlugin({ drive, driveId: "primary" }),
|
||||
]);
|
||||
|
||||
// Seed both backends with distinct bytes.
|
||||
await drive.write("a.bin", new Uint8Array([0xaa]));
|
||||
// Use the file plugin directly to write — the resolver rejects
|
||||
// writes for drive:// (by design) so we exercise the file side via
|
||||
// resolver.write to confirm it works.
|
||||
await resolver.write("file:///b.bin", new Uint8Array([0xbb]));
|
||||
|
||||
const fileBytes = await resolver.read("file:///b.bin");
|
||||
const driveBytes = await resolver.read("drive://primary/a.bin");
|
||||
expect(Array.from(fileBytes)).toEqual([0xbb]);
|
||||
expect(Array.from(driveBytes)).toEqual([0xaa]);
|
||||
|
||||
// Neither plugin sees the other's state.
|
||||
expect(resolver.hasScheme("file")).toBe(true);
|
||||
expect(resolver.hasScheme("drive")).toBe(true);
|
||||
expect(resolver.schemeCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { driveUriPlugin } from "../../src/plugins/drive/drive.js";
|
||||
|
||||
describe("drive-roundtrip integration", () => {
|
||||
let rootDir: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-roundtrip-"));
|
||||
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("writes a file via Drive.write then reads it via resolver.read('drive://')", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const payload = new Uint8Array([0x4d, 0x4f, 0x56, 0x21]);
|
||||
await drive.write("folder/clip.mov", payload);
|
||||
const bytes = await resolver.read("drive://test-drive/folder/clip.mov");
|
||||
expect(Array.from(bytes)).toEqual(Array.from(payload));
|
||||
});
|
||||
|
||||
it("resolver.open streams the file via Drive.createReadStream", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await drive.write("clip.dat", payload);
|
||||
const stream = await resolver.open("drive://test-drive/clip.dat");
|
||||
const reader = stream.getReader();
|
||||
const chunks: number[] = [];
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value !== undefined) chunks.push(...value);
|
||||
}
|
||||
reader.releaseLock();
|
||||
expect(chunks).toEqual(Array.from(payload));
|
||||
});
|
||||
|
||||
it("resolver.remove deletes the file", { timeout: 30000 }, async () => {
|
||||
const plugin = driveUriPlugin({ drive, driveId: "test-drive" });
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
await drive.write("doomed.bin", new Uint8Array([42]));
|
||||
const removed = await resolver.remove("drive://test-drive/doomed.bin");
|
||||
expect(removed).toBe(true);
|
||||
// A subsequent read should yield STORAGE_URI.NOT_FOUND (the
|
||||
// adapter translates Drive's not-found into undefined, and the
|
||||
// resolver maps undefined to NOT_FOUND).
|
||||
await expect(
|
||||
resolver.read("drive://test-drive/doomed.bin")
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import {
|
||||
DRIVE_URI_CODES,
|
||||
DriveUriError,
|
||||
} from "../../src/error-codes/error-codes.js";
|
||||
import { driveTemplateUriPlugin } from "../../src/plugins/drive-template/drive-template.js";
|
||||
|
||||
describe("template-roundtrip integration", () => {
|
||||
let rootDir: string;
|
||||
let drive: Drive;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), "drive-uri-template-"));
|
||||
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
|
||||
const storage = Object.assign(fileStorage, { id: "default" });
|
||||
drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drive.dispose();
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("real Drive.hydrateFrom rejects unsigned template as TEMPLATE_INVALID", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const provider = {
|
||||
async fetchTemplate(): Promise<unknown> {
|
||||
// Returning an obviously-malformed template forces
|
||||
// Drive.hydrateFrom's validation pipeline to reject.
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive,
|
||||
templateProvider: provider as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["templateProvider"],
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
// SuaveplanError subclasses pass through the resolver
|
||||
// unchanged, so the caller sees the DriveUriError directly.
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_INVALID);
|
||||
// The inner cause is the real DTP_*/DRIVE error surfaced by
|
||||
// Drive's hydrateFrom — confirming the wrapping path is wired
|
||||
// through real package code, not a stub.
|
||||
expect(e.cause).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("provider rejection becomes TEMPLATE_NOT_AVAILABLE via real Drive", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const original = new Error("network down");
|
||||
const provider = {
|
||||
async fetchTemplate(): Promise<never> {
|
||||
throw original;
|
||||
},
|
||||
};
|
||||
const plugin = driveTemplateUriPlugin({
|
||||
drive,
|
||||
templateProvider: provider as unknown as Parameters<
|
||||
typeof driveTemplateUriPlugin
|
||||
>[0]["templateProvider"],
|
||||
});
|
||||
const resolver = createStorageUriResolver([plugin]);
|
||||
try {
|
||||
await resolver.read("drive+template://templA/x");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DriveUriError);
|
||||
const e = err as DriveUriError;
|
||||
expect(e.code).toBe(DRIVE_URI_CODES.TEMPLATE_NOT_AVAILABLE);
|
||||
expect(e.cause).toBe(original);
|
||||
}
|
||||
});
|
||||
});
|
||||
151
packages/networking/drive/drive-uri/tests/setup/fake-drive.ts
Normal file
151
packages/networking/drive/drive-uri/tests/setup/fake-drive.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { Drive } from "@suaveplan/drive";
|
||||
import type { DriveTemplate } from "@suaveplan/drive-template";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Real, hand-written `Drive`-shaped test double. NOT a mock — it is a real
|
||||
* class implementing every method consumed by `@suaveplan/drive-uri`. Used
|
||||
* for unit-tier tests where spinning up a full `Drive + storage-file +
|
||||
* chunksPlugin` would be excessive; integration tests use the real `Drive`.
|
||||
*
|
||||
* Per CLAUDE.md rule 2 this is acceptable because:
|
||||
* - the class is concrete, with deterministic behaviour
|
||||
* - no `vi.mock`/`jest.mock` is involved
|
||||
* - it implements the structural contract drive-uri consumes
|
||||
*/
|
||||
export class FakeDriveError extends SuaveplanError<string> {
|
||||
public constructor(
|
||||
code: string,
|
||||
message: string,
|
||||
options?: { cause?: unknown }
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "FakeDriveError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface FakeDriveCalls {
|
||||
read: Array<{ path: string; options?: { signal?: AbortSignal } }>;
|
||||
write: Array<{ path: string; data: Uint8Array }>;
|
||||
delete: Array<{ path: string }>;
|
||||
stat: Array<{ path: string }>;
|
||||
list: Array<{ prefix?: string }>;
|
||||
createReadStream: Array<{ path: string; signal?: AbortSignal }>;
|
||||
hydrateFrom: Array<{
|
||||
template: DriveTemplate;
|
||||
options?: { signal?: AbortSignal };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface FakeDriveOptions {
|
||||
readonly contents?: Map<string, Uint8Array>;
|
||||
readonly readError?: () => unknown;
|
||||
readonly hydrateError?: () => unknown;
|
||||
}
|
||||
|
||||
export class FakeDrive {
|
||||
public readonly clock = undefined;
|
||||
public readonly rng = undefined;
|
||||
public readonly logger = undefined;
|
||||
public readonly events = undefined;
|
||||
public readonly calls: FakeDriveCalls = {
|
||||
read: [],
|
||||
write: [],
|
||||
delete: [],
|
||||
stat: [],
|
||||
list: [],
|
||||
createReadStream: [],
|
||||
hydrateFrom: [],
|
||||
};
|
||||
readonly #contents: Map<string, Uint8Array>;
|
||||
readonly #options: FakeDriveOptions;
|
||||
|
||||
public constructor(options: FakeDriveOptions = {}) {
|
||||
this.#contents = options.contents ?? new Map();
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
public async read(
|
||||
path: string,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<Uint8Array> {
|
||||
const entry: { path: string; options?: { signal?: AbortSignal } } = {
|
||||
path,
|
||||
};
|
||||
if (options !== undefined) entry.options = options;
|
||||
this.calls.read.push(entry);
|
||||
if (this.#options.readError !== undefined) {
|
||||
throw this.#options.readError();
|
||||
}
|
||||
const bytes = this.#contents.get(path);
|
||||
if (bytes === undefined) {
|
||||
throw new FakeDriveError(
|
||||
"DRIVE.NOT_FOUND",
|
||||
`no entry for '${path}'`
|
||||
);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public async write(path: string, data: Uint8Array): Promise<void> {
|
||||
this.calls.write.push({ path, data });
|
||||
this.#contents.set(path, data);
|
||||
}
|
||||
|
||||
public async delete(path: string): Promise<boolean> {
|
||||
this.calls.delete.push({ path });
|
||||
return this.#contents.delete(path);
|
||||
}
|
||||
|
||||
public async stat(path: string): Promise<unknown | null> {
|
||||
this.calls.stat.push({ path });
|
||||
const bytes = this.#contents.get(path);
|
||||
if (bytes === undefined) return null;
|
||||
return { path, size: bytes.byteLength };
|
||||
}
|
||||
|
||||
public async list(prefix?: string): Promise<unknown> {
|
||||
this.calls.list.push({ ...(prefix !== undefined ? { prefix } : {}) });
|
||||
const keys = Array.from(this.#contents.keys()).filter((k) =>
|
||||
prefix === undefined || prefix === "" ? true : k.startsWith(prefix)
|
||||
);
|
||||
return { keys, total: keys.length, hasMore: false };
|
||||
}
|
||||
|
||||
public createReadStream(
|
||||
path: string,
|
||||
signal?: AbortSignal
|
||||
): ReadableStream<Uint8Array> {
|
||||
const entry: { path: string; signal?: AbortSignal } = { path };
|
||||
if (signal !== undefined) entry.signal = signal;
|
||||
this.calls.createReadStream.push(entry);
|
||||
const bytes = this.#contents.get(path);
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(ctrl) {
|
||||
if (bytes !== undefined) ctrl.enqueue(bytes);
|
||||
ctrl.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async hydrateFrom(
|
||||
template: DriveTemplate,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<{ filesAdded: number }> {
|
||||
const entry: {
|
||||
template: DriveTemplate;
|
||||
options?: { signal?: AbortSignal };
|
||||
} = { template };
|
||||
if (options !== undefined) entry.options = options;
|
||||
this.calls.hydrateFrom.push(entry);
|
||||
if (this.#options.hydrateError !== undefined) {
|
||||
throw this.#options.hydrateError();
|
||||
}
|
||||
return { filesAdded: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a `Drive`-typed reference to the fake — narrows for ergonomic use. */
|
||||
export function asDrive(fake: FakeDrive): Drive {
|
||||
return fake as unknown as Drive;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Polyfills for test environment — none required; Node 18+ provides
|
||||
// `URL`, `AbortController`, `ReadableStream`, and `Response` natively.
|
||||
// File exists so vitest has a stable setup entry and to mirror sibling
|
||||
// drive packages.
|
||||
export {};
|
||||
Reference in New Issue
Block a user