obsidian-e2e 0.3.1 → 0.4.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.
package/README.md CHANGED
@@ -338,6 +338,27 @@ part of the set: desktop permissions, display availability, or Obsidian state
338
338
  can prevent `screenshot.png` from being produced, in which case you should
339
339
  expect `screenshot.error.txt` instead.
340
340
 
341
+ If you are not using the Vitest fixtures, the same artifact capture path is
342
+ available directly from the main package:
343
+
344
+ ```ts
345
+ import { captureFailureArtifacts, createObsidianClient } from "obsidian-e2e";
346
+
347
+ const obsidian = createObsidianClient({ vault: "dev" });
348
+
349
+ await captureFailureArtifacts(
350
+ {
351
+ id: "quickadd_case_1234abcd",
352
+ name: "captures quickadd diagnostics",
353
+ },
354
+ obsidian,
355
+ {
356
+ captureOnFailure: true,
357
+ plugin: obsidian.plugin("quickadd"),
358
+ },
359
+ );
360
+ ```
361
+
341
362
  ## Maintainer CI And Releases
342
363
 
343
364
  This repo now ships with a hardened CI and release flow built around Vite+
@@ -406,15 +427,25 @@ test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
406
427
  If you need to work below the fixture layer:
407
428
 
408
429
  ```ts
409
- import { createObsidianClient } from "obsidian-e2e";
430
+ import { createObsidianClient, createVaultApi } from "obsidian-e2e";
410
431
 
411
432
  const obsidian = createObsidianClient({
412
433
  vault: "dev",
413
434
  bin: "obsidian",
435
+ defaultExecOptions: {
436
+ allowNonZeroExit: true,
437
+ },
414
438
  });
439
+ const vault = createVaultApi({ obsidian });
415
440
 
416
441
  await obsidian.verify();
417
- await obsidian.exec("plugin:reload", { id: "my-plugin" });
442
+ await vault.write("Inbox/Today.md", "# Today\n", { waitForContent: true });
443
+ await obsidian.plugin("my-plugin").reload({
444
+ waitUntilReady: true,
445
+ readyOptions: {
446
+ commandId: "my-plugin:refresh",
447
+ },
448
+ });
418
449
  ```
419
450
 
420
451
  ## App And Commands
@@ -436,6 +467,7 @@ to the real `obsidian` CLI:
436
467
  - `obsidian.workspace()`
437
468
  - `obsidian.open({ file? | path?, newTab? })`
438
469
  - `obsidian.openTab({ file?, group?, view? })`
470
+ - `obsidian.sleep(ms)`
439
471
 
440
472
  Example:
441
473
 
@@ -463,6 +495,38 @@ test("reloads the app and runs a plugin command when it becomes available", asyn
463
495
  `obsidian.app.restart()` waits for the app to come back by default. Pass
464
496
  `{ waitUntilReady: false }` if you need to manage readiness explicitly.
465
497
 
498
+ ## Vault And Plugin Wait Helpers
499
+
500
+ The higher-level vault and plugin handles now expose the most common polling
501
+ patterns directly, so tests do not need to hand-roll `waitFor()` loops around
502
+ content reads, command discovery, or plugin data migration:
503
+
504
+ ```ts
505
+ test("waits for generated content and plugin state", async ({ obsidian, vault }) => {
506
+ const plugin = obsidian.plugin("quickadd");
507
+
508
+ await vault.write("queue.md", "pending", {
509
+ waitForContent: true,
510
+ });
511
+
512
+ await vault.waitForContent("queue.md", (content) => content.includes("pending"));
513
+
514
+ await plugin.reload({
515
+ waitUntilReady: true,
516
+ readyOptions: {
517
+ commandId: "quickadd:run-choice",
518
+ },
519
+ });
520
+
521
+ await plugin.waitForData<{ migrations: Record<string, boolean> }>(
522
+ (data) => data.migrations.quickadd_v2 === true,
523
+ );
524
+ });
525
+ ```
526
+
527
+ If you just need time to pass without inventing a fake polling condition, use
528
+ `await obsidian.sleep(ms)`.
529
+
466
530
  Workspace and tab readers return parsed structures, so you can inspect layout
467
531
  state without writing custom parsers in every test:
468
532
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ObsidianCommandHandle, B as WorkspaceNode, C as ExecOptions, D as ObsidianAppHandle, E as JsonFileUpdater, F as RestartAppOptions, H as WorkspaceTab, I as SandboxApi, L as TabsOptions, M as OpenFileOptions, N as OpenTabOptions, O as ObsidianArg, P as PluginHandle, R as VaultApi, S as DevDomResult, T as JsonFile, V as WorkspaceOptions, a as acquireVaultRunLock, b as CreateObsidianClientOptions, c as readVaultRunLockMarker, i as VaultRunLockState, j as ObsidianDevHandle, k as ObsidianClient, n as VaultRunLock, o as clearVaultRunLockMarker, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, v as CommandListOptions, w as ExecResult, x as DevDomQueryOptions, y as CommandTransport, z as WaitForOptions } from "./vault-lock-DarzOEzv.mjs";
1
+ import { $ as WaitForOptions, A as ExecOptions, B as OpenTabOptions, C as FailureArtifactTask, D as CreateObsidianClientOptions, E as CommandTransport, F as ObsidianArg, G as PluginWaitUntilReadyOptions, H as PluginHandle, I as ObsidianClient, J as TabsOptions, K as RestartAppOptions, L as ObsidianCommandHandle, M as JsonFile, N as JsonFileUpdater, O as DevDomQueryOptions, P as ObsidianAppHandle, Q as VaultWriteOptions, R as ObsidianDevHandle, S as FailureArtifactRegistrationOptions, T as CommandListOptions, U as PluginReloadOptions, V as PluginDataPredicate, W as PluginWaitForDataOptions, X as VaultContentPredicate, Y as VaultApi, Z as VaultWaitForContentOptions, a as acquireVaultRunLock, b as FailureArtifactConfig, c as readVaultRunLockMarker, et as WorkspaceNode, i as VaultRunLockState, j as ExecResult, k as DevDomResult, n as VaultRunLock, nt as WorkspaceTab, o as clearVaultRunLockMarker, q as SandboxApi, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, tt as WorkspaceOptions, v as CaptureFailureArtifactsOptions, w as captureFailureArtifacts, x as FailureArtifactOptions, y as DEFAULT_FAILURE_ARTIFACTS_DIR, z as OpenFileOptions } from "./vault-lock-LmqAsLDT.mjs";
2
2
 
3
3
  //#region src/core/client.d.ts
4
4
  declare function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient;
@@ -18,5 +18,5 @@ interface CreateVaultApiOptions {
18
18
  }
19
19
  declare function createVaultApi(options: CreateVaultApiOptions): VaultApi;
20
20
  //#endregion
