Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ jobs:
e2e-relevant:
- 'src/services/**'
- 'src/logic/sync-manager.ts'
- 'src/logic/sync/**'
- 'src/logic/source-control/**'
- 'src/utils/git-blob-sha.ts'
- 'src/utils/path.ts'
- 'src/utils/symlink.ts'
- 'e2e/**'
- 'e2e-tests/**'
- 'vitest.e2e.config.ts'
- 'scripts/e2e-harness.sh'
- 'scripts/e2e-namespace.sh'
- 'scripts/e2e-namespace-cleanup.sh'
- 'scripts/e2e-suites.txt'
- 'scripts/run-e2e.sh'
- 'package.json'
- 'package-lock.json'
Expand Down
53 changes: 38 additions & 15 deletions docs/obsidian-scanner-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ after the normal 1.5.7 release workflow completes.
## Phase 1 re-audit (Shell/Git E2E harness)

Real-provider E2E returned in `e2e/**` (test/real-provider-e2e), rebuilt so none of the
previously-flagged APIs are used in any committed `.ts` file, regardless of directory —
`scripts/e2e-harness.sh` (Shell, not TypeScript) now owns branch/container lifecycle and git
authentication, and everything Node-only the suites still need at runtime (the real `requestUrl`
shim, the `window` timer alias, a git-CLI-backed verifier) is generated by that script into
`$E2E_RUNTIME_DIR` per run, never committed. See `docs/testing/real-provider-e2e.md`.
previously-flagged APIs are used in any committed `.ts` file — `scripts/e2e-harness.sh` (Shell,
not TypeScript) owned branch/container lifecycle and git authentication, and everything Node-only
the suites needed at runtime (the real `requestUrl` shim, the `window` timer alias, a
git-CLI-backed verifier) was generated by that script into `$E2E_RUNTIME_DIR` per run, never
committed.

Same grep-based method as the baseline above, re-run against the current tree:
Same grep-based method as the baseline above, re-run against that tree:

| Check | Result |
| --- | --- |
Expand All @@ -56,13 +56,36 @@ Same grep-based method as the baseline above, re-run against the current tree:
| Bare `setTimeout`/`setInterval` (not `window.*`) in `e2e/**`/`src/**` | None |
| Unnecessary `as string` assertions in `e2e/config/env.ts` | Fixed — replaced with `requiredEnv()`, which throws instead of asserting |

`e2e/**/*.ts` is back in `tsconfig.json`'s `include` and in `eslint.config.mts`'s scope
(`npx eslint .` — 0 errors; `tsc -noEmit -skipLibCheck` — clean), since neither tool needs the
harness to have run first: the only imports of generated (not-yet-existing-at-typecheck-time)
files are runtime-computed dynamic `import()` calls, which `tsc` doesn't attempt to statically
resolve.
## Phase 2 re-audit (e2e-tests/ boundary, static runtime files)

The actual official scanner rescan against this harness is still outstanding from this checkout
(no access to the submission tooling here) — this section is the best available self-check in
the meantime, per the task's own acknowledgment that the real validation is a separate,
later step.
The harness moved to `e2e-tests/provider/**`, and the previously-generated runtime files
(`obsidian-request-url.ts`, `window-timers.ts`, `git-verifier.ts`) are now **committed** static
`.ts` files under `e2e-tests/provider/runtime/` and `e2e-tests/provider/support/`, on the premise
that the scanner's flagging is scoped to a submission's declared plugin surface
(`manifest.json`/`main.js`), not a blanket repo-wide grep.

**This premise is unverified.** Phase 1's own removal was prompted by the baseline finding above,
which recorded these exact APIs being flagged while committed under `e2e/**` — a differently-named
directory, not a different scoping mechanism. Re-committing them under `e2e-tests/**` instead of
`e2e/**` changes the directory name but not, as far as this repo's own audit trail shows, the
thing the scanner actually keys on. No official rescan has been run against this change from this
checkout to confirm or refute that.

Grep-based self-check against the current tree (same method as before, informational only — it
was already passing under the generated-runtime design too, so it does not distinguish the two):

| Check | Result |
| --- | --- |
| `fetch(` in `src/**` | None |
| `globalThis` in `src/**` | None |
| `node:crypto`/`node:child_process`/`node:util` in `src/**` | None |
| `fetch`/`globalThis`/`node:child_process` in `e2e-tests/**` | Present (by design — see above) |

