obsidian-e2e 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1558 @@
1
+ import { n as parseNoteDocument, t as createNoteDocument } from "./document-DunL2Moz.mjs";
2
+ import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import path, { posix } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import os from "node:os";
7
+ //#region src/core/args.ts
8
+ function buildCommandArgv(vaultName, command, args = {}) {
9
+ const argv = [`vault=${vaultName}`, command];
10
+ for (const [key, value] of Object.entries(args)) {
11
+ if (value === false || value === null || value === void 0) continue;
12
+ if (value === true) {
13
+ argv.push(key);
14
+ continue;
15
+ }
16
+ argv.push(`${key}=${String(value)}`);
17
+ }
18
+ return argv;
19
+ }
20
+ //#endregion
21
+ //#region src/dev/harness.ts
22
+ const HARNESS_NAMESPACE = "__obsidianE2E";
23
+ const HARNESS_VERSION = 1;
24
+ function buildHarnessCallCode(method, ...args) {
25
+ return `(() => {
26
+ const __obsidianE2EMethod = ${JSON.stringify(method)};
27
+ const __obsidianE2EArgs = ${JSON.stringify(args)};
28
+ const __obsidianE2ENamespace = ${JSON.stringify(HARNESS_NAMESPACE)};
29
+ const __obsidianE2EVersion = ${HARNESS_VERSION};
30
+ ${HARNESS_RUNTIME}
31
+ })()`;
32
+ }
33
+ function parseHarnessEnvelope(raw) {
34
+ const envelope = JSON.parse(raw.startsWith("=> ") ? raw.slice(3) : raw);
35
+ if (!envelope.ok) throw envelope.error;
36
+ return decodeHarnessValue(envelope.value);
37
+ }
38
+ function decodeHarnessValue(value) {
39
+ if (Array.isArray(value)) return value.map((entry) => decodeHarnessValue(entry));
40
+ if (!value || typeof value !== "object") return value;
41
+ if ("__obsidianE2EType" in value && value.__obsidianE2EType === "undefined") return;
42
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeHarnessValue(entry)]));
43
+ }
44
+ function createDevDiagnostics(value) {
45
+ return {
46
+ consoleMessages: value?.consoleMessages ?? [],
47
+ notices: value?.notices ?? [],
48
+ runtimeErrors: value?.runtimeErrors ?? []
49
+ };
50
+ }
51
+ const HARNESS_RUNTIME = String.raw`
52
+ const __obsidianE2EMaxEntries = 100;
53
+
54
+ const __obsidianE2EPush = (entries, value) => {
55
+ if (entries.length >= __obsidianE2EMaxEntries) {
56
+ entries.shift();
57
+ }
58
+ entries.push(value);
59
+ };
60
+
61
+ const __obsidianE2EFormat = (value) => {
62
+ if (typeof value === "string") {
63
+ return value;
64
+ }
65
+
66
+ try {
67
+ return JSON.stringify(value);
68
+ } catch {
69
+ return String(value);
70
+ }
71
+ };
72
+
73
+ const __obsidianE2ESerialize = (value, path = "$") => {
74
+ if (value === null) {
75
+ return null;
76
+ }
77
+
78
+ if (value === undefined) {
79
+ return { __obsidianE2EType: "undefined" };
80
+ }
81
+
82
+ const valueType = typeof value;
83
+
84
+ if (valueType === "string" || valueType === "boolean") {
85
+ return value;
86
+ }
87
+
88
+ if (valueType === "number") {
89
+ if (!Number.isFinite(value)) {
90
+ throw new Error(\`Cannot serialize non-finite number at \${path}.\`);
91
+ }
92
+
93
+ return value;
94
+ }
95
+
96
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") {
97
+ throw new Error(\`Cannot serialize \${valueType} at \${path}.\`);
98
+ }
99
+
100
+ if (Array.isArray(value)) {
101
+ return value.map((item, index) => __obsidianE2ESerialize(item, \`\${path}[\${index}]\`));
102
+ }
103
+
104
+ const prototype = Object.getPrototypeOf(value);
105
+
106
+ if (prototype !== Object.prototype && prototype !== null) {
107
+ throw new Error(\`Cannot serialize non-plain object at \${path}.\`);
108
+ }
109
+
110
+ const next = {};
111
+
112
+ for (const [key, entry] of Object.entries(value)) {
113
+ next[key] = __obsidianE2ESerialize(entry, \`\${path}.\${key}\`);
114
+ }
115
+
116
+ return next;
117
+ };
118
+
119
+ const __obsidianE2EClone = (value) => JSON.parse(JSON.stringify(__obsidianE2ESerialize(value)));
120
+
121
+ const __obsidianE2ECreateHarness = () => {
122
+ const state = {
123
+ consoleMessages: [],
124
+ notices: [],
125
+ runtimeErrors: [],
126
+ };
127
+
128
+ const pushConsoleMessage = (level, args) => {
129
+ __obsidianE2EPush(state.consoleMessages, {
130
+ args: args.map((entry) => {
131
+ try {
132
+ return __obsidianE2EClone(entry);
133
+ } catch {
134
+ return __obsidianE2EFormat(entry);
135
+ }
136
+ }),
137
+ at: Date.now(),
138
+ level,
139
+ text: args.map(__obsidianE2EFormat).join(" "),
140
+ });
141
+ };
142
+
143
+ const pushRuntimeError = (source, errorLike) => {
144
+ const message =
145
+ errorLike && typeof errorLike === "object" && "message" in errorLike
146
+ ? String(errorLike.message)
147
+ : String(errorLike);
148
+ const stack =
149
+ errorLike && typeof errorLike === "object" && "stack" in errorLike
150
+ ? String(errorLike.stack)
151
+ : undefined;
152
+
153
+ __obsidianE2EPush(state.runtimeErrors, {
154
+ at: Date.now(),
155
+ message,
156
+ source,
157
+ stack,
158
+ });
159
+ };
160
+
161
+ const installConsolePatch = (root) => {
162
+ if (root.__obsidianE2EConsolePatched) {
163
+ return;
164
+ }
165
+
166
+ for (const level of ["debug", "error", "info", "log", "warn"]) {
167
+ const original = root.console?.[level];
168
+
169
+ if (typeof original !== "function") {
170
+ continue;
171
+ }
172
+
173
+ root.console[level] = (...args) => {
174
+ pushConsoleMessage(level, args);
175
+ return original.apply(root.console, args);
176
+ };
177
+ }
178
+
179
+ root.__obsidianE2EConsolePatched = true;
180
+ };
181
+
182
+ const installRuntimePatch = (root) => {
183
+ if (root.__obsidianE2ERuntimePatched || typeof root.addEventListener !== "function") {
184
+ return;
185
+ }
186
+
187
+ root.addEventListener("error", (event) => {
188
+ pushRuntimeError("error", event?.error ?? event?.message ?? "Unknown error");
189
+ });
190
+ root.addEventListener("unhandledrejection", (event) => {
191
+ pushRuntimeError("unhandledrejection", event?.reason ?? "Unhandled rejection");
192
+ });
193
+
194
+ root.__obsidianE2ERuntimePatched = true;
195
+ };
196
+
197
+ const installNoticePatch = (root) => {
198
+ if (root.__obsidianE2ENoticePatched || typeof root.Notice !== "function") {
199
+ return;
200
+ }
201
+
202
+ const OriginalNotice = root.Notice;
203
+ root.Notice = new Proxy(OriginalNotice, {
204
+ construct(target, ctorArgs, newTarget) {
205
+ __obsidianE2EPush(state.notices, {
206
+ at: Date.now(),
207
+ message: __obsidianE2EFormat(ctorArgs[0] ?? ""),
208
+ timeout:
209
+ typeof ctorArgs[1] === "number" && Number.isFinite(ctorArgs[1])
210
+ ? ctorArgs[1]
211
+ : undefined,
212
+ });
213
+
214
+ return Reflect.construct(target, ctorArgs, newTarget);
215
+ },
216
+ });
217
+ root.__obsidianE2ENoticePatched = true;
218
+ };
219
+
220
+ const ensureInstalled = () => {
221
+ const root = globalThis;
222
+ installConsolePatch(root);
223
+ installRuntimePatch(root);
224
+ installNoticePatch(root);
225
+ };
226
+
227
+ const getFileCache = (vaultPath) => {
228
+ const file = app?.vault?.getAbstractFileByPath?.(vaultPath);
229
+ if (!file) {
230
+ return null;
231
+ }
232
+
233
+ return app?.metadataCache?.getFileCache?.(file) ?? null;
234
+ };
235
+
236
+ return {
237
+ activeFilePath() {
238
+ ensureInstalled();
239
+ return app?.workspace?.getActiveFile?.()?.path ?? null;
240
+ },
241
+ consoleMessages() {
242
+ ensureInstalled();
243
+ return state.consoleMessages;
244
+ },
245
+ diagnostics() {
246
+ ensureInstalled();
247
+ return state;
248
+ },
249
+ editorText() {
250
+ ensureInstalled();
251
+ return app?.workspace?.activeLeaf?.view?.editor?.getValue?.() ?? null;
252
+ },
253
+ eval(code) {
254
+ ensureInstalled();
255
+ return (0, eval)(code);
256
+ },
257
+ frontmatter(vaultPath) {
258
+ ensureInstalled();
259
+ return getFileCache(vaultPath)?.frontmatter ?? null;
260
+ },
261
+ metadata(vaultPath) {
262
+ ensureInstalled();
263
+ return getFileCache(vaultPath);
264
+ },
265
+ notices() {
266
+ ensureInstalled();
267
+ return state.notices;
268
+ },
269
+ pluginLoaded(pluginId) {
270
+ ensureInstalled();
271
+ const plugins = app?.plugins;
272
+ return Boolean(
273
+ plugins?.enabledPlugins?.has?.(pluginId) &&
274
+ plugins?.plugins?.[pluginId],
275
+ );
276
+ },
277
+ resetDiagnostics() {
278
+ state.consoleMessages.splice(0);
279
+ state.notices.splice(0);
280
+ state.runtimeErrors.splice(0);
281
+ ensureInstalled();
282
+ return true;
283
+ },
284
+ runtimeErrors() {
285
+ ensureInstalled();
286
+ return state.runtimeErrors;
287
+ },
288
+ };
289
+ };
290
+
291
+ const root = globalThis;
292
+ const current = root[__obsidianE2ENamespace];
293
+ const harness =
294
+ current && current.version === __obsidianE2EVersion
295
+ ? current
296
+ : (root[__obsidianE2ENamespace] = {
297
+ api: __obsidianE2ECreateHarness(),
298
+ version: __obsidianE2EVersion,
299
+ });
300
+
301
+ try {
302
+ const result = harness.api[__obsidianE2EMethod](...__obsidianE2EArgs);
303
+ return JSON.stringify({
304
+ ok: true,
305
+ value: __obsidianE2ESerialize(result),
306
+ });
307
+ } catch (error) {
308
+ return JSON.stringify({
309
+ error: {
310
+ message: error instanceof Error ? error.message : String(error),
311
+ name: error instanceof Error ? error.name : "Error",
312
+ stack: error instanceof Error ? error.stack : undefined,
313
+ },
314
+ ok: false,
315
+ });
316
+ }`;
317
+ //#endregion
318
+ //#region src/core/exec-options.ts
319
+ function mergeExecOptions(defaults, overrides) {
320
+ if (!defaults) return overrides ? { ...overrides } : {};
321
+ if (!overrides) return { ...defaults };
322
+ return {
323
+ ...defaults,
324
+ ...overrides,
325
+ env: mergeEnvironments(defaults.env, overrides.env)
326
+ };
327
+ }
328
+ function mergeEnvironments(defaults, overrides) {
329
+ if (!defaults) return overrides ? { ...overrides } : void 0;
330
+ if (!overrides) return { ...defaults };
331
+ return {
332
+ ...defaults,
333
+ ...overrides
334
+ };
335
+ }
336
+ //#endregion
337
+ //#region src/core/internals.ts
338
+ const clientInternals = /* @__PURE__ */ new WeakMap();
339
+ function attachClientInternals(client, internals) {
340
+ clientInternals.set(client, internals);
341
+ }
342
+ function getClientInternals(client) {
343
+ const internals = clientInternals.get(client);
344
+ if (!internals) throw new Error("Missing obsidian client internals.");
345
+ return internals;
346
+ }
347
+ function createRestoreManager(readFile) {
348
+ const snapshots = /* @__PURE__ */ new Map();
349
+ return {
350
+ async restoreAll() {
351
+ const entries = [...snapshots.entries()].reverse();
352
+ for (const [filePath, snapshot] of entries) await restoreSnapshot(filePath, snapshot);
353
+ snapshots.clear();
354
+ },
355
+ async restoreFile(filePath) {
356
+ const snapshot = snapshots.get(filePath);
357
+ if (!snapshot) return;
358
+ await restoreSnapshot(filePath, snapshot);
359
+ snapshots.delete(filePath);
360
+ },
361
+ async snapshotFileOnce(filePath) {
362
+ if (snapshots.has(filePath)) return;
363
+ try {
364
+ snapshots.set(filePath, {
365
+ exists: true,
366
+ value: await readFile(filePath)
367
+ });
368
+ } catch (error) {
369
+ if (isMissingFileError(error)) {
370
+ snapshots.set(filePath, {
371
+ exists: false,
372
+ value: ""
373
+ });
374
+ return;
375
+ }
376
+ throw error;
377
+ }
378
+ }
379
+ };
380
+ }
381
+ async function restoreSnapshot(filePath, snapshot) {
382
+ if (snapshot.exists) {
383
+ await writeFile(filePath, snapshot.value, "utf8");
384
+ return;
385
+ }
386
+ await rm(filePath, {
387
+ force: true,
388
+ recursive: true
389
+ });
390
+ }
391
+ function isMissingFileError(error) {
392
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
393
+ }
394
+ //#endregion
395
+ //#region src/metadata/metadata.ts
396
+ function createObsidianMetadataHandle(client) {
397
+ return {
398
+ async fileCache(path, execOptions) {
399
+ return readMetadata(client, "metadata", path, execOptions);
400
+ },
401
+ async frontmatter(path, execOptions) {
402
+ return readMetadata(client, "frontmatter", path, execOptions);
403
+ },
404
+ async waitForFileCache(path, predicate, options) {
405
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata cache", options);
406
+ },
407
+ async waitForFrontmatter(path, predicate, options) {
408
+ return waitForPresentValue(client, path, () => client.metadata.frontmatter(path), predicate, "frontmatter", options);
409
+ },
410
+ async waitForMetadata(path, predicate, options) {
411
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata", options);
412
+ }
413
+ };
414
+ }
415
+ async function readMetadata(client, method, path, execOptions) {
416
+ return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode(method, path), execOptions));
417
+ }
418
+ async function waitForPresentValue(client, path, readValue, predicate, label, options = {}) {
419
+ return client.waitFor(async () => {
420
+ const value = await readValue();
421
+ if (value === null) return false;
422
+ return await (predicate?.(value) ?? true) ? value : false;
423
+ }, {
424
+ ...options,
425
+ message: options.message ?? `vault path "${path}" to expose ${label}`
426
+ });
427
+ }
428
+ //#endregion
429
+ //#region src/vault/json-file.ts
430
+ function createJsonFile(filePath, beforeMutate) {
431
+ return {
432
+ async patch(updater) {
433
+ await beforeMutate?.();
434
+ const currentValue = await this.read();
435
+ const draft = structuredClone(currentValue);
436
+ const nextValue = await updater(draft) ?? draft;
437
+ await this.write(nextValue);
438
+ return nextValue;
439
+ },
440
+ async read() {
441
+ const value = await readFile(filePath, "utf8");
442
+ return JSON.parse(value);
443
+ },
444
+ async write(value) {
445
+ await beforeMutate?.();
446
+ await mkdir(path.dirname(filePath), { recursive: true });
447
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
448
+ }
449
+ };
450
+ }
451
+ //#endregion
452
+ //#region src/plugin/plugin.ts
453
+ function createPluginHandle(client, id) {
454
+ async function resolveDataPath() {
455
+ const vaultPath = await client.vaultPath();
456
+ return path.join(vaultPath, ".obsidian", "plugins", id, "data.json");
457
+ }
458
+ async function isLoadedInApp() {
459
+ try {
460
+ return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode("pluginLoaded", id)));
461
+ } catch {
462
+ return false;
463
+ }
464
+ }
465
+ function withDefaultReadyReloadOptions(options = {}) {
466
+ return {
467
+ ...options,
468
+ waitUntilReady: options.waitUntilReady ?? true
469
+ };
470
+ }
471
+ return {
472
+ data() {
473
+ return {
474
+ async patch(updater) {
475
+ const dataPath = await resolveDataPath();
476
+ return createJsonFile(dataPath, () => getClientInternals(client).snapshotFileOnce(dataPath)).patch(updater);
477
+ },
478
+ async read() {
479
+ return createJsonFile(await resolveDataPath()).read();
480
+ },
481
+ async write(value) {
482
+ const dataPath = await resolveDataPath();
483
+ await createJsonFile(dataPath, () => getClientInternals(client).snapshotFileOnce(dataPath)).write(value);
484
+ }
485
+ };
486
+ },
487
+ async dataPath() {
488
+ return resolveDataPath();
489
+ },
490
+ async disable(options = {}) {
491
+ await client.exec("plugin:disable", {
492
+ filter: options.filter,
493
+ id
494
+ });
495
+ },
496
+ async enable(options = {}) {
497
+ await client.exec("plugin:enable", {
498
+ filter: options.filter,
499
+ id
500
+ });
501
+ },
502
+ id,
503
+ async isEnabled() {
504
+ const output = await client.execText("plugin", { id }, { allowNonZeroExit: true });
505
+ return /enabled\s+true/i.test(output);
506
+ },
507
+ async reload(options = {}) {
508
+ const { readyOptions, waitUntilReady, ...execOptions } = options;
509
+ await client.exec("plugin:reload", { id }, execOptions);
510
+ if (waitUntilReady) await this.waitUntilReady(readyOptions);
511
+ },
512
+ async restoreData() {
513
+ await getClientInternals(client).restoreFile(await resolveDataPath());
514
+ },
515
+ async updateDataAndReload(updater, options = {}) {
516
+ const nextData = await this.data().patch(updater);
517
+ if (await this.isEnabled()) await this.reload(withDefaultReadyReloadOptions(options));
518
+ return nextData;
519
+ },
520
+ async withPatchedData(updater, run, options = {}) {
521
+ const pluginWasEnabled = await this.isEnabled();
522
+ const reloadOptions = withDefaultReadyReloadOptions(options);
523
+ let hasPatchedData = false;
524
+ let runResult;
525
+ let runError;
526
+ let restoreError;
527
+ try {
528
+ await this.data().patch(updater);
529
+ hasPatchedData = true;
530
+ if (pluginWasEnabled) await this.reload(reloadOptions);
531
+ runResult = await run(this);
532
+ } catch (error) {
533
+ runError = error;
534
+ }
535
+ if (hasPatchedData) try {
536
+ await this.restoreData();
537
+ if (pluginWasEnabled) await this.reload(reloadOptions);
538
+ } catch (error) {
539
+ restoreError = error;
540
+ }
541
+ if (runError && restoreError) throw new AggregateError([runError, restoreError], `Plugin "${id}" patch execution and restore both failed.`);
542
+ if (runError) throw runError;
543
+ if (restoreError) throw restoreError;
544
+ return runResult;
545
+ },
546
+ async waitForData(predicate, options = {}) {
547
+ return client.waitFor(async () => {
548
+ try {
549
+ const data = await this.data().read();
550
+ return await predicate(data) ? data : false;
551
+ } catch {
552
+ return false;
553
+ }
554
+ }, options);
555
+ },
556
+ async waitUntilReady(options = {}) {
557
+ await client.waitFor(async () => {
558
+ if (!await isLoadedInApp()) return false;
559
+ if (options.commandId) return await client.command(options.commandId).exists();
560
+ return true;
561
+ }, {
562
+ ...options,
563
+ message: options.message ?? (options.commandId ? `plugin "${id}" to be ready with command "${options.commandId}"` : `plugin "${id}" to be ready`)
564
+ });
565
+ }
566
+ };
567
+ }
568
+ //#endregion
569
+ //#region src/core/errors.ts
570
+ var ObsidianCommandError = class extends Error {
571
+ result;
572
+ constructor(message, result) {
573
+ super(message);
574
+ this.name = "ObsidianCommandError";
575
+ this.result = result;
576
+ }
577
+ };
578
+ var WaitForTimeoutError = class extends Error {
579
+ causeError;
580
+ constructor(message, causeError) {
581
+ super(message);
582
+ this.name = "WaitForTimeoutError";
583
+ this.causeError = causeError;
584
+ }
585
+ };
586
+ var DevEvalError = class extends Error {
587
+ remote;
588
+ constructor(message, remote) {
589
+ super(message);
590
+ this.name = "DevEvalError";
591
+ this.remote = remote;
592
+ if (remote.stack) this.stack = `${this.name}: ${message}\nRemote stack:\n${remote.stack}`;
593
+ }
594
+ };
595
+ //#endregion
596
+ //#region src/core/transport.ts
597
+ const DEFAULT_TIMEOUT_MS$2 = 3e4;
598
+ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, timeoutMs = DEFAULT_TIMEOUT_MS$2 }) => {
599
+ const child = spawn(bin, argv, {
600
+ cwd,
601
+ env,
602
+ stdio: [
603
+ "ignore",
604
+ "pipe",
605
+ "pipe"
606
+ ]
607
+ });
608
+ const stdoutChunks = [];
609
+ const stderrChunks = [];
610
+ child.stdout.on("data", (chunk) => {
611
+ stdoutChunks.push(Buffer.from(chunk));
612
+ });
613
+ child.stderr.on("data", (chunk) => {
614
+ stderrChunks.push(Buffer.from(chunk));
615
+ });
616
+ const exitCode = await new Promise((resolve, reject) => {
617
+ const timer = setTimeout(() => {
618
+ child.kill("SIGTERM");
619
+ reject(/* @__PURE__ */ new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(" ")}`));
620
+ }, timeoutMs);
621
+ child.on("error", (error) => {
622
+ clearTimeout(timer);
623
+ reject(error);
624
+ });
625
+ child.on("close", (code) => {
626
+ clearTimeout(timer);
627
+ resolve(code ?? 0);
628
+ });
629
+ });
630
+ const result = {
631
+ argv,
632
+ command: bin,
633
+ exitCode,
634
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
635
+ stdout: Buffer.concat(stdoutChunks).toString("utf8")
636
+ };
637
+ if (exitCode !== 0 && !allowNonZeroExit) throw new ObsidianCommandError(`Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(" ")}`, result);
638
+ return result;
639
+ };
640
+ //#endregion
641
+ //#region src/core/wait.ts
642
+ const DEFAULT_INTERVAL_MS = 100;
643
+ const DEFAULT_TIMEOUT_MS$1 = 5e3;
644
+ async function sleep$1(ms) {
645
+ await new Promise((resolve) => setTimeout(resolve, ms));
646
+ }
647
+ async function waitForValue(fn, options = {}) {
648
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
649
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
650
+ const startTime = Date.now();
651
+ let lastError;
652
+ while (Date.now() - startTime <= timeoutMs) {
653
+ try {
654
+ const result = await fn();
655
+ if (result !== false && result !== null && result !== void 0) return result;
656
+ } catch (error) {
657
+ lastError = error;
658
+ }
659
+ await sleep$1(intervalMs);
660
+ }
661
+ throw new WaitForTimeoutError(`Timed out waiting for ${options.message ?? "condition"} after ${timeoutMs}ms.`, lastError);
662
+ }
663
+ //#endregion
664
+ //#region src/core/client.ts
665
+ function createObsidianClient(options) {
666
+ const transport = options.transport ?? executeCommand;
667
+ const defaultExecOptions = options.defaultExecOptions;
668
+ const waitDefaults = {
669
+ intervalMs: options.intervalMs,
670
+ timeoutMs: options.timeoutMs
671
+ };
672
+ const restoreManager = createRestoreManager(async (filePath) => {
673
+ const { readFile } = await import("node:fs/promises");
674
+ return readFile(filePath, "utf8");
675
+ });
676
+ let cachedVaultPath;
677
+ const client = {};
678
+ const metadata = createObsidianMetadataHandle(client);
679
+ const app = {
680
+ async reload(execOptions = {}) {
681
+ await client.exec("reload", {}, execOptions);
682
+ },
683
+ async restart({ readyOptions, waitUntilReady = true, ...execOptions } = {}) {
684
+ await client.exec("restart", {}, execOptions);
685
+ if (waitUntilReady) await app.waitUntilReady(readyOptions);
686
+ },
687
+ version(execOptions = {}) {
688
+ return client.execText("version", {}, execOptions);
689
+ },
690
+ async waitUntilReady(waitOptions) {
691
+ await client.waitFor(async () => {
692
+ try {
693
+ await client.vaultPath();
694
+ await client.commands();
695
+ return true;
696
+ } catch {
697
+ return false;
698
+ }
699
+ }, waitOptions);
700
+ }
701
+ };
702
+ const dev = {
703
+ async activeFilePath(execOptions = {}) {
704
+ return readHarnessValue(this, "activeFilePath", execOptions);
705
+ },
706
+ async consoleMessages(execOptions = {}) {
707
+ return readHarnessValue(this, "consoleMessages", execOptions);
708
+ },
709
+ async diagnostics(execOptions = {}) {
710
+ return createDevDiagnostics(await readHarnessValue(this, "diagnostics", execOptions));
711
+ },
712
+ async dom(options, execOptions = {}) {
713
+ const output = await client.execText("dev:dom", {
714
+ all: options.all,
715
+ attr: options.attr,
716
+ css: options.css,
717
+ inner: options.inner,
718
+ selector: options.selector,
719
+ text: options.text,
720
+ total: options.total
721
+ }, execOptions);
722
+ if (options.total) return Number.parseInt(output, 10);
723
+ if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
724
+ return output;
725
+ },
726
+ async eval(code, execOptions = {}) {
727
+ try {
728
+ return parseHarnessEnvelope(await this.evalRaw(buildHarnessCallCode("eval", code), execOptions));
729
+ } catch (error) {
730
+ if (error && typeof error === "object" && "message" in error && "name" in error && typeof error.message === "string" && typeof error.name === "string") throw new DevEvalError(`Failed to evaluate Obsidian code: ${error.message}`, {
731
+ message: error.message,
732
+ name: error.name,
733
+ stack: "stack" in error && typeof error.stack === "string" ? error.stack : void 0
734
+ });
735
+ throw error;
736
+ }
737
+ },
738
+ async evalRaw(code, execOptions = {}) {
739
+ return client.execText("eval", { code }, execOptions);
740
+ },
741
+ async editorText(execOptions = {}) {
742
+ return readHarnessValue(this, "editorText", execOptions);
743
+ },
744
+ async notices(execOptions = {}) {
745
+ return readHarnessValue(this, "notices", execOptions);
746
+ },
747
+ async resetDiagnostics(execOptions = {}) {
748
+ await readHarnessValue(this, "resetDiagnostics", execOptions);
749
+ },
750
+ async runtimeErrors(execOptions = {}) {
751
+ return readHarnessValue(this, "runtimeErrors", execOptions);
752
+ },
753
+ async screenshot(targetPath, execOptions = {}) {
754
+ await client.exec("dev:screenshot", { path: targetPath }, execOptions);
755
+ return targetPath;
756
+ }
757
+ };
758
+ Object.assign(client, {
759
+ app,
760
+ bin: options.bin ?? "obsidian",
761
+ dev,
762
+ metadata,
763
+ command(id) {
764
+ return {
765
+ async exists(commandOptions = {}) {
766
+ return (await client.commands({
767
+ ...commandOptions,
768
+ filter: commandOptions.filter ?? id
769
+ })).includes(id);
770
+ },
771
+ id,
772
+ async run(execOptions = {}) {
773
+ await client.exec("command", { id }, execOptions);
774
+ }
775
+ };
776
+ },
777
+ async commands(commandOptions = {}, execOptions = {}) {
778
+ return parseCommandIds(await client.execText("commands", { filter: commandOptions.filter }, execOptions));
779
+ },
780
+ exec(command, args = {}, execOptions = {}) {
781
+ return transport({
782
+ ...mergeExecOptions(defaultExecOptions, execOptions),
783
+ argv: buildCommandArgv(options.vault, command, args),
784
+ bin: this.bin
785
+ });
786
+ },
787
+ async execJson(command, args = {}, execOptions = {}) {
788
+ const output = await this.execText(command, args, execOptions);
789
+ return JSON.parse(output);
790
+ },
791
+ async execText(command, args = {}, execOptions = {}) {
792
+ return (await this.exec(command, args, execOptions)).stdout.trimEnd();
793
+ },
794
+ async open(openOptions, execOptions = {}) {
795
+ await client.exec("open", {
796
+ file: openOptions.file,
797
+ newtab: openOptions.newTab,
798
+ path: openOptions.path
799
+ }, execOptions);
800
+ },
801
+ async openTab(tabOptions = {}, execOptions = {}) {
802
+ await client.exec("tab:open", {
803
+ file: tabOptions.file,
804
+ group: tabOptions.group,
805
+ view: tabOptions.view
806
+ }, execOptions);
807
+ },
808
+ plugin(id) {
809
+ return createPluginHandle(this, id);
810
+ },
811
+ sleep(ms) {
812
+ return sleep$1(ms);
813
+ },
814
+ async tabs(tabOptions = {}, execOptions = {}) {
815
+ return parseTabs(await client.execText("tabs", { ids: tabOptions.ids ?? true }, execOptions));
816
+ },
817
+ async vaultPath() {
818
+ if (!cachedVaultPath) cachedVaultPath = await this.execText("vault", { info: "path" });
819
+ return cachedVaultPath;
820
+ },
821
+ async verify() {
822
+ await transport({
823
+ ...mergeExecOptions(defaultExecOptions, void 0),
824
+ argv: ["--help"],
825
+ bin: this.bin
826
+ });
827
+ await this.vaultPath();
828
+ },
829
+ vaultName: options.vault,
830
+ async waitForActiveFile(path, options) {
831
+ return client.waitFor(async () => {
832
+ const activePath = await dev.activeFilePath();
833
+ return activePath === path ? activePath : false;
834
+ }, {
835
+ ...options,
836
+ message: options?.message ?? `active file "${path}"`
837
+ });
838
+ },
839
+ async waitForConsoleMessage(predicate, options) {
840
+ return waitForDiagnosticEntry(client, () => client.dev.consoleMessages(), predicate, options?.message ?? "console message", options);
841
+ },
842
+ async waitForNotice(predicate, options) {
843
+ return waitForDiagnosticEntry(client, () => client.dev.notices(), typeof predicate === "string" ? (notice) => notice.message.includes(predicate) : predicate, options?.message ?? "notice", options);
844
+ },
845
+ async waitForRuntimeError(predicate, options) {
846
+ return waitForDiagnosticEntry(client, () => client.dev.runtimeErrors(), typeof predicate === "string" ? (error) => error.message.includes(predicate) : predicate, options?.message ?? "runtime error", options);
847
+ },
848
+ waitFor(fn, waitOptions) {
849
+ return waitForValue(fn, {
850
+ ...waitDefaults,
851
+ ...waitOptions
852
+ });
853
+ },
854
+ async workspace(workspaceOptions = {}, execOptions = {}) {
855
+ return parseWorkspace(await client.execText("workspace", { ids: workspaceOptions.ids ?? true }, execOptions));
856
+ }
857
+ });
858
+ attachClientInternals(client, restoreManager);
859
+ return client;
860
+ }
861
+ function parseCommandIds(output) {
862
+ return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
863
+ }
864
+ function parseTabs(output) {
865
+ return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
866
+ }
867
+ function parseTabLine(line) {
868
+ const [descriptor, id] = line.split(" ");
869
+ const match = descriptor?.match(/^\[(.+?)\]\s+(.*)$/u);
870
+ if (!match) return {
871
+ id: id?.trim() || void 0,
872
+ title: descriptor?.trim() ?? "",
873
+ viewType: "unknown"
874
+ };
875
+ return {
876
+ id: id?.trim() || void 0,
877
+ title: match[2],
878
+ viewType: match[1]
879
+ };
880
+ }
881
+ function parseWorkspace(output) {
882
+ const roots = [];
883
+ const stack = [];
884
+ for (const rawLine of output.split(/\r?\n/u)) {
885
+ if (!rawLine.trim()) continue;
886
+ const depth = getWorkspaceDepth(rawLine);
887
+ const node = parseWorkspaceNode(rawLine);
888
+ while (stack.length > 0 && stack.at(-1).depth >= depth) stack.pop();
889
+ const parent = stack.at(-1)?.node;
890
+ if (parent) parent.children.push(node);
891
+ else roots.push(node);
892
+ stack.push({
893
+ depth,
894
+ node
895
+ });
896
+ }
897
+ return roots;
898
+ }
899
+ async function waitForDiagnosticEntry(client, readEntries, predicate, label, options) {
900
+ return client.waitFor(async () => {
901
+ const entries = await readEntries();
902
+ for (const entry of entries) if (await predicate(entry)) return entry;
903
+ return false;
904
+ }, {
905
+ ...options,
906
+ message: options?.message ?? label
907
+ });
908
+ }
909
+ async function readHarnessValue(dev, method, execOptions) {
910
+ return parseHarnessEnvelope(await dev.evalRaw(buildHarnessCallCode(method), execOptions));
911
+ }
912
+ function getWorkspaceDepth(line) {
913
+ let depth = 0;
914
+ let remainder = line;
915
+ while (true) {
916
+ if (remainder.startsWith("│ ") || remainder.startsWith(" ") || remainder.startsWith("├── ") || remainder.startsWith("└── ")) {
917
+ depth += 1;
918
+ remainder = remainder.slice(4);
919
+ continue;
920
+ }
921
+ return depth;
922
+ }
923
+ }
924
+ function parseWorkspaceNode(line) {
925
+ let withoutTree = line;
926
+ while (true) {
927
+ if (withoutTree.startsWith("│ ") || withoutTree.startsWith(" ") || withoutTree.startsWith("├── ") || withoutTree.startsWith("└── ")) {
928
+ withoutTree = withoutTree.slice(4);
929
+ continue;
930
+ }
931
+ break;
932
+ }
933
+ withoutTree = withoutTree.trim();
934
+ const idMatch = withoutTree.match(/^(.*?)(?: \(([a-z0-9]+)\))?$/iu);
935
+ const content = idMatch?.[1]?.trim() ?? withoutTree;
936
+ const id = idMatch?.[2];
937
+ const leafMatch = content.match(/^\[(.+?)\]\s+(.*)$/u);
938
+ if (leafMatch) return {
939
+ children: [],
940
+ id,
941
+ label: leafMatch[2],
942
+ title: leafMatch[2],
943
+ viewType: leafMatch[1]
944
+ };
945
+ return {
946
+ children: [],
947
+ id,
948
+ label: content
949
+ };
950
+ }
951
+ //#endregion
952
+ //#region src/core/path-slug.ts
953
+ function sanitizePathSegment(value, options = {}) {
954
+ const fallback = options.fallback ?? "test";
955
+ const maxLength = options.maxLength ?? 80;
956
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength) || fallback;
957
+ }
958
+ //#endregion
959
+ //#region src/vault/paths.ts
960
+ function normalizeScope(scope) {
961
+ if (!scope || scope === ".") return "";
962
+ return scope.replace(/^\/+|\/+$/g, "");
963
+ }
964
+ function resolveVaultPath(scopeRoot, targetPath) {
965
+ if (!targetPath || targetPath === ".") return scopeRoot;
966
+ return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
967
+ }
968
+ async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
969
+ const vaultPath = await obsidian.vaultPath();
970
+ const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
971
+ const resolvedPath = path.resolve(vaultPath, ...relativePath);
972
+ const normalizedVaultPath = path.resolve(vaultPath);
973
+ if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
974
+ return resolvedPath;
975
+ }
976
+ //#endregion
977
+ //#region src/vault/vault.ts
978
+ function createVaultApi(options) {
979
+ const scopeRoot = normalizeScope(options.root);
980
+ return {
981
+ async delete(targetPath, deleteOptions = {}) {
982
+ await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
983
+ force: true,
984
+ recursive: true
985
+ });
986
+ if (deleteOptions.permanent === false) return;
987
+ },
988
+ async exists(targetPath) {
989
+ try {
990
+ await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
991
+ return true;
992
+ } catch {
993
+ return false;
994
+ }
995
+ },
996
+ json(targetPath) {
997
+ const jsonFile = {
998
+ async patch(updater) {
999
+ const currentValue = await jsonFile.read();
1000
+ const draft = structuredClone(currentValue);
1001
+ const nextValue = await updater(draft) ?? draft;
1002
+ await jsonFile.write(nextValue);
1003
+ return nextValue;
1004
+ },
1005
+ async read() {
1006
+ const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
1007
+ return JSON.parse(rawValue);
1008
+ },
1009
+ async write(value) {
1010
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1011
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
1012
+ await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1013
+ }
1014
+ };
1015
+ return jsonFile;
1016
+ },
1017
+ async mkdir(targetPath) {
1018
+ await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
1019
+ },
1020
+ async read(targetPath) {
1021
+ return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
1022
+ },
1023
+ async waitForContent(targetPath, predicate, waitOptions = {}) {
1024
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1025
+ return options.obsidian.waitFor(async () => {
1026
+ try {
1027
+ const content = await readFile(resolvedPath, "utf8");
1028
+ return await predicate(content) ? content : false;
1029
+ } catch {
1030
+ return false;
1031
+ }
1032
+ }, {
1033
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
1034
+ ...waitOptions
1035
+ });
1036
+ },
1037
+ async waitForExists(targetPath, waitOptions) {
1038
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1039
+ await options.obsidian.waitFor(async () => {
1040
+ try {
1041
+ await access(resolvedPath);
1042
+ return true;
1043
+ } catch {
1044
+ return false;
1045
+ }
1046
+ }, {
1047
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
1048
+ ...waitOptions
1049
+ });
1050
+ },
1051
+ async waitForMissing(targetPath, waitOptions) {
1052
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1053
+ await options.obsidian.waitFor(async () => {
1054
+ try {
1055
+ await access(resolvedPath);
1056
+ return false;
1057
+ } catch {
1058
+ return true;
1059
+ }
1060
+ }, {
1061
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
1062
+ ...waitOptions
1063
+ });
1064
+ },
1065
+ async write(targetPath, content, writeOptions = {}) {
1066
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1067
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
1068
+ await writeFile(resolvedPath, content, "utf8");
1069
+ if (!writeOptions.waitForContent) return;
1070
+ const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
1071
+ await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
1072
+ }
1073
+ };
1074
+ }
1075
+ //#endregion
1076
+ //#region src/artifacts/failure-artifacts.ts
1077
+ const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
1078
+ const DEFAULT_FAILURE_ARTIFACT_CAPTURE = {
1079
+ activeFile: true,
1080
+ activeNote: true,
1081
+ consoleMessages: true,
1082
+ dom: true,
1083
+ editorText: true,
1084
+ notices: true,
1085
+ parsedFrontmatter: true,
1086
+ runtimeErrors: true,
1087
+ screenshot: true,
1088
+ tabs: true,
1089
+ workspace: true
1090
+ };
1091
+ function getFailureArtifactConfig(options) {
1092
+ if (!options.captureOnFailure) return {
1093
+ artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
1094
+ capture: { ...DEFAULT_FAILURE_ARTIFACT_CAPTURE },
1095
+ enabled: false
1096
+ };
1097
+ const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
1098
+ return {
1099
+ artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
1100
+ capture: {
1101
+ ...DEFAULT_FAILURE_ARTIFACT_CAPTURE,
1102
+ ...overrides
1103
+ },
1104
+ enabled: true
1105
+ };
1106
+ }
1107
+ function getFailureArtifactDirectory(artifactsDir, task) {
1108
+ const suffix = task.id.split("_").at(-1) ?? "test";
1109
+ return path.join(artifactsDir, `${sanitizePathSegment(task.name, { maxLength: 60 })}-${suffix}`);
1110
+ }
1111
+ async function captureFailureArtifacts(task, obsidian, options) {
1112
+ const config = getFailureArtifactConfig(options);
1113
+ if (!config.enabled) return;
1114
+ const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
1115
+ await mkdir(artifactDirectory, { recursive: true });
1116
+ const activeFile = readArtifactInput(() => readActiveFilePath(obsidian));
1117
+ const activeNote = readArtifactInput(async () => {
1118
+ const activeFilePath = await unwrapArtifactInput(activeFile);
1119
+ return activeFilePath ? readActiveNoteSnapshot(obsidian, activeFilePath) : null;
1120
+ });
1121
+ const diagnostics = await obsidian.dev.diagnostics().catch(() => null);
1122
+ await Promise.all([
1123
+ captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await unwrapArtifactInput(activeFile) })),
1124
+ captureTextArtifact(artifactDirectory, "active-note.md", config.capture.activeNote, async () => (await unwrapArtifactInput(activeNote))?.raw ?? ""),
1125
+ captureJsonArtifact(artifactDirectory, "active-note-frontmatter.json", config.capture.parsedFrontmatter, async () => ({ frontmatter: (await unwrapArtifactInput(activeNote))?.frontmatter ?? null })),
1126
+ captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
1127
+ inner: true,
1128
+ selector: ".workspace"
1129
+ }))),
1130
+ captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.editorText() })),
1131
+ captureJsonArtifact(artifactDirectory, "console-messages.json", config.capture.consoleMessages, async () => diagnostics?.consoleMessages ?? []),
1132
+ captureJsonArtifact(artifactDirectory, "runtime-errors.json", config.capture.runtimeErrors, async () => diagnostics?.runtimeErrors ?? []),
1133
+ captureJsonArtifact(artifactDirectory, "notices.json", config.capture.notices, async () => diagnostics?.notices ?? []),
1134
+ captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
1135
+ captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
1136
+ captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
1137
+ options.plugin ? captureJsonArtifact(artifactDirectory, `${options.plugin.id}-data.json`, true, () => options.plugin.data().read()) : Promise.resolve()
1138
+ ]);
1139
+ return artifactDirectory;
1140
+ }
1141
+ async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
1142
+ if (!enabled) return;
1143
+ try {
1144
+ const value = await readValue();
1145
+ await writeFile(path.join(artifactDirectory, filename), `${JSON.stringify(value, null, 2)}\n`, "utf8");
1146
+ } catch (error) {
1147
+ await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
1148
+ }
1149
+ }
1150
+ async function captureScreenshotArtifact(artifactDirectory, enabled, obsidian) {
1151
+ if (!enabled) return;
1152
+ const screenshotPath = path.join(artifactDirectory, "screenshot.png");
1153
+ try {
1154
+ await obsidian.dev.screenshot(screenshotPath);
1155
+ } catch (error) {
1156
+ await writeFile(path.join(artifactDirectory, "screenshot.error.txt"), formatArtifactError(error), "utf8");
1157
+ }
1158
+ }
1159
+ async function captureTextArtifact(artifactDirectory, filename, enabled, readValue) {
1160
+ if (!enabled) return;
1161
+ try {
1162
+ await writeFile(path.join(artifactDirectory, filename), await readValue(), "utf8");
1163
+ } catch (error) {
1164
+ await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
1165
+ }
1166
+ }
1167
+ function formatArtifactError(error) {
1168
+ return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
1169
+ }
1170
+ async function readActiveFilePath(obsidian) {
1171
+ return obsidian.dev.activeFilePath();
1172
+ }
1173
+ async function readActiveNoteSnapshot(obsidian, activeFile) {
1174
+ return parseNoteDocument(await createVaultApi({ obsidian }).read(activeFile));
1175
+ }
1176
+ function readArtifactInput(readValue) {
1177
+ return { promise: readValue().then((value) => ({
1178
+ ok: true,
1179
+ value
1180
+ }), (error) => ({
1181
+ error,
1182
+ ok: false
1183
+ })) };
1184
+ }
1185
+ async function unwrapArtifactInput(result) {
1186
+ const value = await result.promise;
1187
+ if (!value.ok) throw value.error;
1188
+ return value.value;
1189
+ }
1190
+ //#endregion
1191
+ //#region src/vault/sandbox.ts
1192
+ async function createSandboxApi(options) {
1193
+ const root = posix.join(options.sandboxRoot, `${sanitizePathSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
1194
+ const sandboxPath = (...segments) => posix.join(root, ...segments);
1195
+ const vault = createVaultApi({
1196
+ obsidian: options.obsidian,
1197
+ root
1198
+ });
1199
+ await vault.mkdir(".");
1200
+ return {
1201
+ ...vault,
1202
+ async cleanup() {
1203
+ await vault.delete(".", { permanent: true });
1204
+ },
1205
+ async frontmatter(targetPath) {
1206
+ return options.obsidian.metadata.frontmatter(sandboxPath(targetPath));
1207
+ },
1208
+ path(...segments) {
1209
+ return sandboxPath(...segments);
1210
+ },
1211
+ async readNote(targetPath) {
1212
+ return parseNoteDocument(await vault.read(targetPath));
1213
+ },
1214
+ root,
1215
+ async waitForFrontmatter(targetPath, predicate, waitOptions) {
1216
+ return options.obsidian.metadata.waitForFrontmatter(sandboxPath(targetPath), predicate, waitOptions);
1217
+ },
1218
+ async waitForMetadata(targetPath, predicate, waitOptions) {
1219
+ return options.obsidian.metadata.waitForMetadata(sandboxPath(targetPath), predicate, waitOptions);
1220
+ },
1221
+ async writeNote(writeOptions) {
1222
+ const { path, waitForMetadata = true, waitOptions, ...noteInput } = writeOptions;
1223
+ const document = createNoteDocument(noteInput);
1224
+ await vault.write(path, document.raw);
1225
+ if (waitForMetadata) {
1226
+ const predicate = typeof waitForMetadata === "function" ? waitForMetadata : void 0;
1227
+ await options.obsidian.metadata.waitForMetadata(sandboxPath(path), predicate, waitOptions);
1228
+ }
1229
+ return document;
1230
+ }
1231
+ };
1232
+ }
1233
+ //#endregion
1234
+ //#region src/fixtures/vault-lock.ts
1235
+ const DEFAULT_HEARTBEAT_MS = 2e3;
1236
+ const DEFAULT_STALE_MS = 15e3;
1237
+ const DEFAULT_TIMEOUT_MS = 6e4;
1238
+ const DEFAULT_WAIT_INTERVAL_MS = 500;
1239
+ const DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), "obsidian-e2e-locks");
1240
+ const LOCK_METADATA_FILE = "lock.json";
1241
+ const APP_LOCK_KEY = "__obsidianE2ELock";
1242
+ const heldLocks = /* @__PURE__ */ new Map();
1243
+ async function acquireVaultRunLock({ heartbeatMs = DEFAULT_HEARTBEAT_MS, lockRoot = DEFAULT_LOCK_ROOT, onBusy = "wait", staleMs = DEFAULT_STALE_MS, timeoutMs = DEFAULT_TIMEOUT_MS, vaultName, vaultPath }) {
1244
+ const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
1245
+ const heldLock = heldLocks.get(lockDir);
1246
+ if (heldLock) {
1247
+ heldLock.refs += 1;
1248
+ return createVaultRunLockHandle(heldLock);
1249
+ }
1250
+ const ownerId = randomUUID();
1251
+ const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
1252
+ const metadata = {
1253
+ acquiredAt: Date.now(),
1254
+ cwd: process.cwd(),
1255
+ heartbeatAt: Date.now(),
1256
+ hostname: os.hostname(),
1257
+ ownerId,
1258
+ pid: process.pid,
1259
+ staleMs,
1260
+ vaultName,
1261
+ vaultPath
1262
+ };
1263
+ await mkdir(lockRoot, { recursive: true });
1264
+ const startedAt = Date.now();
1265
+ while (true) try {
1266
+ await mkdir(lockDir);
1267
+ await writeMetadata(metadataPath, metadata);
1268
+ break;
1269
+ } catch (error) {
1270
+ if (!isAlreadyExistsError(error)) throw error;
1271
+ const currentLock = await inspectVaultRunLock({
1272
+ lockRoot,
1273
+ staleMs,
1274
+ vaultPath
1275
+ });
1276
+ if (currentLock && !currentLock.isStale) {
1277
+ if (onBusy === "fail") throw new Error(formatBusyLockMessage(vaultPath, currentLock));
1278
+ } else {
1279
+ await rm(lockDir, {
1280
+ force: true,
1281
+ recursive: true
1282
+ });
1283
+ continue;
1284
+ }
1285
+ if (Date.now() - startedAt >= timeoutMs) throw new Error(currentLock ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}` : `Timed out waiting for shared vault lock on ${vaultPath}`);
1286
+ await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));
1287
+ }
1288
+ const heartbeat = setInterval(() => {
1289
+ metadata.heartbeatAt = Date.now();
1290
+ writeMetadata(metadataPath, metadata).catch(() => {});
1291
+ }, heartbeatMs);
1292
+ heartbeat.unref();
1293
+ const nextHeldLock = {
1294
+ heartbeat,
1295
+ lockDir,
1296
+ metadata,
1297
+ metadataPath,
1298
+ refs: 1
1299
+ };
1300
+ heldLocks.set(lockDir, nextHeldLock);
1301
+ return createVaultRunLockHandle(nextHeldLock);
1302
+ }
1303
+ async function clearVaultRunLockMarker(obsidian) {
1304
+ await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; "cleared"`, { allowNonZeroExit: true });
1305
+ }
1306
+ async function inspectVaultRunLock({ lockRoot = DEFAULT_LOCK_ROOT, staleMs = DEFAULT_STALE_MS, vaultPath }) {
1307
+ const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
1308
+ const metadata = await readLockState(lockDir);
1309
+ if (!metadata) return null;
1310
+ return {
1311
+ heartbeatAgeMs: Date.now() - metadata.heartbeatAt,
1312
+ isStale: isLockStale(metadata, staleMs),
1313
+ lockDir,
1314
+ metadata
1315
+ };
1316
+ }
1317
+ async function readVaultRunLockMarker(obsidian) {
1318
+ return obsidian.dev.eval(`window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`, { allowNonZeroExit: true });
1319
+ }
1320
+ function createVaultLockKey(vaultPath) {
1321
+ return createHash("sha256").update(path.resolve(vaultPath)).digest("hex");
1322
+ }
1323
+ function buildSetMarkerCode(metadata) {
1324
+ return `(() => {
1325
+ const lock = ${JSON.stringify(metadata)};
1326
+ window.${APP_LOCK_KEY} = lock;
1327
+ app.${APP_LOCK_KEY} = lock;
1328
+ return lock;
1329
+ })()`;
1330
+ }
1331
+ function createVaultRunLockHandle(heldLock) {
1332
+ return {
1333
+ get lockDir() {
1334
+ return heldLock.lockDir;
1335
+ },
1336
+ get metadata() {
1337
+ return heldLock.metadata;
1338
+ },
1339
+ async publishMarker(obsidian) {
1340
+ await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));
1341
+ },
1342
+ async release() {
1343
+ if (heldLock.refs > 1) {
1344
+ heldLock.refs -= 1;
1345
+ return;
1346
+ }
1347
+ heldLocks.delete(heldLock.lockDir);
1348
+ clearInterval(heldLock.heartbeat);
1349
+ if ((await readLockState(heldLock.lockDir))?.ownerId !== heldLock.metadata.ownerId) return;
1350
+ await rm(heldLock.lockDir, {
1351
+ force: true,
1352
+ recursive: true
1353
+ });
1354
+ }
1355
+ };
1356
+ }
1357
+ async function readLockState(lockDir) {
1358
+ const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
1359
+ try {
1360
+ return JSON.parse(await readFile(metadataPath, "utf8"));
1361
+ } catch {
1362
+ try {
1363
+ const directoryStat = await stat(lockDir);
1364
+ return {
1365
+ acquiredAt: directoryStat.mtimeMs,
1366
+ cwd: "",
1367
+ heartbeatAt: directoryStat.mtimeMs,
1368
+ hostname: "",
1369
+ ownerId: "",
1370
+ pid: 0,
1371
+ staleMs: DEFAULT_STALE_MS,
1372
+ vaultName: "",
1373
+ vaultPath: ""
1374
+ };
1375
+ } catch {
1376
+ return null;
1377
+ }
1378
+ }
1379
+ }
1380
+ function formatBusyLockMessage(vaultPath, state) {
1381
+ return `vault ${vaultPath} is locked by ${state.metadata.ownerId ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || "<unknown>"}` : "owner=<unknown>"} ${`heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`}`;
1382
+ }
1383
+ function isAlreadyExistsError(error) {
1384
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
1385
+ }
1386
+ function isLockStale(metadata, staleMs) {
1387
+ return Date.now() - metadata.heartbeatAt > staleMs;
1388
+ }
1389
+ async function sleep(durationMs) {
1390
+ await new Promise((resolve) => setTimeout(resolve, durationMs));
1391
+ }
1392
+ async function writeMetadata(metadataPath, metadata) {
1393
+ await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
1394
+ }
1395
+ function createBaseFixtures(options, fixtureOptions = {}) {
1396
+ const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
1397
+ return {
1398
+ _vaultLock: [async ({}, use) => {
1399
+ if (!options.sharedVaultLock) {
1400
+ await use(null);
1401
+ return;
1402
+ }
1403
+ const lockClient = createObsidianClient(options);
1404
+ await lockClient.verify();
1405
+ const vaultLock = await acquireVaultRunLock({
1406
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1407
+ vaultName: options.vault,
1408
+ vaultPath: await lockClient.vaultPath()
1409
+ });
1410
+ await vaultLock.publishMarker(lockClient);
1411
+ try {
1412
+ await use(vaultLock);
1413
+ } finally {
1414
+ try {
1415
+ await clearVaultRunLockMarker(lockClient);
1416
+ } catch {}
1417
+ await vaultLock.release();
1418
+ }
1419
+ }, { scope: "worker" }],
1420
+ _testContext: async ({ _vaultLock, onTestFailed, task }, use) => {
1421
+ let failedTask = false;
1422
+ onTestFailed(() => {
1423
+ failedTask = true;
1424
+ });
1425
+ const context = await createInternalTestContext({
1426
+ ...options,
1427
+ createVault,
1428
+ testName: task.name,
1429
+ vaultLock: _vaultLock
1430
+ });
1431
+ try {
1432
+ await use(context);
1433
+ } finally {
1434
+ await context.cleanup({ failedTask: failedTask ? task : void 0 });
1435
+ }
1436
+ },
1437
+ obsidian: async ({ _testContext }, use) => {
1438
+ await use(_testContext.obsidian);
1439
+ },
1440
+ sandbox: async ({ _testContext }, use) => {
1441
+ await use(_testContext.sandbox);
1442
+ },
1443
+ vault: async ({ _testContext }, use) => {
1444
+ await use(_testContext.vault);
1445
+ }
1446
+ };
1447
+ }
1448
+ //#endregion
1449
+ //#region src/fixtures/test-context.ts
1450
+ async function createTestContext(options) {
1451
+ return createInternalTestContext(options);
1452
+ }
1453
+ async function withVaultSandbox(options, run) {
1454
+ const context = await createInternalTestContext(options);
1455
+ try {
1456
+ return await run(context);
1457
+ } finally {
1458
+ await context.cleanup();
1459
+ }
1460
+ }
1461
+ async function createInternalTestContext(options) {
1462
+ const obsidian = createObsidianClient(options);
1463
+ const trackedPlugins = /* @__PURE__ */ new Map();
1464
+ let sandbox = null;
1465
+ const vaultLock = options.vaultLock ?? await maybeAcquireVaultLock(options, obsidian);
1466
+ const ownsVaultLock = options.vaultLock === void 0 && vaultLock !== null;
1467
+ const vaultFactory = options.createVault ?? ((client) => createVaultApi({ obsidian: client }));
1468
+ let disposed = false;
1469
+ try {
1470
+ await obsidian.verify();
1471
+ if (vaultLock) await vaultLock.publishMarker(obsidian);
1472
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1473
+ const vault = await vaultFactory(obsidian);
1474
+ sandbox = await createSandboxApi({
1475
+ obsidian,
1476
+ sandboxRoot: options.sandboxRoot ?? "__obsidian_e2e__",
1477
+ testName: options.testName ?? "test"
1478
+ });
1479
+ const captureArtifacts = async (task) => captureFailureArtifacts(task, obsidian, {
1480
+ ...options,
1481
+ plugin: trackedPlugins.size === 1 ? [...trackedPlugins.values()][0].plugin : void 0
1482
+ });
1483
+ const cleanup = async (cleanupOptions = {}) => {
1484
+ if (disposed) return;
1485
+ disposed = true;
1486
+ const cleanupErrors = [];
1487
+ try {
1488
+ if (cleanupOptions.failedTask && options.captureOnFailure) await captureArtifacts(cleanupOptions.failedTask);
1489
+ } finally {
1490
+ try {
1491
+ await getClientInternals(obsidian).restoreAll();
1492
+ } finally {
1493
+ for (const session of [...trackedPlugins.values()].reverse()) if (!session.wasEnabled) await recordCleanupError(cleanupErrors, async () => session.plugin.disable({ filter: session.filter }));
1494
+ try {
1495
+ await clearVaultRunLockMarker(obsidian);
1496
+ } catch {}
1497
+ if (ownsVaultLock) await recordCleanupError(cleanupErrors, async () => vaultLock.release());
1498
+ await recordCleanupError(cleanupErrors, async () => sandbox.cleanup());
1499
+ }
1500
+ }
1501
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
1502
+ if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "One or more test cleanup steps failed.");
1503
+ };
1504
+ return {
1505
+ obsidian,
1506
+ sandbox,
1507
+ vault,
1508
+ captureFailureArtifacts: captureArtifacts,
1509
+ cleanup,
1510
+ async plugin(id, sessionOptions = {}) {
1511
+ const existing = trackedPlugins.get(id);
1512
+ if (existing) return existing.plugin;
1513
+ const plugin = obsidian.plugin(id);
1514
+ const wasEnabled = await plugin.isEnabled();
1515
+ if (!wasEnabled) await plugin.enable({ filter: sessionOptions.filter });
1516
+ if (sessionOptions.seedData !== void 0) await plugin.data().write(sessionOptions.seedData);
1517
+ trackedPlugins.set(id, {
1518
+ filter: sessionOptions.filter,
1519
+ plugin,
1520
+ wasEnabled
1521
+ });
1522
+ return plugin;
1523
+ },
1524
+ async resetDiagnostics() {
1525
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1526
+ }
1527
+ };
1528
+ } catch (error) {
1529
+ try {
1530
+ if (sandbox) await sandbox.cleanup();
1531
+ } finally {
1532
+ try {
1533
+ await clearVaultRunLockMarker(obsidian);
1534
+ } catch {}
1535
+ if (ownsVaultLock) await vaultLock.release();
1536
+ }
1537
+ throw error;
1538
+ }
1539
+ }
1540
+ async function recordCleanupError(cleanupErrors, run) {
1541
+ try {
1542
+ await run();
1543
+ } catch (error) {
1544
+ cleanupErrors.push(error);
1545
+ }
1546
+ }
1547
+ async function maybeAcquireVaultLock(options, obsidian) {
1548
+ if (!options.sharedVaultLock) return null;
1549
+ return acquireVaultRunLock({
1550
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1551
+ vaultName: options.vault,
1552
+ vaultPath: await obsidian.vaultPath()
1553
+ });
1554
+ }
1555
+ //#endregion
1556
+ export { clearVaultRunLockMarker as a, createSandboxApi as c, createVaultApi as d, resolveFilesystemPath as f, acquireVaultRunLock as i, DEFAULT_FAILURE_ARTIFACTS_DIR as l, getClientInternals as m, withVaultSandbox as n, inspectVaultRunLock as o, createObsidianClient as p, createBaseFixtures as r, readVaultRunLockMarker as s, createTestContext as t, captureFailureArtifacts as u };
1557
+
1558
+ //# sourceMappingURL=test-context-BprSx6U1.mjs.map