21
- export { type AcquireVaultRunLockOptions, type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginHandle, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
21
+ export { type AcquireVaultRunLockOptions, type CaptureFailureArtifactsOptions, type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, DEFAULT_FAILURE_ARTIFACTS_DIR, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type FailureArtifactConfig, type FailureArtifactOptions, type FailureArtifactRegistrationOptions, type FailureArtifactTask, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginDataPredicate, type PluginHandle, type PluginReloadOptions, type PluginWaitForDataOptions, type PluginWaitUntilReadyOptions, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type VaultContentPredicate, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type VaultWaitForContentOptions, type VaultWriteOptions, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
22
22
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as inspectVaultRunLock, i as clearVaultRunLockMarker, n as createVaultApi, o as readVaultRunLockMarker, r as acquireVaultRunLock, s as createObsidianClient, t as createSandboxApi } from "./sandbox-Cz3rj_Rn.mjs";
2
- export { acquireVaultRunLock, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
1
+ import { a as clearVaultRunLockMarker, c as DEFAULT_FAILURE_ARTIFACTS_DIR, d as createObsidianClient, i as acquireVaultRunLock, l as captureFailureArtifacts, n as createVaultApi, o as inspectVaultRunLock, s as readVaultRunLockMarker, t as createSandboxApi } from "./sandbox--mUbNsh7.mjs";
2
+ export { DEFAULT_FAILURE_ARTIFACTS_DIR, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
@@ -17,6 +17,25 @@ function buildCommandArgv(vaultName, command, args = {}) {
17
17
  return argv;
18
18
  }
19
19
  //#endregion
20
+ //#region src/core/exec-options.ts
21
+ function mergeExecOptions(defaults, overrides) {
22
+ if (!defaults) return overrides ? { ...overrides } : {};
23
+ if (!overrides) return { ...defaults };
24
+ return {
25
+ ...defaults,
26
+ ...overrides,
27
+ env: mergeEnvironments(defaults.env, overrides.env)
28
+ };
29
+ }
30
+ function mergeEnvironments(defaults, overrides) {
31
+ if (!defaults) return overrides ? { ...overrides } : void 0;
32
+ if (!overrides) return { ...defaults };
33
+ return {
34
+ ...defaults,
35
+ ...overrides
36
+ };
37
+ }
38
+ //#endregion
20
39
  //#region src/core/internals.ts
21
40
  const clientInternals = /* @__PURE__ */ new WeakMap();
22
41
  function attachClientInternals(client, internals) {
@@ -104,6 +123,19 @@ function createPluginHandle(client, id) {
104
123
  const vaultPath = await client.vaultPath();
105
124
  return path.join(vaultPath, ".obsidian", "plugins", id, "data.json");
106
125
  }
126
+ async function isLoadedInApp() {
127
+ try {
128
+ return await client.dev.eval(`(() => {
129
+ const plugins = app?.plugins;
130
+ return Boolean(
131
+ plugins?.enabledPlugins?.has?.(${JSON.stringify(id)}) &&
132
+ plugins?.plugins?.[${JSON.stringify(id)}],
133
+ );
134
+ })()`);
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
107
139
  return {
108
140
  data() {
109
141
  return {
@@ -140,11 +172,33 @@ function createPluginHandle(client, id) {
140
172
  const output = await client.execText("plugin", { id }, { allowNonZeroExit: true });
141
173
  return /enabled\s+true/i.test(output);
142
174
  },
143
- async reload() {
144
- await client.exec("plugin:reload", { id });
175
+ async reload(options = {}) {
176
+ const { readyOptions, waitUntilReady, ...execOptions } = options;
177
+ await client.exec("plugin:reload", { id }, execOptions);
178
+ if (waitUntilReady) await this.waitUntilReady(readyOptions);
145
179
  },
146
180
  async restoreData() {
147
181
  await getClientInternals(client).restoreFile(await resolveDataPath());
182
+ },
183
+ async waitForData(predicate, options = {}) {
184
+ return client.waitFor(async () => {
185
+ try {
186
+ const data = await this.data().read();
187
+ return await predicate(data) ? data : false;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }, options);
192
+ },
193
+ async waitUntilReady(options = {}) {
194
+ await client.waitFor(async () => {
195
+ if (!await isLoadedInApp()) return false;
196
+ if (options.commandId) return await client.command(options.commandId).exists();
197
+ return true;
198
+ }, {
199
+ ...options,
200
+ message: options.message ?? (options.commandId ? `plugin "${id}" to be ready with command "${options.commandId}"` : `plugin "${id}" to be ready`)
201
+ });
148
202
  }
149
203
  };
150
204
  }
@@ -215,6 +269,9 @@ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, t
215
269
  //#region src/core/wait.ts
216
270
  const DEFAULT_INTERVAL_MS = 100;
217
271
  const DEFAULT_TIMEOUT_MS$1 = 5e3;
272
+ async function sleep$1(ms) {
273
+ await new Promise((resolve) => setTimeout(resolve, ms));
274
+ }
218
275
  async function waitForValue(fn, options = {}) {
219
276
  const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
220
277
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
@@ -227,7 +284,7 @@ async function waitForValue(fn, options = {}) {
227
284
  } catch (error) {
228
285
  lastError = error;
229
286
  }
230
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
287
+ await sleep$1(intervalMs);
231
288
  }
232
289
  throw new WaitForTimeoutError(`Timed out waiting for ${options.message ?? "condition"} after ${timeoutMs}ms.`, lastError);
233
290
  }
@@ -235,6 +292,7 @@ async function waitForValue(fn, options = {}) {
235
292
  //#region src/core/client.ts
236
293
  function createObsidianClient(options) {
237
294
  const transport = options.transport ?? executeCommand;
295
+ const defaultExecOptions = options.defaultExecOptions;
238
296
  const waitDefaults = {
239
297
  intervalMs: options.intervalMs,
240
298
  timeoutMs: options.timeoutMs
@@ -313,7 +371,7 @@ function createObsidianClient(options) {
313
371
  },
314
372
  exec(command, args = {}, execOptions = {}) {
315
373
  return transport({
316
- ...execOptions,
374
+ ...mergeExecOptions(defaultExecOptions, execOptions),
317
375
  argv: buildCommandArgv(options.vault, command, args),
318
376
  bin: this.bin
319
377
  });
@@ -342,6 +400,9 @@ function createObsidianClient(options) {
342
400
  plugin(id) {
343
401
  return createPluginHandle(this, id);
344
402
  },
403
+ sleep(ms) {
404
+ return sleep$1(ms);
405
+ },
345
406
  async tabs(tabOptions = {}, execOptions = {}) {
346
407
  return parseTabs(await client.execText("tabs", { ids: tabOptions.ids ?? true }, execOptions));
347
408
  },
@@ -351,6 +412,7 @@ function createObsidianClient(options) {
351
412
  },
352
413
  async verify() {
353
414
  await transport({
415
+ ...mergeExecOptions(defaultExecOptions, void 0),
354
416
  argv: ["--help"],
355
417
  bin: this.bin
356
418
  });
@@ -456,6 +518,99 @@ function parseWorkspaceNode(line) {
456
518
  };
457
519
  }
458
520
  //#endregion
521
+ //#region src/artifacts/failure-artifacts.ts
522
+ const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
523
+ function getFailureArtifactConfig(options) {
524
+ if (!options.captureOnFailure) return {
525
+ artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
526
+ capture: {
527
+ activeFile: true,
528
+ dom: true,
529
+ editorText: true,
530
+ screenshot: true,
531
+ tabs: true,
532
+ workspace: true
533
+ },
534
+ enabled: false
535
+ };
536
+ const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
537
+ return {
538
+ artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
539
+ capture: {
540
+ activeFile: overrides.activeFile ?? true,
541
+ dom: overrides.dom ?? true,
542
+ editorText: overrides.editorText ?? true,
543
+ screenshot: overrides.screenshot ?? true,
544
+ tabs: overrides.tabs ?? true,
545
+ workspace: overrides.workspace ?? true
546
+ },
547
+ enabled: true
548
+ };
549
+ }
550
+ function getFailureArtifactDirectory(artifactsDir, task) {
551
+ const suffix = task.id.split("_").at(-1) ?? "test";
552
+ return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);
553
+ }
554
+ async function captureFailureArtifacts(task, obsidian, options) {
555
+ const config = getFailureArtifactConfig(options);
556
+ if (!config.enabled) return;
557
+ const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
558
+ await mkdir(artifactDirectory, { recursive: true });
559
+ await Promise.all([
560
+ captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await obsidian.dev.eval("app.workspace.getActiveFile()?.path ?? null") })),
561
+ captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
562
+ inner: true,
563
+ selector: ".workspace"
564
+ }))),
565
+ captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null") })),
566
+ captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
567
+ captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
568
+ captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
569
+ options.plugin ? captureJsonArtifact(artifactDirectory, `${options.plugin.id}-data.json`, true, () => options.plugin.data().read()) : Promise.resolve()
570
+ ]);
571
+ return artifactDirectory;
572
+ }
573
+ async function capturePluginFailureArtifacts(task, plugin, options) {
574
+ const config = getFailureArtifactConfig(options);
575
+ if (!config.enabled) return;
576
+ const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
577
+ await mkdir(artifactDirectory, { recursive: true });
578
+ await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () => plugin.data().read());
579
+ return artifactDirectory;
580
+ }
581
+ async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
582
+ if (!enabled) return;
583
+ try {
584
+ const value = await readValue();
585
+ await writeFile(path.join(artifactDirectory, filename), `${JSON.stringify(value, null, 2)}\n`, "utf8");
586
+ } catch (error) {
587
+ await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
588
+ }
589
+ }
590
+ async function captureScreenshotArtifact(artifactDirectory, enabled, obsidian) {
591
+ if (!enabled) return;
592
+ const screenshotPath = path.join(artifactDirectory, "screenshot.png");
593
+ try {
594
+ await obsidian.dev.screenshot(screenshotPath);
595
+ } catch (error) {
596
+ await writeFile(path.join(artifactDirectory, "screenshot.error.txt"), formatArtifactError(error), "utf8");
597
+ }
598
+ }
599
+ async function captureTextArtifact(artifactDirectory, filename, enabled, readValue) {
600
+ if (!enabled) return;
601
+ try {
602
+ await writeFile(path.join(artifactDirectory, filename), await readValue(), "utf8");
603
+ } catch (error) {
604
+ await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
605
+ }
606
+ }
607
+ function formatArtifactError(error) {
608
+ return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
609
+ }
610
+ function sanitizeForPath(value) {
611
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
612
+ }
613
+ //#endregion
459
614
  //#region src/fixtures/vault-lock.ts
460
615
  const DEFAULT_HEARTBEAT_MS = 2e3;
461
616
  const DEFAULT_STALE_MS = 15e3;
@@ -618,6 +773,24 @@ async function writeMetadata(metadataPath, metadata) {
618
773
  await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
619
774
  }
620
775
  //#endregion
776
+ //#region src/vault/paths.ts
777
+ function normalizeScope(scope) {
778
+ if (!scope || scope === ".") return "";
779
+ return scope.replace(/^\/+|\/+$/g, "");
780
+ }
781
+ function resolveVaultPath(scopeRoot, targetPath) {
782
+ if (!targetPath || targetPath === ".") return scopeRoot;
783
+ return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
784
+ }
785
+ async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
786
+ const vaultPath = await obsidian.vaultPath();
787
+ const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
788
+ const resolvedPath = path.resolve(vaultPath, ...relativePath);
789
+ const normalizedVaultPath = path.resolve(vaultPath);
790
+ if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
791
+ return resolvedPath;
792
+ }
793
+ //#endregion
621
794
  //#region src/vault/vault.ts
622
795
  function createVaultApi(options) {
623
796
  const scopeRoot = normalizeScope(options.root);
@@ -664,41 +837,58 @@ function createVaultApi(options) {
664
837
  async read(targetPath) {
665
838
  return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
666
839
  },
840
+ async waitForContent(targetPath, predicate, waitOptions = {}) {
841
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
842
+ return options.obsidian.waitFor(async () => {
843
+ try {
844
+ const content = await readFile(resolvedPath, "utf8");
845
+ return await predicate(content) ? content : false;
846
+ } catch {
847
+ return false;
848
+ }
849
+ }, {
850
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
851
+ ...waitOptions
852
+ });
853
+ },
667
854
  async waitForExists(targetPath, waitOptions) {
668
- await options.obsidian.waitFor(async () => await this.exists(targetPath) ? true : false, {
855
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
856
+ await options.obsidian.waitFor(async () => {
857
+ try {
858
+ await access(resolvedPath);
859
+ return true;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }, {
669
864
  message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
670
865
  ...waitOptions
671
866
  });
672
867
  },
673
868
  async waitForMissing(targetPath, waitOptions) {
674
- await options.obsidian.waitFor(async () => await this.exists(targetPath) ? false : true, {
869
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
870
+ await options.obsidian.waitFor(async () => {
871
+ try {
872
+ await access(resolvedPath);
873
+ return false;
874
+ } catch {
875
+ return true;
876
+ }
877
+ }, {
675
878
  message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
676
879
  ...waitOptions
677
880
  });
678
881
  },
679
- async write(targetPath, content) {
882
+ async write(targetPath, content, writeOptions = {}) {
680
883
  const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
681
884
  await mkdir(path.dirname(resolvedPath), { recursive: true });
682
885
  await writeFile(resolvedPath, content, "utf8");
886
+ if (!writeOptions.waitForContent) return;
887
+ const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
888
+ await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
683
889
  }
684
890
  };
685
891
  }
686
- function normalizeScope(scope) {
687
- if (!scope || scope === ".") return "";
688
- return scope.replace(/^\/+|\/+$/g, "");
689
- }
690
- function resolveVaultPath(scopeRoot, targetPath) {
691
- if (!targetPath || targetPath === ".") return scopeRoot;
692
- return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
693
- }
694
- async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
695
- const vaultPath = await obsidian.vaultPath();
696
- const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
697
- const resolvedPath = path.resolve(vaultPath, ...relativePath);
698
- const normalizedVaultPath = path.resolve(vaultPath);
699
- if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
700
- return resolvedPath;
701
- }
702
892
  //#endregion
703
893
  //#region src/vault/sandbox.ts
704
894
  async function createSandboxApi(options) {
@@ -723,6 +913,6 @@ function sanitizeSegment(value) {
723
913
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "test";
724
914
  }
725
915
  //#endregion
726
- export { inspectVaultRunLock as a, getClientInternals as c, clearVaultRunLockMarker as i, createVaultApi as n, readVaultRunLockMarker as o, acquireVaultRunLock as r, createObsidianClient as s, createSandboxApi as t };
916
+ export { clearVaultRunLockMarker as a, DEFAULT_FAILURE_ARTIFACTS_DIR as c, createObsidianClient as d, getClientInternals as f, acquireVaultRunLock as i, captureFailureArtifacts as l, createVaultApi as n, inspectVaultRunLock as o, resolveFilesystemPath as r, readVaultRunLockMarker as s, createSandboxApi as t, capturePluginFailureArtifacts as u };
727
917
 
728
- //# sourceMappingURL=sandbox-Cz3rj_Rn.mjs.map
918
+ //# sourceMappingURL=sandbox--mUbNsh7.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox--mUbNsh7.mjs","names":["DEFAULT_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","sleep","sleep","pathPosix","pathPosix"],"sources":["../src/core/args.ts","../src/core/exec-options.ts","../src/core/internals.ts","../src/vault/json-file.ts","../src/plugin/plugin.ts","../src/core/errors.ts","../src/core/transport.ts","../src/core/wait.ts","../src/core/client.ts","../src/artifacts/failure-artifacts.ts","../src/fixtures/vault-lock.ts","../src/vault/paths.ts","../src/vault/vault.ts","../src/vault/sandbox.ts"],"sourcesContent":["import type { ObsidianArg } from \"./types\";\n\nexport function buildCommandArgv(\n vaultName: string,\n command: string,\n args: Record<string, ObsidianArg> = {},\n): string[] {\n const argv = [`vault=${vaultName}`, command];\n\n for (const [key, value] of Object.entries(args)) {\n if (value === false || value === null || value === undefined) {\n continue;\n }\n\n if (value === true) {\n argv.push(key);\n continue;\n }\n\n argv.push(`${key}=${String(value)}`);\n }\n\n return argv;\n}\n","import type { ExecOptions } from \"./types\";\n\nexport function mergeExecOptions(\n defaults: ExecOptions | undefined,\n overrides: ExecOptions | undefined,\n): ExecOptions {\n if (!defaults) {\n return overrides ? { ...overrides } : {};\n }\n\n if (!overrides) {\n return { ...defaults };\n }\n\n return {\n ...defaults,\n ...overrides,\n env: mergeEnvironments(defaults.env, overrides.env),\n };\n}\n\nfunction mergeEnvironments(\n defaults: NodeJS.ProcessEnv | undefined,\n overrides: NodeJS.ProcessEnv | undefined,\n): NodeJS.ProcessEnv | undefined {\n if (!defaults) {\n return overrides ? { ...overrides } : undefined;\n }\n\n if (!overrides) {\n return { ...defaults };\n }\n\n return {\n ...defaults,\n ...overrides,\n };\n}\n","import { rm, writeFile } from \"node:fs/promises\";\n\nimport type { ObsidianClient } from \"./types\";\n\ninterface SnapshotEntry {\n exists: boolean;\n value: string;\n}\n\ninterface ClientInternals {\n restoreAll(): Promise<void>;\n restoreFile(filePath: string): Promise<void>;\n snapshotFileOnce(filePath: string): Promise<void>;\n}\n\nconst clientInternals = new WeakMap<ObsidianClient, ClientInternals>();\n\nexport function attachClientInternals(client: ObsidianClient, internals: ClientInternals): void {\n clientInternals.set(client, internals);\n}\n\nexport function getClientInternals(client: ObsidianClient): ClientInternals {\n const internals = clientInternals.get(client);\n\n if (!internals) {\n throw new Error(\"Missing obsidian client internals.\");\n }\n\n return internals;\n}\n\nexport function createRestoreManager(readFile: (filePath: string) => Promise<string>) {\n const snapshots = new Map<string, SnapshotEntry>();\n\n return {\n async restoreAll() {\n const entries = [...snapshots.entries()].reverse();\n\n for (const [filePath, snapshot] of entries) {\n await restoreSnapshot(filePath, snapshot);\n }\n\n snapshots.clear();\n },\n async restoreFile(filePath: string) {\n const snapshot = snapshots.get(filePath);\n\n if (!snapshot) {\n return;\n }\n\n await restoreSnapshot(filePath, snapshot);\n snapshots.delete(filePath);\n },\n async snapshotFileOnce(filePath: string) {\n if (snapshots.has(filePath)) {\n return;\n }\n\n try {\n snapshots.set(filePath, {\n exists: true,\n value: await readFile(filePath),\n });\n } catch (error) {\n if (isMissingFileError(error)) {\n snapshots.set(filePath, {\n exists: false,\n value: \"\",\n });\n return;\n }\n\n throw error;\n }\n },\n };\n}\n\nasync function restoreSnapshot(filePath: string, snapshot: SnapshotEntry): Promise<void> {\n if (snapshot.exists) {\n await writeFile(filePath, snapshot.value, \"utf8\");\n return;\n }\n\n await rm(filePath, { force: true, recursive: true });\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return Boolean(error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { JsonFile, JsonFileUpdater } from \"../core/types\";\n\nexport function createJsonFile<T = unknown>(\n filePath: string,\n beforeMutate?: () => Promise<void>,\n): JsonFile<T> {\n return {\n async patch(updater: JsonFileUpdater<T>) {\n await beforeMutate?.();\n\n const currentValue = await this.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await this.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const value = await readFile(filePath, \"utf8\");\n return JSON.parse(value) as T;\n },\n async write(value: T) {\n await beforeMutate?.();\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n}\n","import path from \"node:path\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type {\n JsonFile,\n ObsidianClient,\n PluginDataPredicate,\n PluginHandle,\n PluginReloadOptions,\n PluginToggleOptions,\n PluginWaitForDataOptions,\n PluginWaitUntilReadyOptions,\n} from \"../core/types\";\nimport { createJsonFile } from \"../vault/json-file\";\n\nexport function createPluginHandle(client: ObsidianClient, id: string): PluginHandle {\n async function resolveDataPath() {\n const vaultPath = await client.vaultPath();\n return path.join(vaultPath, \".obsidian\", \"plugins\", id, \"data.json\");\n }\n\n async function isLoadedInApp(): Promise<boolean> {\n try {\n return await client.dev.eval<boolean>(`(() => {\n const plugins = app?.plugins;\n return Boolean(\n plugins?.enabledPlugins?.has?.(${JSON.stringify(id)}) &&\n plugins?.plugins?.[${JSON.stringify(id)}],\n );\n })()`);\n } catch {\n return false;\n }\n }\n\n return {\n data<T = unknown>(): JsonFile<T> {\n return {\n async patch(updater) {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).patch(updater);\n },\n async read() {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath).read();\n },\n async write(value) {\n const dataPath = await resolveDataPath();\n await createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).write(value);\n },\n };\n },\n async dataPath() {\n return resolveDataPath();\n },\n async disable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:disable\", {\n filter: options.filter,\n id,\n });\n },\n async enable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:enable\", {\n filter: options.filter,\n id,\n });\n },\n id,\n async isEnabled() {\n const output = await client.execText(\"plugin\", { id }, { allowNonZeroExit: true });\n return /enabled\\s+true/i.test(output);\n },\n async reload(options: PluginReloadOptions = {}) {\n const { readyOptions, waitUntilReady, ...execOptions } = options;\n\n await client.exec(\"plugin:reload\", { id }, execOptions);\n\n if (waitUntilReady) {\n await this.waitUntilReady(readyOptions);\n }\n },\n async restoreData() {\n await getClientInternals(client).restoreFile(await resolveDataPath());\n },\n async waitForData<T = unknown>(\n predicate: PluginDataPredicate<T>,\n options: PluginWaitForDataOptions = {},\n ) {\n return client.waitFor(async () => {\n try {\n const data = await this.data<T>().read();\n return (await predicate(data)) ? data : false;\n } catch {\n return false;\n }\n }, options);\n },\n async waitUntilReady(options: PluginWaitUntilReadyOptions = {}) {\n await client.waitFor(\n async () => {\n if (!(await isLoadedInApp())) {\n return false;\n }\n\n if (options.commandId) {\n return await client.command(options.commandId).exists();\n }\n\n return true;\n },\n {\n ...options,\n message:\n options.message ??\n (options.commandId\n ? `plugin \"${id}\" to be ready with command \"${options.commandId}\"`\n : `plugin \"${id}\" to be ready`),\n },\n );\n },\n };\n}\n","import type { ExecResult } from \"./types\";\n\nexport class ObsidianCommandError extends Error {\n readonly result: ExecResult;\n\n constructor(message: string, result: ExecResult) {\n super(message);\n this.name = \"ObsidianCommandError\";\n this.result = result;\n }\n}\n\nexport class WaitForTimeoutError extends Error {\n readonly causeError?: unknown;\n\n constructor(message: string, causeError?: unknown) {\n super(message);\n this.name = \"WaitForTimeoutError\";\n this.causeError = causeError;\n }\n}\n","import { spawn } from \"node:child_process\";\n\nimport { ObsidianCommandError } from \"./errors\";\nimport type { CommandTransport, ExecuteRequest, ExecResult } from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const executeCommand: CommandTransport = async ({\n allowNonZeroExit = false,\n argv,\n bin,\n cwd,\n env,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n}: ExecuteRequest): Promise<ExecResult> => {\n const child = spawn(bin, argv, {\n cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on(\"data\", (chunk) => {\n stdoutChunks.push(Buffer.from(chunk));\n });\n\n child.stderr.on(\"data\", (chunk) => {\n stderrChunks.push(Buffer.from(chunk));\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(\" \")}`));\n }, timeoutMs);\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n reject(error);\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n resolve(code ?? 0);\n });\n });\n\n const result: ExecResult = {\n argv,\n command: bin,\n exitCode,\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n };\n\n if (exitCode !== 0 && !allowNonZeroExit) {\n throw new ObsidianCommandError(\n `Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(\" \")}`,\n result,\n );\n }\n\n return result;\n};\n","import { WaitForTimeoutError } from \"./errors\";\nimport type { WaitForOptions } from \"./types\";\n\nconst DEFAULT_INTERVAL_MS = 100;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\nexport async function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function waitForValue<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n options: WaitForOptions = {},\n): Promise<T> {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const startTime = Date.now();\n\n let lastError: unknown;\n\n while (Date.now() - startTime <= timeoutMs) {\n try {\n const result = await fn();\n if (result !== false && result !== null && result !== undefined) {\n return result;\n }\n } catch (error) {\n lastError = error;\n }\n\n await sleep(intervalMs);\n }\n\n const label = options.message ?? \"condition\";\n throw new WaitForTimeoutError(`Timed out waiting for ${label} after ${timeoutMs}ms.`, lastError);\n}\n","import { buildCommandArgv } from \"./args\";\nimport { mergeExecOptions } from \"./exec-options\";\nimport { attachClientInternals, createRestoreManager } from \"./internals\";\nimport { createPluginHandle } from \"../plugin/plugin\";\nimport { executeCommand } from \"./transport\";\nimport type {\n CommandListOptions,\n CreateObsidianClientOptions,\n DevDomQueryOptions,\n DevDomResult,\n ExecOptions,\n ObsidianArg,\n ObsidianAppHandle,\n ObsidianCommandHandle,\n ObsidianClient,\n ObsidianDevHandle,\n OpenFileOptions,\n OpenTabOptions,\n RestartAppOptions,\n TabsOptions,\n WaitForOptions,\n WorkspaceNode,\n WorkspaceOptions,\n WorkspaceTab,\n} from \"./types\";\nimport { sleep, waitForValue } from \"./wait\";\n\nexport function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient {\n const transport = options.transport ?? executeCommand;\n const defaultExecOptions = options.defaultExecOptions;\n const waitDefaults = {\n intervalMs: options.intervalMs,\n timeoutMs: options.timeoutMs,\n };\n\n const restoreManager = createRestoreManager(async (filePath) => {\n const { readFile } = await import(\"node:fs/promises\");\n return readFile(filePath, \"utf8\");\n });\n\n let cachedVaultPath: string | undefined;\n\n const client = {} as ObsidianClient;\n\n const app: ObsidianAppHandle = {\n async reload(execOptions: ExecOptions = {}) {\n await client.exec(\"reload\", {}, execOptions);\n },\n async restart({\n readyOptions,\n waitUntilReady = true,\n ...execOptions\n }: RestartAppOptions & ExecOptions = {}) {\n await client.exec(\"restart\", {}, execOptions);\n\n if (waitUntilReady) {\n await app.waitUntilReady(readyOptions);\n }\n },\n version(execOptions: ExecOptions = {}) {\n return client.execText(\"version\", {}, execOptions);\n },\n async waitUntilReady(waitOptions?: WaitForOptions) {\n await client.waitFor(async () => {\n try {\n await client.vaultPath();\n await client.commands();\n return true;\n } catch {\n return false;\n }\n }, waitOptions);\n },\n };\n\n const dev: ObsidianDevHandle = {\n async dom(options: DevDomQueryOptions, execOptions: ExecOptions = {}): Promise<DevDomResult> {\n const output = await client.execText(\n \"dev:dom\",\n {\n all: options.all,\n attr: options.attr,\n css: options.css,\n inner: options.inner,\n selector: options.selector,\n text: options.text,\n total: options.total,\n },\n execOptions,\n );\n\n if (options.total) {\n return Number.parseInt(output, 10);\n }\n\n if (options.all) {\n return output ? output.split(/\\r?\\n/u).filter(Boolean) : [];\n }\n\n return output;\n },\n async eval<T = unknown>(code: string, execOptions: ExecOptions = {}) {\n const output = await client.execText(\n \"eval\",\n {\n code,\n },\n execOptions,\n );\n return parseDevEvalOutput<T>(output);\n },\n async screenshot(targetPath: string, execOptions: ExecOptions = {}) {\n await client.exec(\n \"dev:screenshot\",\n {\n path: targetPath,\n },\n execOptions,\n );\n\n return targetPath;\n },\n };\n\n Object.assign(client, {\n app,\n bin: options.bin ?? \"obsidian\",\n dev,\n command(id: string): ObsidianCommandHandle {\n return {\n async exists(commandOptions: CommandListOptions = {}) {\n const commands = await client.commands({\n ...commandOptions,\n filter: commandOptions.filter ?? id,\n });\n\n return commands.includes(id);\n },\n id,\n async run(execOptions: ExecOptions = {}) {\n await client.exec(\"command\", { id }, execOptions);\n },\n };\n },\n async commands(\n commandOptions: CommandListOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<string[]> {\n const output = await client.execText(\n \"commands\",\n {\n filter: commandOptions.filter,\n },\n execOptions,\n );\n return parseCommandIds(output);\n },\n exec(command: string, args: Record<string, ObsidianArg> = {}, execOptions: ExecOptions = {}) {\n return transport({\n ...mergeExecOptions(defaultExecOptions, execOptions),\n argv: buildCommandArgv(options.vault, command, args),\n bin: this.bin,\n });\n },\n async execJson<T = unknown>(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const output = await this.execText(command, args, execOptions);\n return JSON.parse(output) as T;\n },\n async execText(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const result = await this.exec(command, args, execOptions);\n return result.stdout.trimEnd();\n },\n async open(openOptions: OpenFileOptions, execOptions: ExecOptions = {}) {\n await client.exec(\n \"open\",\n {\n file: openOptions.file,\n newtab: openOptions.newTab,\n path: openOptions.path,\n },\n execOptions,\n );\n },\n async openTab(tabOptions: OpenTabOptions = {}, execOptions: ExecOptions = {}) {\n await client.exec(\n \"tab:open\",\n {\n file: tabOptions.file,\n group: tabOptions.group,\n view: tabOptions.view,\n },\n execOptions,\n );\n },\n plugin(id: string) {\n return createPluginHandle(this, id);\n },\n sleep(ms: number) {\n return sleep(ms);\n },\n async tabs(\n tabOptions: TabsOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceTab[]> {\n const output = await client.execText(\n \"tabs\",\n {\n ids: tabOptions.ids ?? true,\n },\n execOptions,\n );\n return parseTabs(output);\n },\n async vaultPath() {\n if (!cachedVaultPath) {\n cachedVaultPath = await this.execText(\"vault\", { info: \"path\" });\n }\n\n return cachedVaultPath;\n },\n async verify() {\n await transport({\n ...mergeExecOptions(defaultExecOptions, undefined),\n argv: [\"--help\"],\n bin: this.bin,\n });\n\n await this.vaultPath();\n },\n vaultName: options.vault,\n waitFor<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n waitOptions?: WaitForOptions,\n ) {\n return waitForValue(fn, {\n ...waitDefaults,\n ...waitOptions,\n });\n },\n async workspace(\n workspaceOptions: WorkspaceOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceNode[]> {\n const output = await client.execText(\n \"workspace\",\n {\n ids: workspaceOptions.ids ?? true,\n },\n execOptions,\n );\n return parseWorkspace(output);\n },\n });\n\n attachClientInternals(client, restoreManager);\n\n return client;\n}\n\nfunction parseCommandIds(output: string): string[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => line.split(\"\\t\", 1)[0]?.trim() ?? \"\")\n .filter(Boolean);\n}\n\nfunction parseDevEvalOutput<T>(output: string): T {\n const normalized = output.startsWith(\"=> \") ? output.slice(3) : output;\n\n try {\n return JSON.parse(normalized) as T;\n } catch {\n return normalized as T;\n }\n}\n\nfunction parseTabs(output: string): WorkspaceTab[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map(parseTabLine);\n}\n\nfunction parseTabLine(line: string): WorkspaceTab {\n const [descriptor, id] = line.split(\"\\t\");\n const match = descriptor?.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (!match) {\n return {\n id: id?.trim() || undefined,\n title: descriptor?.trim() ?? \"\",\n viewType: \"unknown\",\n };\n }\n\n return {\n id: id?.trim() || undefined,\n title: match[2]!,\n viewType: match[1]!,\n };\n}\n\nfunction parseWorkspace(output: string): WorkspaceNode[] {\n const roots: WorkspaceNode[] = [];\n const stack: Array<{ depth: number; node: WorkspaceNode }> = [];\n\n for (const rawLine of output.split(/\\r?\\n/u)) {\n if (!rawLine.trim()) {\n continue;\n }\n\n const depth = getWorkspaceDepth(rawLine);\n const node = parseWorkspaceNode(rawLine);\n\n while (stack.length > 0 && stack.at(-1)!.depth >= depth) {\n stack.pop();\n }\n\n const parent = stack.at(-1)?.node;\n\n if (parent) {\n parent.children.push(node);\n } else {\n roots.push(node);\n }\n\n stack.push({ depth, node });\n }\n\n return roots;\n}\n\nfunction getWorkspaceDepth(line: string): number {\n let depth = 0;\n let remainder = line;\n\n while (true) {\n if (\n remainder.startsWith(\"│ \") ||\n remainder.startsWith(\" \") ||\n remainder.startsWith(\"├── \") ||\n remainder.startsWith(\"└── \")\n ) {\n depth += 1;\n remainder = remainder.slice(4);\n continue;\n }\n\n return depth;\n }\n}\n\nfunction parseWorkspaceNode(line: string): WorkspaceNode {\n let withoutTree = line;\n\n while (true) {\n if (\n withoutTree.startsWith(\"│ \") ||\n withoutTree.startsWith(\" \") ||\n withoutTree.startsWith(\"├── \") ||\n withoutTree.startsWith(\"└── \")\n ) {\n withoutTree = withoutTree.slice(4);\n continue;\n }\n\n break;\n }\n\n withoutTree = withoutTree.trim();\n const idMatch = withoutTree.match(/^(.*?)(?: \\(([a-z0-9]+)\\))?$/iu);\n const content = idMatch?.[1]?.trim() ?? withoutTree;\n const id = idMatch?.[2];\n const leafMatch = content.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (leafMatch) {\n return {\n children: [],\n id,\n label: leafMatch[2]!,\n title: leafMatch[2]!,\n viewType: leafMatch[1]!,\n };\n }\n\n return {\n children: [],\n id,\n label: content,\n };\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\n\nexport const DEFAULT_FAILURE_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\nexport interface FailureArtifactOptions {\n activeFile?: boolean;\n dom?: boolean;\n editorText?: boolean;\n screenshot?: boolean;\n tabs?: boolean;\n workspace?: boolean;\n}\n\nexport interface FailureArtifactTask {\n id: string;\n name: string;\n}\n\nexport interface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport interface FailureArtifactRegistrationOptions {\n artifactsDir?: string;\n captureOnFailure?: boolean | FailureArtifactOptions;\n}\n\nexport interface CaptureFailureArtifactsOptions extends FailureArtifactRegistrationOptions {\n plugin?: PluginHandle;\n}\n\nexport function getFailureArtifactConfig(\n options: FailureArtifactRegistrationOptions,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_FAILURE_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_FAILURE_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: FailureArtifactTask,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport async function captureFailureArtifacts(\n task: FailureArtifactTask,\n obsidian: ObsidianClient,\n options: CaptureFailureArtifactsOptions,\n): Promise<string | undefined> {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return undefined;\n }\n\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(artifactDirectory, \"editor.json\", config.capture.editorText, async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n })),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () => obsidian.tabs()),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n options.plugin\n ? captureJsonArtifact(artifactDirectory, `${options.plugin.id}-data.json`, true, () =>\n options.plugin!.data().read(),\n )\n : Promise.resolve(),\n ]);\n\n return artifactDirectory;\n}\n\nexport async function capturePluginFailureArtifacts(\n task: FailureArtifactTask,\n plugin: PluginHandle,\n options: FailureArtifactRegistrationOptions,\n): Promise<string | undefined> {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return undefined;\n }\n\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n\n return artifactDirectory;\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import { mkdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createHash, randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient } from \"../core/types\";\nimport type { SharedVaultLockOptions } from \"./types\";\n\nconst DEFAULT_HEARTBEAT_MS = 2_000;\nconst DEFAULT_STALE_MS = 15_000;\nconst DEFAULT_TIMEOUT_MS = 60_000;\nconst DEFAULT_WAIT_INTERVAL_MS = 500;\nconst DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), \"obsidian-e2e-locks\");\nconst LOCK_METADATA_FILE = \"lock.json\";\nconst APP_LOCK_KEY = \"__obsidianE2ELock\";\nconst heldLocks = new Map<string, HeldVaultRunLock>();\n\nexport interface VaultRunLockMetadata {\n acquiredAt: number;\n cwd: string;\n heartbeatAt: number;\n hostname: string;\n ownerId: string;\n pid: number;\n staleMs: number;\n vaultName: string;\n vaultPath: string;\n}\n\nexport interface VaultRunLock {\n readonly lockDir: string;\n readonly metadata: VaultRunLockMetadata;\n\n publishMarker(obsidian: ObsidianClient): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface VaultRunLockState {\n heartbeatAgeMs: number;\n isStale: boolean;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n}\n\nexport interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {\n vaultName: string;\n vaultPath: string;\n}\n\ninterface HeldVaultRunLock {\n heartbeat: NodeJS.Timeout;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n metadataPath: string;\n refs: number;\n}\n\nexport async function acquireVaultRunLock({\n heartbeatMs = DEFAULT_HEARTBEAT_MS,\n lockRoot = DEFAULT_LOCK_ROOT,\n onBusy = \"wait\",\n staleMs = DEFAULT_STALE_MS,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n vaultName,\n vaultPath,\n}: AcquireVaultRunLockOptions): Promise<VaultRunLock> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const heldLock = heldLocks.get(lockDir);\n\n if (heldLock) {\n heldLock.refs += 1;\n return createVaultRunLockHandle(heldLock);\n }\n\n const ownerId = randomUUID();\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n const metadata: VaultRunLockMetadata = {\n acquiredAt: Date.now(),\n cwd: process.cwd(),\n heartbeatAt: Date.now(),\n hostname: os.hostname(),\n ownerId,\n pid: process.pid,\n staleMs,\n vaultName,\n vaultPath,\n };\n\n await mkdir(lockRoot, { recursive: true });\n\n const startedAt = Date.now();\n\n while (true) {\n try {\n await mkdir(lockDir);\n await writeMetadata(metadataPath, metadata);\n break;\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const currentLock = await inspectVaultRunLock({\n lockRoot,\n staleMs,\n vaultPath,\n });\n\n if (currentLock && !currentLock.isStale) {\n if (onBusy === \"fail\") {\n throw new Error(formatBusyLockMessage(vaultPath, currentLock));\n }\n } else {\n await rm(lockDir, { force: true, recursive: true });\n continue;\n }\n\n if (Date.now() - startedAt >= timeoutMs) {\n throw new Error(\n currentLock\n ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}`\n : `Timed out waiting for shared vault lock on ${vaultPath}`,\n );\n }\n\n await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));\n }\n }\n\n const heartbeat = setInterval(() => {\n metadata.heartbeatAt = Date.now();\n void writeMetadata(metadataPath, metadata).catch(() => {});\n }, heartbeatMs);\n heartbeat.unref();\n\n const nextHeldLock: HeldVaultRunLock = {\n heartbeat,\n lockDir,\n metadata,\n metadataPath,\n refs: 1,\n };\n\n heldLocks.set(lockDir, nextHeldLock);\n return createVaultRunLockHandle(nextHeldLock);\n}\n\nexport async function clearVaultRunLockMarker(obsidian: ObsidianClient): Promise<void> {\n await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; \"cleared\"`, {\n allowNonZeroExit: true,\n });\n}\n\nexport async function inspectVaultRunLock({\n lockRoot = DEFAULT_LOCK_ROOT,\n staleMs = DEFAULT_STALE_MS,\n vaultPath,\n}: Pick<\n AcquireVaultRunLockOptions,\n \"lockRoot\" | \"staleMs\" | \"vaultPath\"\n>): Promise<VaultRunLockState | null> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const metadata = await readLockState(lockDir);\n\n if (!metadata) {\n return null;\n }\n\n return {\n heartbeatAgeMs: Date.now() - metadata.heartbeatAt,\n isStale: isLockStale(metadata, staleMs),\n lockDir,\n metadata,\n };\n}\n\nexport async function readVaultRunLockMarker(\n obsidian: ObsidianClient,\n): Promise<VaultRunLockMetadata | null> {\n return obsidian.dev.eval<VaultRunLockMetadata | null>(\n `window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`,\n { allowNonZeroExit: true },\n );\n}\n\nfunction createVaultLockKey(vaultPath: string): string {\n return createHash(\"sha256\").update(path.resolve(vaultPath)).digest(\"hex\");\n}\n\nfunction buildSetMarkerCode(metadata: VaultRunLockMetadata): string {\n const encodedMetadata = JSON.stringify(metadata);\n return `(() => {\n const lock = ${encodedMetadata};\n window.${APP_LOCK_KEY} = lock;\n app.${APP_LOCK_KEY} = lock;\n return lock;\n })()`;\n}\n\nfunction createVaultRunLockHandle(heldLock: HeldVaultRunLock): VaultRunLock {\n return {\n get lockDir() {\n return heldLock.lockDir;\n },\n get metadata() {\n return heldLock.metadata;\n },\n async publishMarker(obsidian: ObsidianClient) {\n await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));\n },\n async release() {\n if (heldLock.refs > 1) {\n heldLock.refs -= 1;\n return;\n }\n\n heldLocks.delete(heldLock.lockDir);\n clearInterval(heldLock.heartbeat);\n\n const currentLock = await readLockState(heldLock.lockDir);\n\n if (currentLock?.ownerId !== heldLock.metadata.ownerId) {\n return;\n }\n\n await rm(heldLock.lockDir, { force: true, recursive: true });\n },\n };\n}\n\nasync function readLockState(lockDir: string): Promise<VaultRunLockMetadata | null> {\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n\n try {\n return JSON.parse(await readFile(metadataPath, \"utf8\")) as VaultRunLockMetadata;\n } catch {\n try {\n const directoryStat = await stat(lockDir);\n return {\n acquiredAt: directoryStat.mtimeMs,\n cwd: \"\",\n heartbeatAt: directoryStat.mtimeMs,\n hostname: \"\",\n ownerId: \"\",\n pid: 0,\n staleMs: DEFAULT_STALE_MS,\n vaultName: \"\",\n vaultPath: \"\",\n };\n } catch {\n return null;\n }\n }\n}\n\nfunction formatBusyLockMessage(vaultPath: string, state: VaultRunLockState): string {\n const ownerDetails = state.metadata.ownerId\n ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || \"<unknown>\"}`\n : \"owner=<unknown>\";\n const ageDetails = `heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`;\n\n return `vault ${vaultPath} is locked by ${ownerDetails} ${ageDetails}`;\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isLockStale(metadata: VaultRunLockMetadata, staleMs: number): boolean {\n return Date.now() - metadata.heartbeatAt > staleMs;\n}\n\nasync function sleep(durationMs: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, durationMs));\n}\n\nasync function writeMetadata(metadataPath: string, metadata: VaultRunLockMetadata): Promise<void> {\n await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\\n`, \"utf8\");\n}\n","import path from \"node:path\";\nimport { posix as pathPosix } from \"node:path\";\n\nimport type { ObsidianClient } from \"../core/types\";\n\nexport function normalizeScope(scope?: string): string {\n if (!scope || scope === \".\") {\n return \"\";\n }\n\n return scope.replace(/^\\/+|\\/+$/g, \"\");\n}\n\nexport function resolveVaultPath(scopeRoot: string, targetPath: string): string {\n if (!targetPath || targetPath === \".\") {\n return scopeRoot;\n }\n\n return scopeRoot ? pathPosix.join(scopeRoot, targetPath) : pathPosix.normalize(targetPath);\n}\n\nexport async function resolveFilesystemPath(\n obsidian: ObsidianClient,\n scopeRoot: string,\n targetPath: string,\n): Promise<string> {\n const vaultPath = await obsidian.vaultPath();\n const scopedPath = resolveVaultPath(scopeRoot, targetPath);\n const relativePath = scopedPath.split(\"/\").filter(Boolean);\n const resolvedPath = path.resolve(vaultPath, ...relativePath);\n const normalizedVaultPath = path.resolve(vaultPath);\n\n if (\n resolvedPath !== normalizedVaultPath &&\n !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)\n ) {\n throw new Error(`Resolved path escapes the vault root: ${targetPath}`);\n }\n\n return resolvedPath;\n}\n","import { access, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type {\n DeleteOptions,\n JsonFile,\n ObsidianClient,\n VaultApi,\n VaultWaitForContentOptions,\n VaultWriteOptions,\n} from \"../core/types\";\nimport { normalizeScope, resolveFilesystemPath, resolveVaultPath } from \"./paths\";\n\ninterface CreateVaultApiOptions {\n obsidian: ObsidianClient;\n root?: string;\n}\n\nexport function createVaultApi(options: CreateVaultApiOptions): VaultApi {\n const scopeRoot = normalizeScope(options.root);\n\n return {\n async delete(targetPath, deleteOptions: DeleteOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await rm(resolvedPath, {\n force: true,\n recursive: true,\n });\n\n if (deleteOptions.permanent === false) {\n return;\n }\n },\n async exists(targetPath) {\n try {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await access(resolvedPath);\n return true;\n } catch {\n return false;\n }\n },\n json<T = unknown>(targetPath: string) {\n const jsonFile: JsonFile<T> = {\n async patch(updater) {\n const currentValue = await jsonFile.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await jsonFile.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n const rawValue = await readFile(resolvedPath, \"utf8\");\n return JSON.parse(rawValue) as T;\n },\n async write(value) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n\n return jsonFile;\n },\n async mkdir(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(resolvedPath, { recursive: true });\n },\n async read(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n return readFile(resolvedPath, \"utf8\");\n },\n async waitForContent(targetPath, predicate, waitOptions: VaultWaitForContentOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n\n return options.obsidian.waitFor(\n async () => {\n try {\n const content = await readFile(resolvedPath, \"utf8\");\n return (await predicate(content)) ? content : false;\n } catch {\n return false;\n }\n },\n {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to match content`,\n ...waitOptions,\n },\n );\n },\n async waitForExists(targetPath, waitOptions) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n\n await options.obsidian.waitFor(\n async () => {\n try {\n await access(resolvedPath);\n return true;\n } catch {\n return false;\n }\n },\n {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to exist`,\n ...waitOptions,\n },\n );\n },\n async waitForMissing(targetPath, waitOptions) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n\n await options.obsidian.waitFor(\n async () => {\n try {\n await access(resolvedPath);\n return false;\n } catch {\n return true;\n }\n },\n {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to be removed`,\n ...waitOptions,\n },\n );\n },\n async write(targetPath, content, writeOptions: VaultWriteOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, content, \"utf8\");\n\n if (!writeOptions.waitForContent) {\n return;\n }\n\n const predicate =\n typeof writeOptions.waitForContent === \"function\"\n ? writeOptions.waitForContent\n : (value: string) => value === content;\n\n await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);\n },\n };\n}\n","import { posix as pathPosix } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient, SandboxApi } from \"../core/types\";\nimport { createVaultApi } from \"./vault\";\n\ninterface CreateSandboxApiOptions {\n obsidian: ObsidianClient;\n sandboxRoot: string;\n testName: string;\n}\n\nexport async function createSandboxApi(options: CreateSandboxApiOptions): Promise<SandboxApi> {\n const root = pathPosix.join(\n options.sandboxRoot,\n `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`,\n );\n const vault = createVaultApi({\n obsidian: options.obsidian,\n root,\n });\n\n await vault.mkdir(\".\");\n\n return {\n ...vault,\n async cleanup() {\n await vault.delete(\".\", { permanent: true });\n },\n path(...segments: string[]) {\n return pathPosix.join(root, ...segments);\n },\n root,\n };\n}\n\nfunction sanitizeSegment(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 80) || \"test\"\n );\n}\n"],"mappings":"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"}
@@ -8,6 +8,21 @@ interface ExecOptions {
8
8
  env?: NodeJS.ProcessEnv;
9
9
  timeoutMs?: number;
10
10
  }
11
+ type VaultContentPredicate = (content: string) => boolean | Promise<boolean>;
12
+ type VaultWaitForContentOptions = WaitForOptions;
13
+ interface VaultWriteOptions {
14
+ waitForContent?: boolean | VaultContentPredicate;
15
+ waitOptions?: VaultWaitForContentOptions;
16
+ }
17
+ type PluginDataPredicate<T = unknown> = (data: T) => boolean | Promise<boolean>;
18
+ type PluginWaitForDataOptions = WaitForOptions;
19
+ interface PluginWaitUntilReadyOptions extends WaitForOptions {
20
+ commandId?: string;
21
+ }
22
+ interface PluginReloadOptions extends ExecOptions {
23
+ readyOptions?: PluginWaitUntilReadyOptions;
24
+ waitUntilReady?: boolean;
25
+ }
11
26
  interface ExecResult {
12
27
  argv: string[];
13
28
  command: string;
@@ -64,8 +79,10 @@ interface PluginHandle {
64
79
  disable(options?: PluginToggleOptions): Promise<void>;
65
80
  enable(options?: PluginToggleOptions): Promise<void>;
66
81
  isEnabled(): Promise<boolean>;
67
- reload(): Promise<void>;
82
+ reload(options?: PluginReloadOptions): Promise<void>;
68
83
  restoreData(): Promise<void>;
84
+ waitForData<T = unknown>(predicate: PluginDataPredicate<T>, options?: PluginWaitForDataOptions): Promise<T>;
85
+ waitUntilReady(options?: PluginWaitUntilReadyOptions): Promise<void>;
69
86
  }
70
87
  interface ObsidianAppHandle {
71
88
  reload(options?: ExecOptions): Promise<void>;
@@ -118,6 +135,7 @@ interface ObsidianClient {
118
135
  open(options: OpenFileOptions, execOptions?: ExecOptions): Promise<void>;
119
136
  openTab(options?: OpenTabOptions, execOptions?: ExecOptions): Promise<void>;
120
137
  plugin(id: string): PluginHandle;
138
+ sleep(ms: number): Promise<void>;
121
139
  tabs(options?: TabsOptions, execOptions?: ExecOptions): Promise<WorkspaceTab[]>;
122
140
  vaultPath(): Promise<string>;
123
141
  verify(): Promise<void>;
@@ -126,6 +144,7 @@ interface ObsidianClient {
126
144
  }
127
145
  interface CreateObsidianClientOptions {
128
146
  bin?: string;
147
+ defaultExecOptions?: ExecOptions;
129
148
  intervalMs?: number;
130
149
  timeoutMs?: number;
131
150
  transport?: CommandTransport;
@@ -140,9 +159,10 @@ interface VaultApi {
140
159
  json<T = unknown>(path: string): JsonFile<T>;
141
160
  mkdir(path: string): Promise<void>;
142
161
  read(path: string): Promise<string>;
162
+ waitForContent(path: string, predicate: VaultContentPredicate, options?: VaultWaitForContentOptions): Promise<string>;
143
163
  waitForExists(path: string, options?: WaitForOptions): Promise<void>;
144
164
  waitForMissing(path: string, options?: WaitForOptions): Promise<void>;
145
- write(path: string, content: string): Promise<void>;
165
+ write(path: string, content: string, options?: VaultWriteOptions): Promise<void>;
146
166
  }
147
167
  interface SandboxApi extends VaultApi {
148
168
  readonly root: string;
@@ -150,7 +170,8 @@ interface SandboxApi extends VaultApi {
150
170
  path(...segments: string[]): string;
151
171
  }
152
172
  //#endregion
153
- //#region src/fixtures/types.d.ts
173
+ //#region src/artifacts/failure-artifacts.d.ts
174
+ declare const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
154
175
  interface FailureArtifactOptions {
155
176
  activeFile?: boolean;
156
177
  dom?: boolean;
@@ -159,6 +180,25 @@ interface FailureArtifactOptions {
159
180
  tabs?: boolean;
160
181
  workspace?: boolean;
161
182
  }
183
+ interface FailureArtifactTask {
184
+ id: string;
185
+ name: string;
186
+ }
187
+ interface FailureArtifactConfig {
188
+ artifactsDir: string;
189
+ capture: Required<FailureArtifactOptions>;
190
+ enabled: boolean;
191
+ }
192
+ interface FailureArtifactRegistrationOptions {
193
+ artifactsDir?: string;
194
+ captureOnFailure?: boolean | FailureArtifactOptions;
195
+ }
196
+ interface CaptureFailureArtifactsOptions extends FailureArtifactRegistrationOptions {
197
+ plugin?: PluginHandle;
198
+ }
199
+ declare function captureFailureArtifacts(task: FailureArtifactTask, obsidian: ObsidianClient, options: CaptureFailureArtifactsOptions): Promise<string | undefined>;
200
+ //#endregion
201
+ //#region src/fixtures/types.d.ts
162
202
  interface SharedVaultLockOptions {
163
203
  heartbeatMs?: number;
164
204
  lockRoot?: string;
@@ -238,5 +278,5 @@ declare function inspectVaultRunLock({
238
278
  }: Pick<AcquireVaultRunLockOptions, "lockRoot" | "staleMs" | "vaultPath">): Promise<VaultRunLockState | null>;
239
279
  declare function readVaultRunLockMarker(obsidian: ObsidianClient): Promise<VaultRunLockMetadata | null>;
240
280
  //#endregion
241
- export { ObsidianCommandHandle as A, WorkspaceNode as B, ExecOptions as C, ObsidianAppHandle as D, JsonFileUpdater as E, RestartAppOptions as F, WorkspaceTab as H, SandboxApi as I, TabsOptions as L, OpenFileOptions as M, OpenTabOptions as N, ObsidianArg as O, PluginHandle as P, VaultApi as R, DevDomResult as S, JsonFile as T, WorkspaceOptions as V, VaultSeedEntry as _, acquireVaultRunLock as a, CreateObsidianClientOptions as b, readVaultRunLockMarker as c, ObsidianFixtures as d, ObsidianTest as f, VaultSeed as g, SharedVaultLockOptions as h, VaultRunLockState as i, ObsidianDevHandle as j, ObsidianClient as k, CreateObsidianTestOptions as l, PluginTest as m, VaultRunLock as n, clearVaultRunLockMarker as o, PluginFixtures as p, VaultRunLockMetadata as r, inspectVaultRunLock as s, AcquireVaultRunLockOptions as t, CreatePluginTestOptions as u, CommandListOptions as v, ExecResult as w, DevDomQueryOptions as x, CommandTransport as y, WaitForOptions as z };
242
- //# sourceMappingURL=vault-lock-DarzOEzv.d.mts.map
281
+ export { WaitForOptions as $, ExecOptions as A, OpenTabOptions as B, FailureArtifactTask as C, CreateObsidianClientOptions as D, CommandTransport as E, ObsidianArg as F, PluginWaitUntilReadyOptions as G, PluginHandle as H, ObsidianClient as I, TabsOptions as J, RestartAppOptions as K, ObsidianCommandHandle as L, JsonFile as M, JsonFileUpdater as N, DevDomQueryOptions as O, ObsidianAppHandle as P, VaultWriteOptions as Q, ObsidianDevHandle as R, FailureArtifactRegistrationOptions as S, CommandListOptions as T, PluginReloadOptions as U, PluginDataPredicate as V, PluginWaitForDataOptions as W, VaultContentPredicate as X, VaultApi as Y, VaultWaitForContentOptions as Z, VaultSeedEntry as _, acquireVaultRunLock as a, FailureArtifactConfig as b, readVaultRunLockMarker as c, ObsidianFixtures as d, WorkspaceNode as et, ObsidianTest as f, VaultSeed as g, SharedVaultLockOptions as h, VaultRunLockState as i, ExecResult as j, DevDomResult as k, CreateObsidianTestOptions as l, PluginTest as m, VaultRunLock as n, WorkspaceTab as nt, clearVaultRunLockMarker as o, PluginFixtures as p, SandboxApi as q, VaultRunLockMetadata as r, inspectVaultRunLock as s, AcquireVaultRunLockOptions as t, WorkspaceOptions as tt, CreatePluginTestOptions as u, CaptureFailureArtifactsOptions as v, captureFailureArtifacts as w, FailureArtifactOptions as x, DEFAULT_FAILURE_ARTIFACTS_DIR as y, OpenFileOptions as z };
282
+ //# sourceMappingURL=vault-lock-LmqAsLDT.d.mts.map
package/dist/vitest.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as VaultSeedEntry, a as acquireVaultRunLock, c as readVaultRunLockMarker, d as ObsidianFixtures, f as ObsidianTest, g as VaultSeed, h as SharedVaultLockOptions, i as VaultRunLockState, l as CreateObsidianTestOptions, m as PluginTest, n as VaultRunLock, o as clearVaultRunLockMarker, p as PluginFixtures, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, u as CreatePluginTestOptions } from "./vault-lock-DarzOEzv.mjs";
1
+ import { _ as VaultSeedEntry, a as acquireVaultRunLock, c as readVaultRunLockMarker, d as ObsidianFixtures, f as ObsidianTest, g as VaultSeed, h as SharedVaultLockOptions, i as VaultRunLockState, l as CreateObsidianTestOptions, m as PluginTest, n as VaultRunLock, o as clearVaultRunLockMarker, p as PluginFixtures, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, u as CreatePluginTestOptions } from "./vault-lock-LmqAsLDT.mjs";
2
2
 
3
3
  //#region src/fixtures/create-obsidian-test.d.ts
4
4
  declare function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest;
package/dist/vitest.mjs CHANGED
@@ -1,100 +1,21 @@
1
- import { a as inspectVaultRunLock, c as getClientInternals, i as clearVaultRunLockMarker, n as createVaultApi, o as readVaultRunLockMarker, r as acquireVaultRunLock, s as createObsidianClient, t as createSandboxApi } from "./sandbox-Cz3rj_Rn.mjs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import path from "node:path";
1
+ import { a as clearVaultRunLockMarker, d as createObsidianClient, f as getClientInternals, i as acquireVaultRunLock, l as captureFailureArtifacts, n as createVaultApi, o as inspectVaultRunLock, r as resolveFilesystemPath, s as readVaultRunLockMarker, t as createSandboxApi, u as capturePluginFailureArtifacts } from "./sandbox--mUbNsh7.mjs";
4
2
  import { test } from "vite-plus/test";
5
3
  //#region src/fixtures/failure-artifacts.ts
6
- const DEFAULT_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
7
- function getFailureArtifactConfig(options) {
8
- if (!options.captureOnFailure) return {
9
- artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),
10
- capture: {
11
- activeFile: true,
12
- dom: true,
13
- editorText: true,
14
- screenshot: true,
15
- tabs: true,
16
- workspace: true
17
- },
18
- enabled: false
19
- };
20
- const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
21
- return {
22
- artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),
23
- capture: {
24
- activeFile: overrides.activeFile ?? true,
25
- dom: overrides.dom ?? true,
26
- editorText: overrides.editorText ?? true,
27
- screenshot: overrides.screenshot ?? true,
28
- tabs: overrides.tabs ?? true,
29
- workspace: overrides.workspace ?? true
30
- },
31
- enabled: true
32
- };
33
- }
34
- function getFailureArtifactDirectory(artifactsDir, task) {
35
- const suffix = task.id.split("_").at(-1) ?? "test";
36
- return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);
37
- }
38
- function registerFailureArtifacts(context, obsidian, options) {
39
- const config = getFailureArtifactConfig(options);
40
- if (!config.enabled) return;
4
+ function registerFailureArtifacts(context, obsidian, options, plugin) {
5
+ if (!options.captureOnFailure) return;
41
6
  context.onTestFailed(async () => {
42
- const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);
43
- await mkdir(artifactDirectory, { recursive: true });
44
- await Promise.all([
45
- captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await obsidian.dev.eval("app.workspace.getActiveFile()?.path ?? null") })),
46
- captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
47
- inner: true,
48
- selector: ".workspace"
49
- }))),
50
- captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null") })),
51
- captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
52
- captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
53
- captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace())
54
- ]);
7
+ await captureFailureArtifacts(context.task, obsidian, {
8
+ ...options,
9
+ plugin
10
+ });
55
11
  });
56
12
  }
57
13
  function registerPluginFailureArtifacts(context, plugin, options) {
58
- const config = getFailureArtifactConfig(options);
59
- if (!config.enabled) return;
14
+ if (!options.captureOnFailure) return;
60
15
  context.onTestFailed(async () => {
61
- const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);
62
- await mkdir(artifactDirectory, { recursive: true });
63
- await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () => plugin.data().read());
16
+ await capturePluginFailureArtifacts(context.task, plugin, options);
64
17
  });
65
18
  }
66
- async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
67
- if (!enabled) return;
68
- try {
69
- const value = await readValue();
70
- await writeFile(path.join(artifactDirectory, filename), `${JSON.stringify(value, null, 2)}\n`, "utf8");
71
- } catch (error) {
72
- await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
73
- }
74
- }
75
- async function captureScreenshotArtifact(artifactDirectory, enabled, obsidian) {
76
- if (!enabled) return;
77
- const screenshotPath = path.join(artifactDirectory, "screenshot.png");
78
- try {
79
- await obsidian.dev.screenshot(screenshotPath);
80
- } catch (error) {
81
- await writeFile(path.join(artifactDirectory, "screenshot.error.txt"), formatArtifactError(error), "utf8");
82
- }
83
- }
84
- async function captureTextArtifact(artifactDirectory, filename, enabled, readValue) {
85
- if (!enabled) return;
86
- try {
87
- await writeFile(path.join(artifactDirectory, filename), await readValue(), "utf8");
88
- } catch (error) {
89
- await writeFile(path.join(artifactDirectory, `${filename}.error.txt`), formatArtifactError(error), "utf8");
90
- }
91
- }
92
- function formatArtifactError(error) {
93
- return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
94
- }
95
- function sanitizeForPath(value) {
96
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
97
- }
98
19
  function createBaseFixtures(options, fixtureOptions = {}) {
99
20
  const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
100
21
  return {
@@ -183,22 +104,19 @@ function createPluginTest(options) {
183
104
  return test.extend(fixtures);
184
105
  }
185
106
  async function applyVaultSeed(obsidian, seedVault) {
186
- const vaultRoot = await obsidian.vaultPath();
107
+ const vault = createVaultApi({ obsidian });
187
108
  for (const [targetPath, value] of Object.entries(seedVault)) {
188
- const resolvedPath = path.resolve(vaultRoot, ...targetPath.split("/").filter(Boolean));
189
- const normalizedVaultRoot = path.resolve(vaultRoot);
190
- if (resolvedPath !== normalizedVaultRoot && !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)) throw new Error(`Seed path escapes the vault root: ${targetPath}`);
109
+ const resolvedPath = await resolveFilesystemPath(obsidian, "", targetPath);
191
110
  await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);
192
- await mkdir(path.dirname(resolvedPath), { recursive: true });
193
- await writeSeedValue(resolvedPath, value);
111
+ await writeSeedValue(vault, targetPath, value);
194
112
  }
195
113
  }