`e2e-tests/**/*.ts` is in `tsconfig.json`'s `include` and in `eslint.config.mts`'s scope
(`npx eslint .` — 0 errors; `tsc -noEmit -skipLibCheck` — clean); unlike Phase 1, these are real
committed files, not ambient-module stand-ins for a generated target.

**Follow-up required**: get an actual official scanner rescan against this directory structure
before relying on it. If it reproduces the Phase 1 finding, revert to per-run generation into an
uncommitted directory (git history has the Phase 1 implementation) rather than trying a third
directory name.
6 changes: 4 additions & 2 deletions docs/test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

All tests are in `tests/` and run with `npm run test` (Vitest).

## Temporary E2E status
## Real-provider E2E

Real-provider E2E source has been temporarily removed from the plugin repository because the Obsidian official scanner treats Node-only E2E tooling as plugin source. The long-term E2E architecture is being evaluated separately.
Real-provider E2E (GitHub/GitLab/Gitea, against a real Git server) lives under
`e2e-tests/provider/`, separate from the unit tests in `tests/`. See
`docs/testing/real-provider-e2e.md` and `docs/obsidian-scanner-audit.md`.

---

Expand Down
92 changes: 57 additions & 35 deletions docs/testing/real-provider-e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,31 @@ GitHub Actions

This replaced an earlier Node-based harness (`e2e/provision`, `e2e/verifier`, `e2e/providers`,
`e2e/shim`, `scripts/run-e2e*.mjs`) that used `fetch`/`node:child_process`/`node:crypto` directly
in committed `.ts` files. The Obsidian community-plugin scanner flags those APIs wherever they
appear in the repo, regardless of directory — it doesn't matter that E2E code never ships in
`main.js`. See `docs/obsidian-scanner-audit.md`.

**The fix isn't "move it to a differently-named folder"** — it's that no committed `.ts` file
uses those APIs at all:

