Skip to content

[Chore]: code hygiene, dead-code removal, and expanded test coverage #22

Description

@brianckeegan

What problem are you trying to solve?

Type: chore
Affected files: src/modules/convert.ts, src/modules/hooks.ts, src/utils/frontmatter.ts, src/utils/window.ts, src/utils/prefs.ts, test/, .env.example, package.json, .github/workflows/ci.yml

Summary

A bundle of small hygiene improvements that surfaced during a code review. Each individual item is too small to warrant its own issue but together they tighten the codebase meaningfully. The bundle is organized into three sub-tasks; a single PR addressing all three is reasonable, or they can be picked off independently.

Sub-task 1: Remove dead code

src/utils/frontmatter.ts exports stripExistingFrontmatter but no caller imports it. Either remove the export, or wire it in at the natural integration point — step 7 of convertAttachmentInner in convert.ts, just before prepending the new frontmatter — to handle the (unusual but not impossible) case where docling-serve returns markdown that already begins with a YAML block. Removal is the smaller change; wiring it in is the more conservative choice. Pick one.

src/utils/window.ts exports isWindowAlive but no caller imports it. Remove the file unless a planned consumer exists in the near-term roadmap.

src/hooks.ts defines an empty onNotify function labeled "Phase 2: auto-convert notifier lands here." The actual notifier registration lives in src/modules/notifier.ts and goes through Zotero.Notifier.registerObserver. The empty onNotify is leftover scaffold and will confuse contributors looking for the notifier logic. Remove it from hooks.ts and from the default export object.

The inFlightItems dedupe in src/modules/convert.ts is somewhat redundant with the batch-level batchInFlight flag in addon.data, since the latter prevents a second batch from starting at all. Keep inFlightItems (it provides defence-in-depth against callers from new code paths) but add a JSDoc comment explaining why both exist.

Sub-task 2: Small validations and polish

Server URL validation. The current code in testServerConnection only strips trailing slashes. If a user pastes localhost:5001 (missing scheme) or http://localhost:5001/api (extra path), the resulting fetch calls fail with confusing errors. Validate the URL via the URL constructor inside testServerConnection and surface a concrete error message if it's not a valid HTTP(S) URL with no path component. Block save in the preferences pane on invalid input with a red-bordered field, matching the existing pattern for the OCR languages field.

Surface conversion duration in the toast. docling-serve returns processing_time in its response and the code logs it but doesn't display it. For a batch completion toast, format as "OK 28 · skipped 2 · failed 0 — took 4m 12s". The sum of processing_time across all items in the batch is a reasonable proxy. Aggregating happens in runBatch in menu.ts and the parallel path in notifier.ts.

.env.example formatting. Switch KEY = value (spaces around =) to KEY=value. dotenv tolerates the current form but some parsers don't.

Node version pinning. Either add "engines": { "node": ">=20" } to package.json or commit a .nvmrc with 20. Document in CONTRIBUTING.md that Node 20+ is required. The CI workflow already uses setup-js@main which pins implicitly; explicit pinning helps local contributors.

Typed pref accessor. Number(getPref(...)) patterns appear in several places with ?? default fallbacks. Consider a small wrapper getPrefAs<T>(key, default) in src/utils/prefs.ts that handles the coercion and default in one call site, reducing the surface area for the Zotero.Prefs.get "undefined during initial install" edge case.

Sub-task 3: Expand test coverage

The current test/ directory has one smoke test confirming the plugin instance is defined. The codebase has several pure functions that are good candidates for unit testing without requiring a live Zotero environment.

buildConvertForm (src/modules/convert.ts). Inputs are bytes, filename, and the Web APIs object. Outputs are form fields. Good test cases:

  • Default preferences produce the expected base set of fields.
  • A valid Advanced JSON payload correctly overrides base fields (the JSON merge is documented to win).
  • An invalid Advanced JSON payload throws a typed error.
  • ocrLang with multiple comma-separated values produces multiple ocr_lang form fields, not a single comma-joined one.
  • A nested object value in Advanced JSON is JSON-stringified, not flattened.

buildFrontmatter (src/utils/frontmatter.ts). Inputs are a mocked Zotero.Item. Outputs are YAML text. Good test cases:

  • A title containing a double quote is correctly escaped.
  • A title containing a backslash is correctly escaped.
  • Multiple authors are emitted as a YAML inline list.
  • A creator with only lastName produces a single-name entry, not lastName, undefined.
  • An item with no metadata returns an empty string, not a stub ---\n---\n.
  • A date field of "April 2024" extracts year: 2024.
  • An empty parent returns an empty string.

stripExistingFrontmatter (src/utils/frontmatter.ts). If the function is kept (per sub-task 1):

  • A markdown starting with ---\nfoo: bar\n---\nbody returns body.
  • A markdown not starting with --- is returned unchanged.
  • A markdown with --- but no closing --- is returned unchanged.

truncateMiddle (src/utils/format.ts). Edge cases:

  • A string shorter than max is returned unchanged.
  • max ≤ 3 returns the first max characters with no ellipsis.
  • A balanced truncation produces head and tail of roughly equal length.

These are all plain Mocha tests that can run via the existing npm run test infrastructure. They need only minimal mocks (a FormData/Blob polyfill from Node's built-in undici, and a small stub for the Zotero.Item shape used by buildFrontmatter). The result is a meaningful regression net for any future changes to the form-builder logic.

Acceptance criteria

Sub-task 1.

  • stripExistingFrontmatter is either removed or has at least one caller.
  • isWindowAlive is either removed or has at least one caller.
  • The empty onNotify in hooks.ts is removed from both the implementation and the default export.
  • inFlightItems retains a JSDoc comment explaining its relationship to batchInFlight.

Sub-task 2.

  • Pasting localhost:5001 (missing scheme) into the server URL field produces an actionable error.
  • Batch-completion toasts include total processing time when at least one item completed successfully.
  • .env.example uses KEY=value (no spaces).
  • package.json declares a Node version requirement.

Sub-task 3.

  • At least the four test files listed above exist in test/ with the test cases described.
  • npm run test passes locally and in CI.
  • The CI workflow is updated if needed to install any new test-only dev dependencies.

Notes for the implementer

These sub-tasks are independently mergeable. If splitting into separate PRs, ordering would be (1) dead-code removal first (no behaviour change, easy review), (2) test coverage second (locks in current behaviour before any refactor), (3) small validations last (now backed by tests).

Proposed solution (optional)

No response

Alternatives considered

No response

How much would this affect your workflow?

None

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions