152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
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;
|
|
}
|