- `scripts/e2e-harness.sh` (Shell, not TypeScript) owns branch/container lifecycle: creating the
isolated test branch via plain `git push <sha>:refs/heads/<branch>` (no REST branch-creation
calls except the one GitLab numeric-project-ID resolution git genuinely can't do), and the
Gitea Docker container lifecycle via the `docker` CLI directly — never
`node:child_process`.
- Everything Node-only that the suites still need at runtime (the real `requestUrl` shim
production services import from `obsidian`, the `window.setTimeout` alias, and a small
git-CLI-backed verifier) is **generated fresh per run** by `scripts/e2e-harness.sh provision`
into `$E2E_RUNTIME_DIR`, not committed. Suites import these statically — `obsidian` and
`@e2e-runtime/git-verifier` are both resolved via `vitest.e2e.config.ts`'s `alias` map to
`$E2E_RUNTIME_DIR` at E2E runtime only. `e2e/runtime-modules.d.ts` gives
`@e2e-runtime/git-verifier` a compile-time shape (backed by `e2e/verifier-runtime-types.ts`)
so `npm run build`'s typecheck never needs the generated files to exist — no committed
suite ever does a runtime-computed dynamic `import()`, so there's nothing scanner-visible
for the Obsidian security lint rules to flag.
in committed `.ts` files. The Obsidian community-plugin scanner flagged those APIs in that
harness's committed files. See `docs/obsidian-scanner-audit.md`.

`scripts/e2e-harness.sh` (Shell, not TypeScript) owns branch/container lifecycle: creating the
isolated test branch via plain `git push <sha>:refs/heads/<branch>` (no REST branch-creation
calls except the one GitLab numeric-project-ID resolution git genuinely can't do), and the
Gitea Docker container lifecycle via the `docker` CLI directly — never `node:child_process`.

Everything Node-only the suites need at runtime (the real `requestUrl` shim production services
import from `obsidian`, the `window.setTimeout` alias, and a small git-CLI-backed verifier) now
lives as **committed, static** TypeScript under `e2e-tests/provider/runtime/` and
`e2e-tests/provider/support/git-verifier.ts`, scoped under the `e2e-tests/` directory rather than
generated per-run into a temp dir. `vitest.e2e.config.ts`'s `alias` map points `obsidian` at the
committed `obsidian-request-url.ts`; suites import `GitVerifier` directly by relative path — no
`@e2e-runtime/*` ambient module, no `E2E_RUNTIME_DIR`.

**Open scanner-risk caveat:** the previous harness generation was removed specifically because a
prior committed-`.ts` version of this same code was flagged by the Obsidian scanner, and this
repo's own audit (`docs/obsidian-scanner-audit.md`) recorded that the scanner's flagging did not
appear to be scoped to what a submission actually bundles into `main.js`. Re-committing these
files under `e2e-tests/` on the premise that the scanner only inspects `manifest.json`'s declared
plugin surface is **unverified** against the real scanner as of this change — see "Known gaps"
below. If a rescan reproduces the earlier finding, the fallback is reverting to per-run generation
into an uncommitted directory (the previous design, preserved in git history), not a further
directory rename.

## Layout

Expand All @@ -62,17 +65,24 @@ uses those APIs at all:
- `scripts/run-e2e.sh` — the shared local/CI entry point: provision → seed → vitest → cleanup.
It allocates a unique temporary workdir when the caller does not supply one, so concurrent local
runs cannot overwrite each other's repository, runtime adapters, or credentials.
- `e2e/config/env.ts` — reads the env vars `provision` resolved and constructs the real,
already-configured `GitServiceInterface` per provider (`githubContext`/`gitlabContext`/
`giteaContext`).
- `e2e/verifier-runtime-types.ts` — type-only `GitVerifier` contract the generated git-CLI
verifier implements.
- `e2e/shim/fake-vault.ts` — real in-memory Obsidian Vault/App stand-in (not a `vi.fn()` mock);
the only thing faked, since the point of this harness is exercising real `SyncManager` +
real provider code against a real Git server.
- `e2e/suites/{github,gitlab,gitea}.e2e.test.ts` — provider contract suites (create/read/
update/delete/batch/rename, plus provider-specific regressions).
- `e2e/suites/sync-manager.e2e.test.ts` — one suite, parametrized by `E2E_PROVIDER`, covering
- `e2e-tests/provider/config/env.ts` — reads the env vars `provision` resolved and constructs
the real, already-configured `GitServiceInterface` per provider (`githubContext`/
`gitlabContext`/`giteaContext`).
- `e2e-tests/provider/runtime/obsidian-request-url.ts` — the real `requestUrl` shim (and the
minimal Obsidian class stand-ins production code touches), aliased in for `obsidian` by
`vitest.e2e.config.ts`.
- `e2e-tests/provider/runtime/window-timers.ts` — the `window` = `globalThis` alias, loaded via
`vitest.e2e.config.ts`'s `setupFiles`.
- `e2e-tests/provider/support/git-verifier.ts` — the git-CLI-backed `GitVerifier` every suite
imports directly; reads the clone path from `E2E_WORKDIR` at call time rather than a baked-in
constant.
- `e2e-tests/provider/shim/fake-vault.ts` — real in-memory Obsidian Vault/App stand-in (not a
`vi.fn()` mock); the only thing faked, since the point of this harness is exercising real
`SyncManager` + real provider code against a real Git server.
- `e2e-tests/provider/suites/{github,gitlab,gitea}.e2e.test.ts` — provider contract suites
(create/read/update/delete/batch/rename, plus provider-specific regressions).
- `e2e-tests/provider/suites/sync-manager.e2e.test.ts` — one suite, parametrized by
`E2E_PROVIDER`, covering
`SyncManager.pushFiles`/`pullFile`/`trackRename`/`clearMetadata` against a real provider.

## Isolation model
Expand Down Expand Up @@ -238,8 +248,9 @@ The E2E jobs are separated by trust boundary. Gitea runs on a fresh GitHub-hoste
`contents: read`; it is safe for fork PRs because it receives no repository secrets and cannot
access the persistent runner fleet. The credentialed `github`/`gitlab` matrix remains self-hosted,
and its job-level condition rejects fork PRs before runner allocation. Both depend on the
`changes` job's path gate (`src/services/**`,
`src/logic/sync-manager.ts`, `e2e/**`, `scripts/e2e-harness.sh`, `scripts/e2e-namespace.sh`, etc. —
`changes` job's path gate (`src/services/**`, `src/logic/sync-manager.ts`, `src/logic/sync/**`,
`src/logic/source-control/**`, `e2e-tests/**`, `vitest.e2e.config.ts`, `scripts/e2e-suites.txt`,
`scripts/e2e-harness.sh`, `scripts/e2e-namespace.sh`, etc. —
computed by the `CI / Detect Changes` job, since GitHub Actions' own `on.*.paths` would gate the
*entire* workflow file, including the always-must-run validation/release jobs). It always runs in
full on `workflow_dispatch`, `schedule` (weekly, Monday 06:00 UTC, for API-drift detection), and
Expand Down Expand Up @@ -328,6 +339,17 @@ GitHub/GitLab checks it cannot produce.
- The official Obsidian community-plugin scanner rescan (as opposed to this repo's own
grep-based self-audit, `docs/obsidian-scanner-audit.md`) hasn't been re-run against this
harness from this checkout.
- **Committed-vs-generated risk, unresolved**: `e2e-tests/provider/runtime/` and
`e2e-tests/provider/support/git-verifier.ts` are committed `.ts` files using
`fetch`/`globalThis`/`node:child_process` — the same APIs a *prior* version of this harness
had flagged by the scanner while committed under `e2e/`. That prior removal's own audit
(`docs/obsidian-scanner-audit.md`) found the scanner's flagging was not evidently scoped to
`manifest.json`'s declared plugin surface. This PR bets that a directory outside `e2e/` (now
`e2e-tests/`) resolves that, on the premise the scanner only inspects what a submission
bundles — that premise has not been re-verified against the actual scanner. If a rescan
reproduces the earlier finding, revert to per-run generation into an uncommitted
`$E2E_WORKDIR`-scoped directory (git history has the prior implementation), not another
directory rename.
- The Phase 2 isolation model (namespace scheme, per-source/provider concurrency groups,
`e2e-pr-cleanup.yml`, `e2e-branch-cleanup.yml`, `e2e-janitor.yml`) is verified by local
unit-level exercises of `scripts/e2e-namespace.sh`/`e2e-namespace-cleanup.sh`/`e2e-janitor.sh`
Expand Down
8 changes: 4 additions & 4 deletions e2e/config/env.ts → e2e-tests/provider/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
* GitServiceInterface implementation against whatever that step already
* resolved, via the env vars it exports (see docs/testing/real-provider-e2e.md).
*/
import { GitHubService } from '../../src/services/github-service';
import { GitLabService } from '../../src/services/gitlab-service';
import { GiteaService } from '../../src/services/gitea-service';
import type { GitServiceInterface } from '../../src/services/git-service-interface';
import { GitHubService } from '../../../src/services/github-service';
import { GitLabService } from '../../../src/services/gitlab-service';
import { GiteaService } from '../../../src/services/gitea-service';
import type { GitServiceInterface } from '../../../src/services/git-service-interface';

