mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 04:52:58 +00:00
test: recover review harness vault fixture
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export type ReviewHarnessScenarioStatus =
|
||||
| "idle"
|
||||
| "queued"
|
||||
| "running"
|
||||
| "waiting-for-user"
|
||||
| "passed"
|
||||
| "failed"
|
||||
| "cancelled";
|
||||
|
||||
export interface ReviewHarnessScenarioResult {
|
||||
readonly status: ReviewHarnessScenarioStatus;
|
||||
readonly detail: string;
|
||||
readonly observations: readonly string[];
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ReviewHarnessScenarioResult } from "./reviewHarnessTypes";
|
||||
|
||||
export const REVIEW_HARNESS_FIXTURE_ROOT = "__self-hosted-livesync-review-harness__";
|
||||
export const REVIEW_HARNESS_SOURCE_FILE = `${REVIEW_HARNESS_FIXTURE_ROOT}/round-trip.md`;
|
||||
export const REVIEW_HARNESS_RENAMED_FILE = `${REVIEW_HARNESS_FIXTURE_ROOT}/round-trip-renamed.md`;
|
||||
|
||||
const CREATED_CONTENT = "review-harness:created\n";
|
||||
const MODIFIED_CONTENT = "review-harness:modified\n";
|
||||
|
||||
export interface ReviewHarnessVaultFixtureRuntime<TFile> {
|
||||
confirmFixtureAccess(): Promise<boolean>;
|
||||
fixtureRootExists(): boolean;
|
||||
createFixtureRoot(): Promise<void>;
|
||||
createFile(path: string, content: string): Promise<TFile>;
|
||||
readFile(file: TFile): Promise<string>;
|
||||
modifyFile(file: TFile, content: string): Promise<void>;
|
||||
renameFile(file: TFile, path: string): Promise<void>;
|
||||
filePath(file: TFile): string;
|
||||
removeFixtureRoot(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exercises a single, fixed fixture tree in a dedicated Vault.
|
||||
*
|
||||
* The runtime owns the platform adapter. This function owns the safety boundary:
|
||||
* it refuses a pre-existing fixture root, tracks whether it created the root, and
|
||||
* removes only that owned root in a `finally` block.
|
||||
*/
|
||||
export async function runReviewHarnessVaultRoundTrip<TFile>(
|
||||
runtime: ReviewHarnessVaultFixtureRuntime<TFile>
|
||||
): Promise<ReviewHarnessScenarioResult> {
|
||||
if (!(await runtime.confirmFixtureAccess())) {
|
||||
return {
|
||||
status: "cancelled",
|
||||
detail: "Vault fixture access was not approved.",
|
||||
observations: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (runtime.fixtureRootExists()) {
|
||||
return {
|
||||
status: "failed",
|
||||
detail: "The owned fixture root already exists. It was left untouched.",
|
||||
observations: [],
|
||||
};
|
||||
}
|
||||
|
||||
let fixtureOwned = false;
|
||||
try {
|
||||
await runtime.createFixtureRoot();
|
||||
fixtureOwned = true;
|
||||
const file = await runtime.createFile(REVIEW_HARNESS_SOURCE_FILE, CREATED_CONTENT);
|
||||
const created = await runtime.readFile(file);
|
||||
if (created !== CREATED_CONTENT) throw new Error("Created fixture content did not round-trip.");
|
||||
|
||||
await runtime.modifyFile(file, MODIFIED_CONTENT);
|
||||
const modified = await runtime.readFile(file);
|
||||
if (modified !== MODIFIED_CONTENT) throw new Error("Modified fixture content did not round-trip.");
|
||||
|
||||
await runtime.renameFile(file, REVIEW_HARNESS_RENAMED_FILE);
|
||||
if (runtime.filePath(file) !== REVIEW_HARNESS_RENAMED_FILE) {
|
||||
throw new Error("The fixture rename did not update its path.");
|
||||
}
|
||||
const renamed = await runtime.readFile(file);
|
||||
if (renamed !== MODIFIED_CONTENT) throw new Error("Renamed fixture content was not preserved.");
|
||||
|
||||
return {
|
||||
status: "passed",
|
||||
detail: "The owned fixture tree completed its round trip and was removed.",
|
||||
observations: ["create", "read", "modify", "rename", "read-after-rename", "cleanup"],
|
||||
};
|
||||
} finally {
|
||||
if (fixtureOwned) await runtime.removeFixtureRoot();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
REVIEW_HARNESS_RENAMED_FILE,
|
||||
REVIEW_HARNESS_SOURCE_FILE,
|
||||
runReviewHarnessVaultRoundTrip,
|
||||
type ReviewHarnessVaultFixtureRuntime,
|
||||
} from "./reviewHarnessVaultFixture";
|
||||
|
||||
interface FixtureFile {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function createRuntime(): ReviewHarnessVaultFixtureRuntime<FixtureFile> & {
|
||||
events: string[];
|
||||
rootExists: boolean;
|
||||
} {
|
||||
const runtime: ReviewHarnessVaultFixtureRuntime<FixtureFile> & {
|
||||
events: string[];
|
||||
rootExists: boolean;
|
||||
} = {
|
||||
events: [],
|
||||
rootExists: false,
|
||||
confirmFixtureAccess: vi.fn(async () => true),
|
||||
fixtureRootExists() {
|
||||
return this.rootExists;
|
||||
},
|
||||
async createFixtureRoot() {
|
||||
this.events.push("create-root");
|
||||
this.rootExists = true;
|
||||
},
|
||||
async createFile(path, content) {
|
||||
this.events.push(`create-file:${path}`);
|
||||
return { path, content };
|
||||
},
|
||||
async readFile(file) {
|
||||
this.events.push(`read:${file.path}`);
|
||||
return file.content;
|
||||
},
|
||||
async modifyFile(file, content) {
|
||||
this.events.push(`modify:${file.path}`);
|
||||
file.content = content;
|
||||
},
|
||||
async renameFile(file, path) {
|
||||
this.events.push(`rename:${file.path}->${path}`);
|
||||
file.path = path;
|
||||
},
|
||||
filePath: (file) => file.path,
|
||||
async removeFixtureRoot() {
|
||||
this.events.push("remove-root");
|
||||
this.rootExists = false;
|
||||
},
|
||||
};
|
||||
return runtime;
|
||||
}
|
||||
|
||||
describe("runReviewHarnessVaultRoundTrip", () => {
|
||||
it("does not inspect or mutate the Vault when access is declined", async () => {
|
||||
const runtime = createRuntime();
|
||||
vi.mocked(runtime.confirmFixtureAccess).mockResolvedValue(false);
|
||||
const exists = vi.spyOn(runtime, "fixtureRootExists");
|
||||
|
||||
await expect(runReviewHarnessVaultRoundTrip(runtime)).resolves.toMatchObject({ status: "cancelled" });
|
||||
|
||||
expect(exists).not.toHaveBeenCalled();
|
||||
expect(runtime.events).toEqual([]);
|
||||
});
|
||||
|
||||
it("leaves a pre-existing fixture root untouched", async () => {
|
||||
const runtime = createRuntime();
|
||||
runtime.rootExists = true;
|
||||
|
||||
await expect(runReviewHarnessVaultRoundTrip(runtime)).resolves.toMatchObject({ status: "failed" });
|
||||
|
||||
expect(runtime.events).toEqual([]);
|
||||
expect(runtime.rootExists).toBe(true);
|
||||
});
|
||||
|
||||
it("completes the round trip and removes its owned fixture root", async () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
await expect(runReviewHarnessVaultRoundTrip(runtime)).resolves.toMatchObject({ status: "passed" });
|
||||
|
||||
expect(runtime.events).toEqual([
|
||||
"create-root",
|
||||
`create-file:${REVIEW_HARNESS_SOURCE_FILE}`,
|
||||
`read:${REVIEW_HARNESS_SOURCE_FILE}`,
|
||||
`modify:${REVIEW_HARNESS_SOURCE_FILE}`,
|
||||
`read:${REVIEW_HARNESS_SOURCE_FILE}`,
|
||||
`rename:${REVIEW_HARNESS_SOURCE_FILE}->${REVIEW_HARNESS_RENAMED_FILE}`,
|
||||
`read:${REVIEW_HARNESS_RENAMED_FILE}`,
|
||||
"remove-root",
|
||||
]);
|
||||
expect(runtime.rootExists).toBe(false);
|
||||
});
|
||||
|
||||
it("removes an owned fixture root when an operation fails", async () => {
|
||||
const runtime = createRuntime();
|
||||
runtime.modifyFile = vi.fn(async () => {
|
||||
throw new Error("fixture write failed");
|
||||
});
|
||||
|
||||
await expect(runReviewHarnessVaultRoundTrip(runtime)).rejects.toThrow("fixture write failed");
|
||||
|
||||
expect(runtime.events[runtime.events.length - 1]).toBe("remove-root");
|
||||
expect(runtime.rootExists).toBe(false);
|
||||
});
|
||||
|
||||
it("does not remove a root when creating it fails", async () => {
|
||||
const runtime = createRuntime();
|
||||
runtime.createFixtureRoot = vi.fn(async () => {
|
||||
throw new Error("root creation failed");
|
||||
});
|
||||
|
||||
await expect(runReviewHarnessVaultRoundTrip(runtime)).rejects.toThrow("root creation failed");
|
||||
|
||||
expect(runtime.events).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user