Skip to content

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 cast lives in one place. obsidian-typingsConfigItem union omits settingsPopoutWindow, so the key cannot be passed to getConfig / setConfig without widening them; every consumer that wrote the setter inline repeated that cast. - 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.
InterfaceDescription
AppConfigRestoreWhat setAppConfig captured before it wrote, and all restoreAppConfig needs to put the key back exactly as it was — including putting it back to absent.
GetAppConfigParamsParameters for getAppConfig, and the shared shape of the writers below.
SetAppConfigParamsParameters for setAppConfig.
WithAppConfigParamsParameters for withAppConfig.
FunctionDescription
getAppConfigReads 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.
restoreAppConfigPuts 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.
setAppConfigWrites 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.
withAppConfigRuns 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.