export const SUPPORTED_PROVIDERS = ['gitea', 'gitlab', 'github'] as const;
export type E2EProvider = typeof SUPPORTED_PROVIDERS[number];
Expand Down
48 changes: 48 additions & 0 deletions e2e-tests/provider/runtime/obsidian-request-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Real `requestUrl` shim for E2E: production services import this from
// `obsidian` (see vitest.e2e.config.ts's `alias`), and E2E suites need actual
// network calls to reach the provisioned provider — the `vi.fn()` mock
// tests/setup.ts installs for unit tests is deliberately not used here.
import type { RequestUrlParam, RequestUrlResponse } from 'obsidian';

export async function requestUrl(request: RequestUrlParam | string): Promise<RequestUrlResponse> {
const params: RequestUrlParam = typeof request === 'string' ? { url: request } : request;
const shouldThrow = params.throw ?? true;
const headers: Record<string, string> = { ...params.headers };
if (params.contentType && !headers['Content-Type']) headers['Content-Type'] = params.contentType;
const res = await fetch(params.url, { method: params.method ?? 'GET', headers, body: params.body });
const arrayBuffer = await res.arrayBuffer();
const text = new TextDecoder().decode(arrayBuffer);
let json: unknown;
try { json = text ? JSON.parse(text) : undefined; } catch { json = undefined; }
const response: RequestUrlResponse = { status: res.status, headers: Object.fromEntries(res.headers.entries()), arrayBuffer, text, json };
if (shouldThrow && res.status >= 400) {
const error = new Error(`Request failed, status ${res.status}`);
(error as Error & { status: number }).status = res.status;
throw error;
}
return response;
}

