Skip to content

vault/app-config

Node-side read/write/restore seam for one of Obsidian’s vault config keys, for the few tests that need a different value than the harness’s headless default.

ensureHeadlessVaultConfig writes alwaysUpdateLinks: true and settingsPopoutWindow: false into every vault the harness provisions, before that vault is ever opened, and neither is a knob a consumer configures — see the project AGENTS.md (L48). That is right for almost every test, and wrong for the few whose subject is the second Electron window the popout creates: a test that waits on activeWindow !== window, or reads app.setting.popout, never observes the thing it asserts and simply times out.

Those tests opt back in through here rather than by hand, which buys three things over an inline setConfig inside an evalInObsidian closure:

  • The value is restored, including deleting a key that had never been written. An inline setter writes into a vault shared with the rest of the run, so every later test in that instance inherits the change — the cross-test contamination the unconditional default exists to end. - The key narrowing lives in one place. The key arrives as a plain string and Obsidian’s getConfig / setConfig take obsidian-typings’ ConfigItem, so it is narrowed here once rather than at every consumer’s inline setter. - Per-test granularity. A parameter on the provisioning write would be per-vault, and a project whose tests share one vault — the usual shape — cannot have it both ways.

Everything here runs on the Node side. An evalInObsidian callback is serialized into the driven Obsidian and can import nothing, so a helper callable from inside one is not expressible; each function below is its own short eval instead.

Why presence is read off app.vault.config, not from getConfig

Section titled “Why presence is read off app.vault.config, not from getConfig”

Both facts were verified in the shipped 1.14.1 bundle rather than assumed:

  • Vault.setConfig(key, value) deletes the key when value is undefined, and does nothing at all when the value is unchanged (no save, no config-changed event). So an exact restore of a key that was never written is expressible. - Vault.getConfig(key) falls back to Obsidian’s defaults table when the key is absent, so it cannot tell unset from set to the shipped default — restoring from it would leave the key written where it had not been. app.vault.config holds only the keys changed from their default, so Object.hasOwn on it is the signal a restore needs.
Interface Description
AppConfigRestore What setAppConfig captured before it wrote, and all restoreAppConfig needs to put the key back exactly as it was — including putting it back to absent.
GetAppConfigParams Parameters for getAppConfig, and the shared shape of the writers below.
SetAppConfigParams Parameters for setAppConfig.
WithAppConfigParams Parameters for withAppConfig.
Function Description
getAppConfig Reads a vault config key, resolving the transport and vault from the current test context. This is Obsidian’s own Vault.getConfig, so an absent key reads back as Obsidian’s default for it rather than as undefined — see this file’s header. Use setAppConfig’s AppConfigRestore.isPresent when the difference matters.
restoreAppConfig Puts a key back exactly as setAppConfig found it — writing the captured value back, or deleting the key when the vault had never carried it. The beforeAll / afterAll half of the seam. A test that scopes the change to one callback wants withAppConfig instead, which cannot forget the restore.
setAppConfig Writes a vault config key, capturing what it held first so the write can be undone exactly. Obsidian persists the change (requestSaveConfig) and fires its config-changed event, both of which the app’s own settings UI does too — so anything listening reacts as it would to a user flipping the toggle. A write of the value the key already holds is a no-op in Obsidian itself, and the returned token still describes the state that preceded it.
withAppConfig Runs work with a vault config key temporarily set, and puts the key back afterwards — on a throw as much as on a return. The shape a popout-subject test wants: ts await withAppConfig(\{ async callback() \{ await evalInObsidian(\{ ... \}); // the settings window is a popout in here \}, configKey: 'settingsPopoutWindow', value: true, vaultPath \}); The callback runs on the Node side, so it is free to make several evals, take a screenshot, or assert between them.