196
- async function writeSeedValue(resolvedPath, value) {
114
+ async function writeSeedValue(vault, targetPath, value) {
197
115
  if (typeof value === "string") {
198
- await writeFile(resolvedPath, value, "utf8");
116
+ await vault.write(targetPath, value, { waitForContent: true });
199
117
  return;
200
118
  }
201
- await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\n`, "utf8");
119
+ await vault.write(targetPath, `${JSON.stringify(value.json, null, 2)}\n`, { waitForContent: true });
202
120
  }
203
121
  //#endregion
204
122
  export { acquireVaultRunLock, clearVaultRunLockMarker, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { TestContext } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions, FailureArtifactOptions } from \"./types\";\n\nconst DEFAULT_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\ninterface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport function getFailureArtifactConfig(\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: Pick<TestContext[\"task\"], \"id\" | \"name\">,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(\n artifactDirectory,\n \"editor.json\",\n config.capture.editorText,\n async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n }),\n ),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () =>\n obsidian.tabs(),\n ),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n ]);\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n });\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\nimport { acquireVaultRunLock, clearVaultRunLockMarker, type VaultRunLock } from \"./vault-lock\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\nexport interface BaseFixtureState {\n _vaultLock: VaultRunLock | null;\n}\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n _vaultLock: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use: (vaultLock: VaultRunLock | null) => Promise<void>) => {\n if (!options.sharedVaultLock) {\n await use(null);\n return;\n }\n\n const lockClient = createObsidianClient(options);\n await lockClient.verify();\n\n const lockOptions = options.sharedVaultLock === true ? {} : options.sharedVaultLock;\n const vaultLock = await acquireVaultRunLock({\n ...lockOptions,\n vaultName: options.vault,\n vaultPath: await lockClient.vaultPath(),\n });\n\n await vaultLock.publishMarker(lockClient);\n try {\n await use(vaultLock);\n } finally {\n try {\n await clearVaultRunLockMarker(lockClient);\n } catch {}\n\n await vaultLock.release();\n }\n },\n { scope: \"worker\" },\n ],\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n {\n _vaultLock,\n onTestFailed,\n task,\n }: Pick<BaseFixtureState & TestContext, \"_vaultLock\" | \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n if (_vaultLock) {\n await _vaultLock.publishMarker(obsidian);\n }\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures & BaseFixtureState>(\n createBaseFixtures(options) as never,\n ) as ObsidianTest;\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n const fixtures = {\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use: (plugin: PluginFixtures[\"plugin\"]) => Promise<void>,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n };\n\n return base.extend<PluginFixtures & BaseFixtureState>(fixtures as never) as PluginTest;\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vaultRoot = await obsidian.vaultPath();\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = path.resolve(vaultRoot, ...targetPath.split(\"/\").filter(Boolean));\n const normalizedVaultRoot = path.resolve(vaultRoot);\n\n if (\n resolvedPath !== normalizedVaultRoot &&\n !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)\n ) {\n throw new Error(`Seed path escapes the vault root: ${targetPath}`);\n }\n\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeSeedValue(resolvedPath, value);\n }\n}\n\nasync function writeSeedValue(resolvedPath: string, value: VaultSeedEntry): Promise<void> {\n if (typeof value === \"string\") {\n await writeFile(resolvedPath, value, \"utf8\");\n return;\n }\n\n await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":";;;;;AAQA,MAAM,wBAAwB;AAQ9B,SAAgB,yBACd,SACuB;AACvB,KAAI,CAAC,QAAQ,iBACX,QAAO;EACL,cAAc,KAAK,QAAQ,QAAQ,gBAAgB,sBAAsB;EACzE,SAAS;GACP,YAAY;GACZ,KAAK;GACL,YAAY;GACZ,YAAY;GACZ,MAAM;GACN,WAAW;GACZ;EACD,SAAS;EACV;CAGH,MAAM,YAAY,QAAQ,qBAAqB,OAAO,EAAE,GAAG,QAAQ;AAEnE,QAAO;EACL,cAAc,KAAK,QAAQ,QAAQ,gBAAgB,sBAAsB;EACzE,SAAS;GACP,YAAY,UAAU,cAAc;GACpC,KAAK,UAAU,OAAO;GACtB,YAAY,UAAU,cAAc;GACpC,YAAY,UAAU,cAAc;GACpC,MAAM,UAAU,QAAQ;GACxB,WAAW,UAAU,aAAa;GACnC;EACD,SAAS;EACV;;AAGH,SAAgB,4BACd,cACA,MACQ;CACR,MAAM,SAAS,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,GAAG,IAAI;AAC5C,QAAO,KAAK,KAAK,cAAc,GAAG,gBAAgB,KAAK,KAAK,CAAC,GAAG,SAAS;;AAG3E,SAAgB,yBACd,SACA,UACA,SACM;CACN,MAAM,SAAS,yBAAyB,QAAQ;AAEhD,KAAI,CAAC,OAAO,QACV;AAGF,SAAQ,aAAa,YAAY;EAC/B,MAAM,oBAAoB,4BAA4B,OAAO,cAAc,QAAQ,KAAK;AACxF,QAAM,MAAM,mBAAmB,EAAE,WAAW,MAAM,CAAC;AAEnD,QAAM,QAAQ,IAAI;GAChB,oBACE,mBACA,oBACA,OAAO,QAAQ,YACf,aAAa,EACX,YAAY,MAAM,SAAS,IAAI,KAC7B,8CACD,EACF,EACF;GACD,oBAAoB,mBAAmB,WAAW,OAAO,QAAQ,KAAK,YACpE,OACE,MAAM,SAAS,IAAI,IAAI;IACrB,OAAO;IACP,UAAU;IACX,CAAC,CACH,CACF;GACD,oBACE,mBACA,eACA,OAAO,QAAQ,YACf,aAAa,EACX,MAAM,MAAM,SAAS,IAAI,KACvB,+DACD,EACF,EACF;GACD,0BAA0B,mBAAmB,OAAO,QAAQ,YAAY,SAAS;GACjF,oBAAoB,mBAAmB,aAAa,OAAO,QAAQ,YACjE,SAAS,MAAM,CAChB;GACD,oBAAoB,mBAAmB,kBAAkB,OAAO,QAAQ,iBACtE,SAAS,WAAW,CACrB;GACF,CAAC;GACF;;AAGJ,SAAgB,+BACd,SACA,QACA,SACM;CACN,MAAM,SAAS,yBAAyB,QAAQ;AAEhD,KAAI,CAAC,OAAO,QACV;AAGF,SAAQ,aAAa,YAAY;EAC/B,MAAM,oBAAoB,4BAA4B,OAAO,cAAc,QAAQ,KAAK;AACxF,QAAM,MAAM,mBAAmB,EAAE,WAAW,MAAM,CAAC;AACnD,QAAM,oBAAoB,mBAAmB,GAAG,OAAO,GAAG,aAAa,YACrE,OAAO,MAAM,CAAC,MAAM,CACrB;GACD;;AAGJ,eAAe,oBACb,mBACA,UACA,SACA,WACe;AACf,KAAI,CAAC,QACH;AAGF,KAAI;EACF,MAAM,QAAQ,MAAM,WAAW;AAC/B,QAAM,UACJ,KAAK,KAAK,mBAAmB,SAAS,EACtC,GAAG,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC,KAClC,OACD;UACM,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,GAAG,SAAS,YAAY,EACrD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,eAAe,0BACb,mBACA,SACA,UACe;AACf,KAAI,CAAC,QACH;CAGF,MAAM,iBAAiB,KAAK,KAAK,mBAAmB,iBAAiB;AAErE,KAAI;AACF,QAAM,SAAS,IAAI,WAAW,eAAe;UACtC,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,uBAAuB,EACpD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,eAAe,oBACb,mBACA,UACA,SACA,WACe;AACf,KAAI,CAAC,QACH;AAGF,KAAI;AACF,QAAM,UAAU,KAAK,KAAK,mBAAmB,SAAS,EAAE,MAAM,WAAW,EAAE,OAAO;UAC3E,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,GAAG,SAAS,YAAY,EACrD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,SAAS,oBAAoB,OAAwB;AACnD,QAAO,iBAAiB,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO,MAAM,CAAC;;AAGzF,SAAS,gBAAgB,OAAuB;AAC9C,QACE,MACG,MAAM,CACN,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG,CACvB,MAAM,GAAG,GAAG,IAAI;;AChMvB,SAAgB,mBACd,SACA,iBAAqC,EAAE,EACvC;CACA,MAAM,cACJ,eAAe,iBAAiB,aAA6B,eAAe,EAAE,UAAU,CAAC;AAE3F,QAAO;EACL,YAAY,CAEV,OAAO,IAAI,QAA2D;AACpE,OAAI,CAAC,QAAQ,iBAAiB;AAC5B,UAAM,IAAI,KAAK;AACf;;GAGF,MAAM,aAAa,qBAAqB,QAAQ;AAChD,SAAM,WAAW,QAAQ;GAGzB,MAAM,YAAY,MAAM,oBAAoB;IAC1C,GAFkB,QAAQ,oBAAoB,OAAO,EAAE,GAAG,QAAQ;IAGlE,WAAW,QAAQ;IACnB,WAAW,MAAM,WAAW,WAAW;IACxC,CAAC;AAEF,SAAM,UAAU,cAAc,WAAW;AACzC,OAAI;AACF,UAAM,IAAI,UAAU;aACZ;AACR,QAAI;AACF,WAAM,wBAAwB,WAAW;YACnC;AAER,UAAM,UAAU,SAAS;;KAG7B,EAAE,OAAO,UAAU,CACpB;EAED,UAAU,OACR,EACE,YACA,cACA,QAEF,QACG;GACH,MAAM,WAAW,qBAAqB,QAAQ;AAE9C,SAAM,SAAS,QAAQ;AACvB,OAAI,WACF,OAAM,WAAW,cAAc,SAAS;AAE1C,4BAAyB;IAAE;IAAc;IAAM,EAAE,UAAU,QAAQ;AAEnE,OAAI;AACF,UAAM,IAAI,SAAS;aACX;AACR,UAAM,mBAAmB,SAAS,CAAC,YAAY;;;EAGnD,SAAS,OACP,EAAE,YACF,QACG;GACH,MAAM,UAAU,MAAM,iBAAiB;IACrC;IACA,aAAa,QAAQ,eAAA;IACrB,UAAU;IACX,CAAC;AAEF,OAAI;AACF,UAAM,IAAI,QAAQ;aACV;AACR,UAAM,QAAQ,SAAS;;;EAG3B,OAAO,OACL,EAAE,YACF,QACG;AACH,SAAM,IAAI,MAAM,YAAY,SAAS,CAAC;;EAEzC;;;;ACpGH,SAAgB,mBAAmB,SAAkD;AACnF,QAAOA,KAAK,OACV,mBAAmB,QAAQ,CAC5B;;;;ACWH,SAAgB,iBAAiB,SAA8C;CAC7E,MAAM,WAAW;EACf,GAAG,mBAAmB,SAAS,EAC7B,MAAM,YAAY,UAAU;AAC1B,OAAI,QAAQ,UACV,OAAM,eAAe,UAAU,QAAQ,UAAU;AAGnD,UAAO,eAAe,EAAE,UAAU,CAAC;KAEtC,CAAC;EACF,QAAQ,OACN,EACE,UACA,cACA,QAEF,QACG;GACH,MAAM,SAAS,SAAS,OAAO,QAAQ,SAAS;GAChD,MAAM,aAAa,MAAM,OAAO,WAAW;AAE3C,OAAI,CAAC,WACH,OAAM,OAAO,OAAO,EAAE,QAAQ,QAAQ,cAAc,CAAC;AAGvD,OAAI,QAAQ,mBAAmB,KAAA,EAC7B,OAAM,OAAO,MAAM,CAAC,MAAM,QAAQ,eAAe;AAGnD,kCAA+B;IAAE;IAAc;IAAM,EAAE,QAAQ,QAAQ;AAEvE,OAAI;AACF,UAAM,IAAI,OAAO;aACT;AACR,QAAI,CAAC,WACH,OAAM,OAAO,QAAQ,EAAE,QAAQ,QAAQ,cAAc,CAAC;;;EAI7D;AAED,QAAOC,KAAK,OAA0C,SAAkB;;AAG1E,eAAe,eAAe,UAA0B,WAAqC;CAC3F,MAAM,YAAY,MAAM,SAAS,WAAW;AAE5C,MAAK,MAAM,CAAC,YAAY,UAAU,OAAO,QAAQ,UAAU,EAAE;EAC3D,MAAM,eAAe,KAAK,QAAQ,WAAW,GAAG,WAAW,MAAM,IAAI,CAAC,OAAO,QAAQ,CAAC;EACtF,MAAM,sBAAsB,KAAK,QAAQ,UAAU;AAEnD,MACE,iBAAiB,uBACjB,CAAC,aAAa,WAAW,GAAG,sBAAsB,KAAK,MAAM,CAE7D,OAAM,IAAI,MAAM,qCAAqC,aAAa;AAGpE,QAAM,mBAAmB,SAAS,CAAC,iBAAiB,aAAa;AACjE,QAAM,MAAM,KAAK,QAAQ,aAAa,EAAE,EAAE,WAAW,MAAM,CAAC;AAC5D,QAAM,eAAe,cAAc,MAAM;;;AAI7C,eAAe,eAAe,cAAsB,OAAsC;AACxF,KAAI,OAAO,UAAU,UAAU;AAC7B,QAAM,UAAU,cAAc,OAAO,OAAO;AAC5C;;AAGF,OAAM,UAAU,cAAc,GAAG,KAAK,UAAU,MAAM,MAAM,MAAM,EAAE,CAAC,KAAK,OAAO"}
1
+ {"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import type { TestContext } from \"vite-plus/test\";\n\nimport {\n captureFailureArtifacts,\n capturePluginFailureArtifacts,\n} from \"../artifacts/failure-artifacts\";\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions } from \"./types\";\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n plugin?: PluginHandle,\n): void {\n if (!options.captureOnFailure) {\n return;\n }\n\n context.onTestFailed(async () => {\n await captureFailureArtifacts(context.task, obsidian, {\n ...options,\n plugin,\n });\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n if (!options.captureOnFailure) {\n return;\n }\n\n context.onTestFailed(async () => {\n await capturePluginFailureArtifacts(context.task, plugin, options);\n });\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\nimport { acquireVaultRunLock, clearVaultRunLockMarker, type VaultRunLock } from \"./vault-lock\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\nexport interface BaseFixtureState {\n _vaultLock: VaultRunLock | null;\n}\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n _vaultLock: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use: (vaultLock: VaultRunLock | null) => Promise<void>) => {\n if (!options.sharedVaultLock) {\n await use(null);\n return;\n }\n\n const lockClient = createObsidianClient(options);\n await lockClient.verify();\n\n const lockOptions = options.sharedVaultLock === true ? {} : options.sharedVaultLock;\n const vaultLock = await acquireVaultRunLock({\n ...lockOptions,\n vaultName: options.vault,\n vaultPath: await lockClient.vaultPath(),\n });\n\n await vaultLock.publishMarker(lockClient);\n try {\n await use(vaultLock);\n } finally {\n try {\n await clearVaultRunLockMarker(lockClient);\n } catch {}\n\n await vaultLock.release();\n }\n },\n { scope: \"worker\" },\n ],\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n {\n _vaultLock,\n onTestFailed,\n task,\n }: Pick<BaseFixtureState & TestContext, \"_vaultLock\" | \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n if (_vaultLock) {\n await _vaultLock.publishMarker(obsidian);\n }\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures & BaseFixtureState>(\n createBaseFixtures(options) as never,\n ) as ObsidianTest;\n}\n","import { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { resolveFilesystemPath } from \"../vault/paths\";\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n const fixtures = {\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use: (plugin: PluginFixtures[\"plugin\"]) => Promise<void>,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n };\n\n return base.extend<PluginFixtures & BaseFixtureState>(fixtures as never) as PluginTest;\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vault = createVaultApi({ obsidian });\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = await resolveFilesystemPath(obsidian, \"\", targetPath);\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await writeSeedValue(vault, targetPath, value);\n }\n}\n\nasync function writeSeedValue(\n vault: VaultApi,\n targetPath: string,\n value: VaultSeedEntry,\n): Promise<void> {\n if (typeof value === \"string\") {\n await vault.write(targetPath, value, {\n waitForContent: true,\n });\n return;\n }\n\n await vault.write(targetPath, `${JSON.stringify(value.json, null, 2)}\\n`, {\n waitForContent: true,\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-e2e",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Vitest-first end-to-end test utilities for Obsidian plugins.",
5
5
  "homepage": "https://github.com/chhoumann/obsidian-e2e#readme",
6
6
  "bugs": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"sandbox-Cz3rj_Rn.mjs","names":["DEFAULT_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","pathPosix","pathPosix"],"sources":["../src/core/args.ts","../src/core/internals.ts","../src/vault/json-file.ts","../src/plugin/plugin.ts","../src/core/errors.ts","../src/core/transport.ts","../src/core/wait.ts","../src/core/client.ts","../src/fixtures/vault-lock.ts","../src/vault/vault.ts","../src/vault/sandbox.ts"],"sourcesContent":["import type { ObsidianArg } from \"./types\";\n\nexport function buildCommandArgv(\n vaultName: string,\n command: string,\n args: Record<string, ObsidianArg> = {},\n): string[] {\n const argv = [`vault=${vaultName}`, command];\n\n for (const [key, value] of Object.entries(args)) {\n if (value === false || value === null || value === undefined) {\n continue;\n }\n\n if (value === true) {\n argv.push(key);\n continue;\n }\n\n argv.push(`${key}=${String(value)}`);\n }\n\n return argv;\n}\n","import { rm, writeFile } from \"node:fs/promises\";\n\nimport type { ObsidianClient } from \"./types\";\n\ninterface SnapshotEntry {\n exists: boolean;\n value: string;\n}\n\ninterface ClientInternals {\n restoreAll(): Promise<void>;\n restoreFile(filePath: string): Promise<void>;\n snapshotFileOnce(filePath: string): Promise<void>;\n}\n\nconst clientInternals = new WeakMap<ObsidianClient, ClientInternals>();\n\nexport function attachClientInternals(client: ObsidianClient, internals: ClientInternals): void {\n clientInternals.set(client, internals);\n}\n\nexport function getClientInternals(client: ObsidianClient): ClientInternals {\n const internals = clientInternals.get(client);\n\n if (!internals) {\n throw new Error(\"Missing obsidian client internals.\");\n }\n\n return internals;\n}\n\nexport function createRestoreManager(readFile: (filePath: string) => Promise<string>) {\n const snapshots = new Map<string, SnapshotEntry>();\n\n return {\n async restoreAll() {\n const entries = [...snapshots.entries()].reverse();\n\n for (const [filePath, snapshot] of entries) {\n await restoreSnapshot(filePath, snapshot);\n }\n\n snapshots.clear();\n },\n async restoreFile(filePath: string) {\n const snapshot = snapshots.get(filePath);\n\n if (!snapshot) {\n return;\n }\n\n await restoreSnapshot(filePath, snapshot);\n snapshots.delete(filePath);\n },\n async snapshotFileOnce(filePath: string) {\n if (snapshots.has(filePath)) {\n return;\n }\n\n try {\n snapshots.set(filePath, {\n exists: true,\n value: await readFile(filePath),\n });\n } catch (error) {\n if (isMissingFileError(error)) {\n snapshots.set(filePath, {\n exists: false,\n value: \"\",\n });\n return;\n }\n\n throw error;\n }\n },\n };\n}\n\nasync function restoreSnapshot(filePath: string, snapshot: SnapshotEntry): Promise<void> {\n if (snapshot.exists) {\n await writeFile(filePath, snapshot.value, \"utf8\");\n return;\n }\n\n await rm(filePath, { force: true, recursive: true });\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return Boolean(error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { JsonFile, JsonFileUpdater } from \"../core/types\";\n\nexport function createJsonFile<T = unknown>(\n filePath: string,\n beforeMutate?: () => Promise<void>,\n): JsonFile<T> {\n return {\n async patch(updater: JsonFileUpdater<T>) {\n await beforeMutate?.();\n\n const currentValue = await this.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await this.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const value = await readFile(filePath, \"utf8\");\n return JSON.parse(value) as T;\n },\n async write(value: T) {\n await beforeMutate?.();\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n}\n","import path from \"node:path\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { JsonFile, ObsidianClient, PluginHandle, PluginToggleOptions } from \"../core/types\";\nimport { createJsonFile } from \"../vault/json-file\";\n\nexport function createPluginHandle(client: ObsidianClient, id: string): PluginHandle {\n async function resolveDataPath() {\n const vaultPath = await client.vaultPath();\n return path.join(vaultPath, \".obsidian\", \"plugins\", id, \"data.json\");\n }\n\n return {\n data<T = unknown>(): JsonFile<T> {\n return {\n async patch(updater) {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).patch(updater);\n },\n async read() {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath).read();\n },\n async write(value) {\n const dataPath = await resolveDataPath();\n await createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).write(value);\n },\n };\n },\n async dataPath() {\n return resolveDataPath();\n },\n async disable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:disable\", {\n filter: options.filter,\n id,\n });\n },\n async enable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:enable\", {\n filter: options.filter,\n id,\n });\n },\n id,\n async isEnabled() {\n const output = await client.execText(\"plugin\", { id }, { allowNonZeroExit: true });\n return /enabled\\s+true/i.test(output);\n },\n async reload() {\n await client.exec(\"plugin:reload\", { id });\n },\n async restoreData() {\n await getClientInternals(client).restoreFile(await resolveDataPath());\n },\n };\n}\n","import type { ExecResult } from \"./types\";\n\nexport class ObsidianCommandError extends Error {\n readonly result: ExecResult;\n\n constructor(message: string, result: ExecResult) {\n super(message);\n this.name = \"ObsidianCommandError\";\n this.result = result;\n }\n}\n\nexport class WaitForTimeoutError extends Error {\n readonly causeError?: unknown;\n\n constructor(message: string, causeError?: unknown) {\n super(message);\n this.name = \"WaitForTimeoutError\";\n this.causeError = causeError;\n }\n}\n","import { spawn } from \"node:child_process\";\n\nimport { ObsidianCommandError } from \"./errors\";\nimport type { CommandTransport, ExecuteRequest, ExecResult } from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const executeCommand: CommandTransport = async ({\n allowNonZeroExit = false,\n argv,\n bin,\n cwd,\n env,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n}: ExecuteRequest): Promise<ExecResult> => {\n const child = spawn(bin, argv, {\n cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on(\"data\", (chunk) => {\n stdoutChunks.push(Buffer.from(chunk));\n });\n\n child.stderr.on(\"data\", (chunk) => {\n stderrChunks.push(Buffer.from(chunk));\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(\" \")}`));\n }, timeoutMs);\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n reject(error);\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n resolve(code ?? 0);\n });\n });\n\n const result: ExecResult = {\n argv,\n command: bin,\n exitCode,\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n };\n\n if (exitCode !== 0 && !allowNonZeroExit) {\n throw new ObsidianCommandError(\n `Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(\" \")}`,\n result,\n );\n }\n\n return result;\n};\n","import { WaitForTimeoutError } from \"./errors\";\nimport type { WaitForOptions } from \"./types\";\n\nconst DEFAULT_INTERVAL_MS = 100;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\nexport async function waitForValue<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n options: WaitForOptions = {},\n): Promise<T> {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const startTime = Date.now();\n\n let lastError: unknown;\n\n while (Date.now() - startTime <= timeoutMs) {\n try {\n const result = await fn();\n if (result !== false && result !== null && result !== undefined) {\n return result;\n }\n } catch (error) {\n lastError = error;\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n\n const label = options.message ?? \"condition\";\n throw new WaitForTimeoutError(`Timed out waiting for ${label} after ${timeoutMs}ms.`, lastError);\n}\n","import { buildCommandArgv } from \"./args\";\nimport { attachClientInternals, createRestoreManager } from \"./internals\";\nimport { createPluginHandle } from \"../plugin/plugin\";\nimport { executeCommand } from \"./transport\";\nimport type {\n CommandListOptions,\n CreateObsidianClientOptions,\n DevDomQueryOptions,\n DevDomResult,\n ExecOptions,\n ObsidianArg,\n ObsidianAppHandle,\n ObsidianCommandHandle,\n ObsidianClient,\n ObsidianDevHandle,\n OpenFileOptions,\n OpenTabOptions,\n RestartAppOptions,\n TabsOptions,\n WaitForOptions,\n WorkspaceNode,\n WorkspaceOptions,\n WorkspaceTab,\n} from \"./types\";\nimport { waitForValue } from \"./wait\";\n\nexport function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient {\n const transport = options.transport ?? executeCommand;\n const waitDefaults = {\n intervalMs: options.intervalMs,\n timeoutMs: options.timeoutMs,\n };\n\n const restoreManager = createRestoreManager(async (filePath) => {\n const { readFile } = await import(\"node:fs/promises\");\n return readFile(filePath, \"utf8\");\n });\n\n let cachedVaultPath: string | undefined;\n\n const client = {} as ObsidianClient;\n\n const app: ObsidianAppHandle = {\n async reload(execOptions: ExecOptions = {}) {\n await client.exec(\"reload\", {}, execOptions);\n },\n async restart({\n readyOptions,\n waitUntilReady = true,\n ...execOptions\n }: RestartAppOptions & ExecOptions = {}) {\n await client.exec(\"restart\", {}, execOptions);\n\n if (waitUntilReady) {\n await app.waitUntilReady(readyOptions);\n }\n },\n version(execOptions: ExecOptions = {}) {\n return client.execText(\"version\", {}, execOptions);\n },\n async waitUntilReady(waitOptions?: WaitForOptions) {\n await client.waitFor(async () => {\n try {\n await client.vaultPath();\n await client.commands();\n return true;\n } catch {\n return false;\n }\n }, waitOptions);\n },\n };\n\n const dev: ObsidianDevHandle = {\n async dom(options: DevDomQueryOptions, execOptions: ExecOptions = {}): Promise<DevDomResult> {\n const output = await client.execText(\n \"dev:dom\",\n {\n all: options.all,\n attr: options.attr,\n css: options.css,\n inner: options.inner,\n selector: options.selector,\n text: options.text,\n total: options.total,\n },\n execOptions,\n );\n\n if (options.total) {\n return Number.parseInt(output, 10);\n }\n\n if (options.all) {\n return output ? output.split(/\\r?\\n/u).filter(Boolean) : [];\n }\n\n return output;\n },\n async eval<T = unknown>(code: string, execOptions: ExecOptions = {}) {\n const output = await client.execText(\n \"eval\",\n {\n code,\n },\n execOptions,\n );\n return parseDevEvalOutput<T>(output);\n },\n async screenshot(targetPath: string, execOptions: ExecOptions = {}) {\n await client.exec(\n \"dev:screenshot\",\n {\n path: targetPath,\n },\n execOptions,\n );\n\n return targetPath;\n },\n };\n\n Object.assign(client, {\n app,\n bin: options.bin ?? \"obsidian\",\n dev,\n command(id: string): ObsidianCommandHandle {\n return {\n async exists(commandOptions: CommandListOptions = {}) {\n const commands = await client.commands({\n ...commandOptions,\n filter: commandOptions.filter ?? id,\n });\n\n return commands.includes(id);\n },\n id,\n async run(execOptions: ExecOptions = {}) {\n await client.exec(\"command\", { id }, execOptions);\n },\n };\n },\n async commands(\n commandOptions: CommandListOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<string[]> {\n const output = await client.execText(\n \"commands\",\n {\n filter: commandOptions.filter,\n },\n execOptions,\n );\n return parseCommandIds(output);\n },\n exec(command: string, args: Record<string, ObsidianArg> = {}, execOptions: ExecOptions = {}) {\n return transport({\n ...execOptions,\n argv: buildCommandArgv(options.vault, command, args),\n bin: this.bin,\n });\n },\n async execJson<T = unknown>(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const output = await this.execText(command, args, execOptions);\n return JSON.parse(output) as T;\n },\n async execText(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const result = await this.exec(command, args, execOptions);\n return result.stdout.trimEnd();\n },\n async open(openOptions: OpenFileOptions, execOptions: ExecOptions = {}) {\n await client.exec(\n \"open\",\n {\n file: openOptions.file,\n newtab: openOptions.newTab,\n path: openOptions.path,\n },\n execOptions,\n );\n },\n async openTab(tabOptions: OpenTabOptions = {}, execOptions: ExecOptions = {}) {\n await client.exec(\n \"tab:open\",\n {\n file: tabOptions.file,\n group: tabOptions.group,\n view: tabOptions.view,\n },\n execOptions,\n );\n },\n plugin(id: string) {\n return createPluginHandle(this, id);\n },\n async tabs(\n tabOptions: TabsOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceTab[]> {\n const output = await client.execText(\n \"tabs\",\n {\n ids: tabOptions.ids ?? true,\n },\n execOptions,\n );\n return parseTabs(output);\n },\n async vaultPath() {\n if (!cachedVaultPath) {\n cachedVaultPath = await this.execText(\"vault\", { info: \"path\" });\n }\n\n return cachedVaultPath;\n },\n async verify() {\n await transport({\n argv: [\"--help\"],\n bin: this.bin,\n });\n\n await this.vaultPath();\n },\n vaultName: options.vault,\n waitFor<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n waitOptions?: WaitForOptions,\n ) {\n return waitForValue(fn, {\n ...waitDefaults,\n ...waitOptions,\n });\n },\n async workspace(\n workspaceOptions: WorkspaceOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceNode[]> {\n const output = await client.execText(\n \"workspace\",\n {\n ids: workspaceOptions.ids ?? true,\n },\n execOptions,\n );\n return parseWorkspace(output);\n },\n });\n\n attachClientInternals(client, restoreManager);\n\n return client;\n}\n\nfunction parseCommandIds(output: string): string[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => line.split(\"\\t\", 1)[0]?.trim() ?? \"\")\n .filter(Boolean);\n}\n\nfunction parseDevEvalOutput<T>(output: string): T {\n const normalized = output.startsWith(\"=> \") ? output.slice(3) : output;\n\n try {\n return JSON.parse(normalized) as T;\n } catch {\n return normalized as T;\n }\n}\n\nfunction parseTabs(output: string): WorkspaceTab[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map(parseTabLine);\n}\n\nfunction parseTabLine(line: string): WorkspaceTab {\n const [descriptor, id] = line.split(\"\\t\");\n const match = descriptor?.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (!match) {\n return {\n id: id?.trim() || undefined,\n title: descriptor?.trim() ?? \"\",\n viewType: \"unknown\",\n };\n }\n\n return {\n id: id?.trim() || undefined,\n title: match[2]!,\n viewType: match[1]!,\n };\n}\n\nfunction parseWorkspace(output: string): WorkspaceNode[] {\n const roots: WorkspaceNode[] = [];\n const stack: Array<{ depth: number; node: WorkspaceNode }> = [];\n\n for (const rawLine of output.split(/\\r?\\n/u)) {\n if (!rawLine.trim()) {\n continue;\n }\n\n const depth = getWorkspaceDepth(rawLine);\n const node = parseWorkspaceNode(rawLine);\n\n while (stack.length > 0 && stack.at(-1)!.depth >= depth) {\n stack.pop();\n }\n\n const parent = stack.at(-1)?.node;\n\n if (parent) {\n parent.children.push(node);\n } else {\n roots.push(node);\n }\n\n stack.push({ depth, node });\n }\n\n return roots;\n}\n\nfunction getWorkspaceDepth(line: string): number {\n let depth = 0;\n let remainder = line;\n\n while (true) {\n if (\n remainder.startsWith(\"│ \") ||\n remainder.startsWith(\" \") ||\n remainder.startsWith(\"├── \") ||\n remainder.startsWith(\"└── \")\n ) {\n depth += 1;\n remainder = remainder.slice(4);\n continue;\n }\n\n return depth;\n }\n}\n\nfunction parseWorkspaceNode(line: string): WorkspaceNode {\n let withoutTree = line;\n\n while (true) {\n if (\n withoutTree.startsWith(\"│ \") ||\n withoutTree.startsWith(\" \") ||\n withoutTree.startsWith(\"├── \") ||\n withoutTree.startsWith(\"└── \")\n ) {\n withoutTree = withoutTree.slice(4);\n continue;\n }\n\n break;\n }\n\n withoutTree = withoutTree.trim();\n const idMatch = withoutTree.match(/^(.*?)(?: \\(([a-z0-9]+)\\))?$/iu);\n const content = idMatch?.[1]?.trim() ?? withoutTree;\n const id = idMatch?.[2];\n const leafMatch = content.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (leafMatch) {\n return {\n children: [],\n id,\n label: leafMatch[2]!,\n title: leafMatch[2]!,\n viewType: leafMatch[1]!,\n };\n }\n\n return {\n children: [],\n id,\n label: content,\n };\n}\n","import { mkdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createHash, randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient } from \"../core/types\";\nimport type { SharedVaultLockOptions } from \"./types\";\n\nconst DEFAULT_HEARTBEAT_MS = 2_000;\nconst DEFAULT_STALE_MS = 15_000;\nconst DEFAULT_TIMEOUT_MS = 60_000;\nconst DEFAULT_WAIT_INTERVAL_MS = 500;\nconst DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), \"obsidian-e2e-locks\");\nconst LOCK_METADATA_FILE = \"lock.json\";\nconst APP_LOCK_KEY = \"__obsidianE2ELock\";\nconst heldLocks = new Map<string, HeldVaultRunLock>();\n\nexport interface VaultRunLockMetadata {\n acquiredAt: number;\n cwd: string;\n heartbeatAt: number;\n hostname: string;\n ownerId: string;\n pid: number;\n staleMs: number;\n vaultName: string;\n vaultPath: string;\n}\n\nexport interface VaultRunLock {\n readonly lockDir: string;\n readonly metadata: VaultRunLockMetadata;\n\n publishMarker(obsidian: ObsidianClient): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface VaultRunLockState {\n heartbeatAgeMs: number;\n isStale: boolean;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n}\n\nexport interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {\n vaultName: string;\n vaultPath: string;\n}\n\ninterface HeldVaultRunLock {\n heartbeat: NodeJS.Timeout;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n metadataPath: string;\n refs: number;\n}\n\nexport async function acquireVaultRunLock({\n heartbeatMs = DEFAULT_HEARTBEAT_MS,\n lockRoot = DEFAULT_LOCK_ROOT,\n onBusy = \"wait\",\n staleMs = DEFAULT_STALE_MS,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n vaultName,\n vaultPath,\n}: AcquireVaultRunLockOptions): Promise<VaultRunLock> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const heldLock = heldLocks.get(lockDir);\n\n if (heldLock) {\n heldLock.refs += 1;\n return createVaultRunLockHandle(heldLock);\n }\n\n const ownerId = randomUUID();\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n const metadata: VaultRunLockMetadata = {\n acquiredAt: Date.now(),\n cwd: process.cwd(),\n heartbeatAt: Date.now(),\n hostname: os.hostname(),\n ownerId,\n pid: process.pid,\n staleMs,\n vaultName,\n vaultPath,\n };\n\n await mkdir(lockRoot, { recursive: true });\n\n const startedAt = Date.now();\n\n while (true) {\n try {\n await mkdir(lockDir);\n await writeMetadata(metadataPath, metadata);\n break;\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const currentLock = await inspectVaultRunLock({\n lockRoot,\n staleMs,\n vaultPath,\n });\n\n if (currentLock && !currentLock.isStale) {\n if (onBusy === \"fail\") {\n throw new Error(formatBusyLockMessage(vaultPath, currentLock));\n }\n } else {\n await rm(lockDir, { force: true, recursive: true });\n continue;\n }\n\n if (Date.now() - startedAt >= timeoutMs) {\n throw new Error(\n currentLock\n ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}`\n : `Timed out waiting for shared vault lock on ${vaultPath}`,\n );\n }\n\n await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));\n }\n }\n\n const heartbeat = setInterval(() => {\n metadata.heartbeatAt = Date.now();\n void writeMetadata(metadataPath, metadata).catch(() => {});\n }, heartbeatMs);\n heartbeat.unref();\n\n const nextHeldLock: HeldVaultRunLock = {\n heartbeat,\n lockDir,\n metadata,\n metadataPath,\n refs: 1,\n };\n\n heldLocks.set(lockDir, nextHeldLock);\n return createVaultRunLockHandle(nextHeldLock);\n}\n\nexport async function clearVaultRunLockMarker(obsidian: ObsidianClient): Promise<void> {\n await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; \"cleared\"`, {\n allowNonZeroExit: true,\n });\n}\n\nexport async function inspectVaultRunLock({\n lockRoot = DEFAULT_LOCK_ROOT,\n staleMs = DEFAULT_STALE_MS,\n vaultPath,\n}: Pick<\n AcquireVaultRunLockOptions,\n \"lockRoot\" | \"staleMs\" | \"vaultPath\"\n>): Promise<VaultRunLockState | null> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const metadata = await readLockState(lockDir);\n\n if (!metadata) {\n return null;\n }\n\n return {\n heartbeatAgeMs: Date.now() - metadata.heartbeatAt,\n isStale: isLockStale(metadata, staleMs),\n lockDir,\n metadata,\n };\n}\n\nexport async function readVaultRunLockMarker(\n obsidian: ObsidianClient,\n): Promise<VaultRunLockMetadata | null> {\n return obsidian.dev.eval<VaultRunLockMetadata | null>(\n `window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`,\n { allowNonZeroExit: true },\n );\n}\n\nfunction createVaultLockKey(vaultPath: string): string {\n return createHash(\"sha256\").update(path.resolve(vaultPath)).digest(\"hex\");\n}\n\nfunction buildSetMarkerCode(metadata: VaultRunLockMetadata): string {\n const encodedMetadata = JSON.stringify(metadata);\n return `(() => {\n const lock = ${encodedMetadata};\n window.${APP_LOCK_KEY} = lock;\n app.${APP_LOCK_KEY} = lock;\n return lock;\n })()`;\n}\n\nfunction createVaultRunLockHandle(heldLock: HeldVaultRunLock): VaultRunLock {\n return {\n get lockDir() {\n return heldLock.lockDir;\n },\n get metadata() {\n return heldLock.metadata;\n },\n async publishMarker(obsidian: ObsidianClient) {\n await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));\n },\n async release() {\n if (heldLock.refs > 1) {\n heldLock.refs -= 1;\n return;\n }\n\n heldLocks.delete(heldLock.lockDir);\n clearInterval(heldLock.heartbeat);\n\n const currentLock = await readLockState(heldLock.lockDir);\n\n if (currentLock?.ownerId !== heldLock.metadata.ownerId) {\n return;\n }\n\n await rm(heldLock.lockDir, { force: true, recursive: true });\n },\n };\n}\n\nasync function readLockState(lockDir: string): Promise<VaultRunLockMetadata | null> {\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n\n try {\n return JSON.parse(await readFile(metadataPath, \"utf8\")) as VaultRunLockMetadata;\n } catch {\n try {\n const directoryStat = await stat(lockDir);\n return {\n acquiredAt: directoryStat.mtimeMs,\n cwd: \"\",\n heartbeatAt: directoryStat.mtimeMs,\n hostname: \"\",\n ownerId: \"\",\n pid: 0,\n staleMs: DEFAULT_STALE_MS,\n vaultName: \"\",\n vaultPath: \"\",\n };\n } catch {\n return null;\n }\n }\n}\n\nfunction formatBusyLockMessage(vaultPath: string, state: VaultRunLockState): string {\n const ownerDetails = state.metadata.ownerId\n ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || \"<unknown>\"}`\n : \"owner=<unknown>\";\n const ageDetails = `heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`;\n\n return `vault ${vaultPath} is locked by ${ownerDetails} ${ageDetails}`;\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isLockStale(metadata: VaultRunLockMetadata, staleMs: number): boolean {\n return Date.now() - metadata.heartbeatAt > staleMs;\n}\n\nasync function sleep(durationMs: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, durationMs));\n}\n\nasync function writeMetadata(metadataPath: string, metadata: VaultRunLockMetadata): Promise<void> {\n await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\\n`, \"utf8\");\n}\n","import { access, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { posix as pathPosix } from \"node:path\";\n\nimport type { DeleteOptions, JsonFile, ObsidianClient, VaultApi } from \"../core/types\";\n\ninterface CreateVaultApiOptions {\n obsidian: ObsidianClient;\n root?: string;\n}\n\nexport function createVaultApi(options: CreateVaultApiOptions): VaultApi {\n const scopeRoot = normalizeScope(options.root);\n\n return {\n async delete(targetPath, deleteOptions: DeleteOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await rm(resolvedPath, {\n force: true,\n recursive: true,\n });\n\n if (deleteOptions.permanent === false) {\n return;\n }\n },\n async exists(targetPath) {\n try {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await access(resolvedPath);\n return true;\n } catch {\n return false;\n }\n },\n json<T = unknown>(targetPath: string) {\n const jsonFile: JsonFile<T> = {\n async patch(updater) {\n const currentValue = await jsonFile.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await jsonFile.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n const rawValue = await readFile(resolvedPath, \"utf8\");\n return JSON.parse(rawValue) as T;\n },\n async write(value) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n\n return jsonFile;\n },\n async mkdir(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(resolvedPath, { recursive: true });\n },\n async read(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n return readFile(resolvedPath, \"utf8\");\n },\n async waitForExists(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? true : false), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to exist`,\n ...waitOptions,\n });\n },\n async waitForMissing(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? false : true), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to be removed`,\n ...waitOptions,\n });\n },\n async write(targetPath, content) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, content, \"utf8\");\n },\n };\n}\n\nfunction normalizeScope(scope?: string): string {\n if (!scope || scope === \".\") {\n return \"\";\n }\n\n return scope.replace(/^\\/+|\\/+$/g, \"\");\n}\n\nfunction resolveVaultPath(scopeRoot: string, targetPath: string): string {\n if (!targetPath || targetPath === \".\") {\n return scopeRoot;\n }\n\n return scopeRoot ? pathPosix.join(scopeRoot, targetPath) : pathPosix.normalize(targetPath);\n}\n\nasync function resolveFilesystemPath(\n obsidian: ObsidianClient,\n scopeRoot: string,\n targetPath: string,\n): Promise<string> {\n const vaultPath = await obsidian.vaultPath();\n const scopedPath = resolveVaultPath(scopeRoot, targetPath);\n const relativePath = scopedPath.split(\"/\").filter(Boolean);\n const resolvedPath = path.resolve(vaultPath, ...relativePath);\n const normalizedVaultPath = path.resolve(vaultPath);\n\n if (\n resolvedPath !== normalizedVaultPath &&\n !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)\n ) {\n throw new Error(`Resolved path escapes the vault root: ${targetPath}`);\n }\n\n return resolvedPath;\n}\n","import { posix as pathPosix } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient, SandboxApi } from \"../core/types\";\nimport { createVaultApi } from \"./vault\";\n\ninterface CreateSandboxApiOptions {\n obsidian: ObsidianClient;\n sandboxRoot: string;\n testName: string;\n}\n\nexport async function createSandboxApi(options: CreateSandboxApiOptions): Promise<SandboxApi> {\n const root = pathPosix.join(\n options.sandboxRoot,\n `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`,\n );\n const vault = createVaultApi({\n obsidian: options.obsidian,\n root,\n });\n\n await vault.mkdir(\".\");\n\n return {\n ...vault,\n async cleanup() {\n await vault.delete(\".\", { permanent: true });\n },\n path(...segments: string[]) {\n return pathPosix.join(root, ...segments);\n },\n root,\n };\n}\n\nfunction sanitizeSegment(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 80) || \"test\"\n );\n}\n"],"mappings":"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"}