export class Modal {
app: unknown;
constructor(app?: unknown) { this.app = app; }
open(): void {}

Check failure on line 29 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty method 'open'.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoZ&open=AaBV-fmlr__DB30bSqoZ&pullRequest=144
close(): void {}

Check failure on line 30 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty method 'close'.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoa&open=AaBV-fmlr__DB30bSqoa&pullRequest=144
}
export class PluginSettingTab { constructor(_app?: unknown, _plugin?: unknown) {} }

Check warning on line 32 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Useless constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoc&open=AaBV-fmlr__DB30bSqoc&pullRequest=144

Check warning on line 32 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected class with only a constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqob&open=AaBV-fmlr__DB30bSqob&pullRequest=144
export class TextComponent {}

Check warning on line 33 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty class.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqod&open=AaBV-fmlr__DB30bSqod&pullRequest=144
export class AbstractInputSuggest<_T> { constructor(_app: unknown, _inputEl: unknown) {} }

Check warning on line 34 in e2e-tests/provider/runtime/obsidian-request-url.ts

View workflow job for this annotation

GitHub Actions / CI / Lint

'_T' is defined but never used

Check warning on line 34 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Useless constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqof&open=AaBV-fmlr__DB30bSqof&pullRequest=144

Check warning on line 34 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected class with only a constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoe&open=AaBV-fmlr__DB30bSqoe&pullRequest=144

Check warning on line 34 in e2e-tests/provider/runtime/obsidian-request-url.ts

View workflow job for this annotation

GitHub Actions / CI / Lint

'_T' is defined but never used
export class TFolder { path: string; constructor(path: string) { this.path = path; } }
export class Setting { constructor(_containerEl?: unknown) {} }

Check warning on line 36 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Useless constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoh&open=AaBV-fmlr__DB30bSqoh&pullRequest=144

Check warning on line 36 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected class with only a constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqog&open=AaBV-fmlr__DB30bSqog&pullRequest=144
export class TFile {
path: string;
name: string;
constructor(path: string) { this.path = path; this.name = path.split('/').pop() ?? path; }
}
export class Notice {
constructor(_message?: string, _timeout?: number) {}

Check warning on line 43 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Useless constructor.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoi&open=AaBV-fmlr__DB30bSqoi&pullRequest=144
setMessage(): this { return this; }
hide(): void {}

Check failure on line 45 in e2e-tests/provider/runtime/obsidian-request-url.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty method 'hide'.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-fmlr__DB30bSqoj&open=AaBV-fmlr__DB30bSqoj&pullRequest=144
}
export const Platform = { isDesktopApp: false, isMobile: false };
export class FileSystemAdapter { getBasePath(): string { return '/e2e/fake-vault'; } }
5 changes: 5 additions & 0 deletions e2e-tests/provider/runtime/window-timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Minimal `window` alias so production code written for Obsidian's Electron
// renderer (e.g. window.setTimeout) runs as-is under Node.
if (typeof (globalThis as { window?: unknown }).window === 'undefined') {

Check warning on line 3 in e2e-tests/provider/runtime/window-timers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Compare with `undefined` directly instead of using `typeof`.

See more on https://sonarcloud.io/project/issues?id=firstsun-dev_git-files-sync&issues=AaBV-forr__DB30bSqok&open=AaBV-forr__DB30bSqok&pullRequest=144
(globalThis as unknown as { window: typeof globalThis }).window = globalThis;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { App } from 'obsidian';

/**
* Real in-memory Obsidian Vault/App stand-in for SyncManager E2E (see
* e2e/suites/sync-manager.e2e.test.ts) — not a `vi.fn()` mock. The point of
* e2e-tests/provider/suites/sync-manager.e2e.test.ts) — not a `vi.fn()` mock. The point of
* SyncManager E2E is to exercise real `SyncManager` + real provider service
* code against a real Git server; the *only* thing worth faking is the
* Obsidian filesystem boundary, so this implements exactly the `vault`/
Expand All @@ -11,8 +11,8 @@ import type { App } from 'obsidian';
*
* `TFile` itself has to come from the caller rather than being imported here:
* production code does `fileOrPath instanceof TFile`, so it must be the exact
* same class the vitest-runtime `obsidian` alias resolves to (generated by
* `scripts/e2e-harness.sh provision`, not committed — see
* same class the vitest `obsidian` alias resolves to
* (e2e-tests/provider/runtime/obsidian-request-url.ts — see
* docs/testing/real-provider-e2e.md), not a second, unrelated class.
*/
export interface TFileLike { path: string; name: string }
Expand Down
Loading
Loading