Commit graph

76 commits

Author SHA1 Message Date
wujunchen
2faf1729f2 Expand i18n language support
Change-Id: I31bf34852ea46947dab186a46942839db7eaaaca
2026-05-19 13:12:11 +08:00
wujunchen
68bfed7f56 perf: eliminate per-card document rescans and redundant hashing
- anchor.ts: precompute line-offset index once; O(log n) binary-search
  offsetToLine replaces two O(n) newline-count loops. findLineForAnchor
  takes an optional lineOffsets param (backward compatible).
- cards.ts / generation.ts: build the line-offset index once per document
  and thread it through all card anchor resolutions (was N cards x ~9
  full-document O(n) passes -> 1 O(n) pass + N x O(log n)).
- anchor.ts: ASCII-whitespace charCode fast path in normalizeWithMap,
  deferring to /\s/ only for non-ASCII (exact semantics preserved).
- settings.ts: memoize generationFingerprint by settings object identity
  (WeakMap) — collapses SHA-1 + stableStringify done on every file-open.
- view.ts: remove keydown listener in onClose (lifecycle leak fix).

Verified: tsc, build, biome, all 28 tests, e2e smoke, branch coverage
100% (new anchor regression tests added). Deferred (higher risk /
test-breaking): view full-render->surgical DOM, scroll rebind guard,
cache scheduleSave timing — see notes/perf-report.md.

Change-Id: I17eef347471bb8233d48164e4453d492f301eac2
2026-05-16 00:33:51 +08:00
wujunchen
052aa13547 test: cover remaining branch gaps in cache.ts and markdown.ts
Branch coverage 27/27 -> 32/32 (100%):
- touchCacheEntry(null) and default-now path
- serializeCacheFile(falsy) -> {}
- shouldConfirmRegenerate all guard branches
- cardToPlain with undefined bullets

Change-Id: I4971f8808727c63f303ebd8c23ca599673928e84
2026-05-16 00:24:33 +08:00
wujunchen
606593e39b test: add c8 coverage harness with source-map remapping
- npm run coverage runs the full suite under c8
- Wire inline source maps into the esbuild test bundles
- Redirect bundles into repo (.test-bundles) under coverage so c8
  processes them; cleaned automatically after the run
- Fix esbuild->v8-to-istanbul source path remap (tests/coverage-sourcemap.js)
- .c8rc.json, .gitignore, dev-dep c8

Note: statement/function totals are degenerate under esbuild bundling;
branch coverage + HTML report are the reliable signals.

Change-Id: I880f5ee1e9a8e5368e27a8e274825f917d972269
2026-05-16 00:22:31 +08:00
wujunchen
9473ff8434 style: apply biome formatter to cli.test.js
Change-Id: I52932009843be1d12872066bf462e01f119ab028
2026-05-14 10:19:46 +08:00
wujunchen
617cc9b668 fix: pass --verbose with stream-json for Claude CLI 2.1.131+ (#3)
Claude CLI 2.1.131+ rejects `--output-format stream-json` unless `--verbose`
is also passed. Without it the CLI exits 1 with "When using --print,
--output-format=stream-json requires --verbose", breaking the Claude Code
backend.

Adds the flag in src/cli.ts and locks both the flag and full args contract
for Claude + Codex backends with new tests:
- testClaudeCodeArgs: extended with mcp-config / disallowed-tools / chrome
  / slash-commands assertions and --verbose position check
- testClaudeCodeStreamJsonResilience: banner-mixed NDJSON, missing result,
  empty stdout, multiple result events, content fallback
- testClaudeCodeVerboseRegressionCanary: simulates real CLI stderr on
  missing --verbose and asserts error pipeline preserves exitCode + stderr
- testCodexErrorPath: codex non-zero exit propagates structured error
- testCodexArgsMinimalContract: deepStrictEqual lock on codex args 5-tuple
- testClaudeBackendSurfacesVerboseError: smoke flow surfaces the canary
- testCodexBackendPropagatesVersionFailure: version probe failure blocks
  smoke instead of silently continuing

Also wires npm run lint:obsidian into CI alongside Biome.

Closes #3

Change-Id: I6dbaa1b55172eb0b5b773bd25d1741b0603a4e4e
2026-05-14 10:15:50 +08:00
wujunchen
714b6a368b fix: harden CLI backend smoke checks
Change-Id: I7b4388095412b155c056c42661cdf481bd601e4c
2026-05-09 11:49:23 +08:00
wujunchen
04428950de chore: prepare 1.0.19 release
Change-Id: I86c01c3d6a2c13a193636ac08ba562d54a2e9a20
2026-05-09 10:48:36 +08:00
wujunchen
07faeddab5 fix Obsidian plugin review scan
Change-Id: I43416145eca8ee04468522c350fdcebeb772ab6e
2026-05-08 22:57:31 +08:00
fancivez
d738fc120a feat(error-ui): structured CliProcessError + ErrorKind-driven UI dispatch
Replaces the single long-Notice failure UX with a kind-aware dispatcher
so users get actionable buttons instead of a wall of text.

- CliProcessError extends Error with a typed CliErrorDetails payload
  (reason / pid / elapsed / idle / bytes / tails / exitCode / signal /
  timeoutMs / idleTimeoutMs). All five runCli failure paths now throw
  the typed error; message format is preserved so existing classifier
  regex and tests still match.
- classifyGenerationError short-circuits on details.reason for the
  deterministic cases (wall/idle-timeout → timeout, spawn/startup →
  config, streams-unavailable → unknown). exit-nonzero falls through
  so stderr-derived auth/rate-limit hints still classify correctly.
  Duck-typed check avoids cli ↔ generation-job-manager circular import.
- New error-ui.ts dispatches by ErrorKind:
    timeout (with details) → TimeoutDiagnosticsModal showing cmd, pid,
      elapsed, idle, bytes, redacted stderr/stdout tail, plus copy /
      open-settings / close buttons.
    timeout (no details, e.g. API streaming) → actionable Notice.
    auth/config → Notice + "Open Settings" CTA.
    rate-limit → Notice + "Copy details".
    schema → Notice + "Copy raw output" CTA.
    unknown → unchanged legacy short Notice.
- main.ts handleGenerationError delegates to dispatcher; new
  openPluginSettings() helper uses Obsidian's app.setting.openTabById.
- 21 i18n strings (zh + en) for all new UI labels and the modal copy.
- styles.css adds error notice + diagnostics modal styling.
- 7 new test assertions cover typed details on every failure path and
  classifyGenerationError respecting structured reasons.

Change-Id: I8af46d375d92ba26d14180ffbeb6c3a6dedd89f5
2026-05-05 20:19:57 +08:00
fancivez
89464608aa feat(cli): enrich CLI timeout diagnostics + idle timeout + debug logging
The CLI provider previously surfaced only "CLI timed out (Xms)" on hang,
discarding accumulated stderr/stdout, pid, and timing — leaving users
unable to tell network hangs from auth failures from a slow model.

- runCli timeout/idle errors now include pid, elapsed, idle duration,
  utf-8 byte counts, plus stderr/stdout tails (with secret redaction
  for Bearer tokens, sk-* keys, and Authorization-style headers).
- Add optional cliIdleTimeoutMs (default 0/disabled): kill the CLI if
  no output for N ms, independent of the wall-clock cliTimeoutMs.
- Add optional debugLogging: console hooks at spawn/ok/fail with pid
  and byte counts for live diagnosis.
- Settled-guard on noteActivity/armIdleTimer prevents stale timers
  after first-wins settle (cancel / wall / idle / child-error / close).
- New unit coverage: rich diagnostic suffix, idle timeout firing, idle
  reset on heartbeat, secret redaction, idle normalization edge cases.

Change-Id: I9191f8d348ef43ff9605dc5169f0951f5af85d26
2026-05-05 17:33:25 +08:00
wujunchen
33371cadd6 chore: integrate obsidianmd eslint plugin and fix recommended rules
Add eslint v9 + @typescript-eslint/parser + eslint-plugin-obsidianmd as
a dedicated `npm run lint:obsidian` track, separate from the existing
biome lint. Apply autofixes from the recommended config and resolve the
follow-on type/test issues.

- prefer-create-el: createEl('div'/'span') -> createDiv/createSpan
- prefer-active-window-timers: setTimeout/clearTimeout -> activeWindow.*
  + retype timer holders from ReturnType<typeof setTimeout> to number
- prefer-active-doc: drop globalThis.fetch in streaming
- no-useless-catch: drop no-op rethrow wrapper
- no-restricted-globals: streaming.ts is exempted because requestUrl
  does not support streaming responses
- @typescript-eslint/no-unsafe-*: disabled (out of scope for an Obsidian
  plugin lint pass; tsc + biome already cover type safety)
- tests: polyfill globalThis.activeWindow so unit tests still run in Node

Change-Id: I43cc652e29a66d490e2834e940843c39b211b80b
2026-05-03 10:45:10 +08:00
fancivez
a5d9d74b54 feat: toggle right sidebar instead of detaching tab + component tests
UX change:
- The "open Parallel Reader" command now toggles the right sidebar's
  collapsed state instead of detaching the leaf. The tab and its content
  are preserved across hide/show, matching standard Obsidian behaviour
  for File Explorer and friends. Detaching the tab on every second
  press destroyed cached cards and forced re-generation.

Behaviour:
- No leaf yet → create + reveal
- Leaf exists, right sidebar collapsed → reveal (expands sidebar)
- Leaf exists, right sidebar expanded → collapse(); tab is preserved
- No right sidebar (mobile / unusual layout) → reveal as fallback

Tests:
- New tests/view-render.test.js (18 cases) covering the component layer
  that was previously a gap. The 1.0.12 → 1.0.13 sidebar regression
  could not have been caught by any existing pure-function test; this
  file fills that gap by mocking app.workspace + view spies and running
  real runForFile / refreshViewAfterCache* / toggleParallelView.
- shouldRender matrix (4 cases): silentView × sourceFile state — the
  exact regression introduced in PR2 where guards were over-applied.
- toggleParallelView (4 cases): no-leaf opens, expanded → collapse,
  collapsed → reveal, no-rightSplit fallback.
- refreshViewAfterCacheDelete + Clear (4 cases): matching file vs
  different file vs no-view, plus clear-all always renderEmpty.
- runForFile outcome enum mapping (6 cases): already-running early
  return, already-running from catch, cancelled, generic error,
  regenerate-confirm cancellation, skipEditConfirm bypass — these
  drive batch statistics and were untested before.
- view.deleteCard / updateCard cardPersistFailed path (3 cases):
  exposes ParallelReaderView via test-exports for instantiation;
  mocks cacheReplaceCards returning false to verify the failure
  notice path lands and the public method returns false.

src/test-exports.ts: export ParallelReaderView for component tests.
tests/catalog.json: register view-render.test.js under component.

Change-Id: I439b5fca07b0928e6fe44d9025c2421c71c6cb28
2026-05-02 15:38:41 +08:00
fancivez
76d2e8050a fix: address 10 P2 behavioral and concurrency findings
Findings from the original review (P2):
- streaming: flush unterminated final SSE event at EOF (some providers
  close the stream without a trailing blank line, dropping the last delta)
- cache-manager: validate entry shape on load — drop entries where
  cards is not an array, anchor is not a string, or bullets is not an
  array. Tolerates missing optional fields (treated as cache miss).
- view: card edit/delete now check cacheReplaceCards return and surface
  failures via a localized Notice instead of pretending success
- main: clear-current / clear-all / file-menu-clear refresh open view
  via renderEmpty so stale UI does not display deleted data
- prompt + settings-tab: card count is normalized via a single helper
  used by buildPrompts and onChange. Prompt and fingerprint stay in sync.
  UI value is written back to the textbox after clamping.
- generation-job-manager: global concurrency limit (default 3) with a
  cancellable wait queue. Race-safe slot accounting via a reserved
  counter so resolved-but-not-yet-set waiters are visible to fast-path
  start() callers.
- runForFile: now returns RunForFileResult; accepts preloadedContent +
  silentView + skipEditConfirm options used by batch (and reflected on
  the PluginHost interface).
- batch: avoid double file read, do not steal UI focus, classify
  results correctly (generated / cached / already-running / empty / ...)

Follow-up from the 1.0.11 review:
- generationFingerprint: codex backend excludes settings.model from the
  hash. Codex ignores --model; previously editing model spuriously
  invalidated all codex cache.

Codex review pass:
- generation-job-manager: race fix — releaseSlot's resolve microtask
  and a synchronous start() fast-path could briefly overshoot
  maxConcurrent. Reserve the slot synchronously inside the wrapped
  resolve to close the window.
- cache-manager: anchor type validation in addition to bullets.

NOTE: Codex backend users will see a one-time "stale cache" banner on
existing notes due to the fingerprint change; regenerate to refresh.

Change-Id: I7721c7dfe51dea3f51b0215764f721523c2f6806
2026-05-02 13:52:49 +08:00
fancivez
01ec934d28 fix: address 13 review findings (security, UX, i18n)
P1:
- cli: pin codex --sandbox read-only (defeat prompt injection)
- settings: clear apiKey/apiHeaders on provider preset switch
- main/view: render error state when LLM returns empty cards
- schema/cli: do not log raw model output; UI errors expose length only

P2:
- cli: pass --model to Claude Code when settings.model set
- settings-tab: add CLI timeout input + normalizeCliTimeoutMs (min 1s)
- view: export source link uses [[path|basename]]
- view: export failures surface a localized Notice

P3:
- schema: cardUntitled fallback now respects uiLanguage
- generation-job-manager: error codes localized via main.ts
- batch: settled guard on promptForBatchFolder modal
- README: drop stale e2e validator install note

Codex review pass:
- provider-parsers: forward settings to normalizeCardsPayload
- generation-job-manager: classifyGenerationError regex covers new EN/zh
  schema-error messages
- cli: do NOT pass --model to codex (DEFAULT_SETTINGS.model is a Claude
  name and would break codex; codex relies on its own config)

Tests: cli (codex sandbox + claude --model + classify zh/en), settings
(applyApiProviderPreset isolation + normalizeCliTimeoutMs), schema
(language-aware fallback title), test-exports (new symbols).

Change-Id: I9f4c21f9299e1a9bf166f69f712859854482fe92
2026-05-02 12:50:35 +08:00
fancivez
418f733d14 ci(e2e): enforce self-contained release gate
Change-Id: I4a7385a861981223edf5b4a3f1861b349ef520bf
2026-04-30 14:45:56 +08:00
wujunchen
310c81a073 fix: add cancel action to batch prompt
Change-Id: Ic3922d55d776d369959eff4e5c811c5d3434db2b
2026-04-29 14:13:47 +08:00
wujunchen
3bdd6d67c6 fix: localize batch prompt confirmation
Change-Id: I13b546785c5984a9d0cb5abaffa843f9ad7655d0
2026-04-29 13:58:10 +08:00
wujunchen
2bf4e86451 fix: confirm before overwriting exports
Change-Id: I922fca90e46a8982686a3449b00ed793e924097b
2026-04-29 13:56:36 +08:00
wujunchen
5304ed3a48 fix: localize remaining action labels
Change-Id: I4ac6db14c4aede9a106e8023effac3c99d3c037a
2026-04-29 13:54:53 +08:00
wujunchen
2d39a91aed test: split architecture guard tests
Change-Id: Ibde4ae79f202ae0e26658ab47bbcf5b52ef9effe
2026-04-29 13:53:19 +08:00
wujunchen
39ddd4289e fix: count batch generation failures
Change-Id: Ie6b29cb3efc22fe72250e9c63c1e82c71b24a356
2026-04-29 13:51:33 +08:00
wujunchen
0801b644f6 fix: address CR findings — bump script, test runner, temp cleanup
- bump-version.mjs: reject duplicate versions (use --force to override),
  auto git-add manifest.json + versions.json for npm version hook
- run-tests.mjs: use execFileSync to handle paths with spaces
- test-setup.js: clean up esbuild temp directory on process exit

Change-Id: I44410b550d09487dd7f5e30a8f9385a2c500252a
2026-04-27 20:19:53 +08:00
wujunchen
199ab73188 refactor: split main.test.js into focused test files
Reduce main.test.js from 560 to 70 lines by keeping only architecture
guards and export smoke tests. Moved provider error tests, Anthropic
tool-use parsing, and cancellationNoticeKey tests to providers.test.js.
Settings, cache, and other tests were already covered in their
dedicated test files.

Change-Id: I8f0f3c576dec7fcf5a4c6e0c2b6071601940349d
2026-04-27 19:59:46 +08:00
wujunchen
f5f974fd74 build: remove __test barrel from production bundle
Tests now load src/test-exports.ts via esbuild buildSync instead of
importing __test from the production main.js. This removes ~60 internal
symbols and all test-only module code from the shipped plugin bundle.

Change-Id: I2caf5232f6894c7514215e1751f2e268b74d7dc4
2026-04-27 19:57:28 +08:00
wujunchen
0bf5ab12b1 lint: extend Biome coverage to tests/ and scripts/
Add tests/**/*.js and scripts/**/*.mjs to biome.json includes and
update npm lint scripts to cover these directories. Auto-fixed all
formatting issues in 26 files.

Change-Id: I8c2763cb9683616a8abc1484d91bf8af79bb590e
2026-04-27 19:53:35 +08:00
wujunchen
b26b241c7f refactor: centralize obsidian mock into tests/obsidian-mock.js
Extracts the duplicated Module._load obsidian stub from main.test.js,
generation-job-manager.test.js, and test-setup.js into a single shared
file. All three now require('./obsidian-mock') instead of defining
their own copy.

Change-Id: I8ac38f779299ac250963357eee9f7824e13c7d33
2026-04-27 19:52:34 +08:00
wujunchen
df48dc2cc4 fix: remove unsupported --max-tokens flag from claude CLI args
The Claude Code CLI doesn't support --max-tokens. This caused the CLI
backend to always fail with exit code 1. Also adds args validation tests
for both claude and codex CLI backends.

Change-Id: I57c5cc672ed08de291e3941511ce68ba56e5b4ff
2026-04-27 19:07:53 +08:00
wujunchen
05ac2b923f test: add batch.ts comprehensive tests including isFileInBatchFolder
Tests cover isFileInBatchFolder (root, subfolder, nested), edge cases
for normalizeBatchFolderInput (null, undefined, only slashes),
hasUnsafeBatchFolderSegments (null, single dot, dotfile), selectBatchFiles
with complex folder structures, and stats immutability verification.

Change-Id: If4964355db427347060ea3f902e8f6ef71ea2471
2026-04-27 15:31:37 +08:00
wujunchen
741012e535 test: add settings.ts edge case tests for utility functions
Tests cover stableStringify (key sorting, nested, types), isApiBackend,
getApiFormat/getApiAuthType fallback behavior, normalizeMaxCacheEntries,
hashContent consistency, normalizeSettings validation (invalid uiLanguage,
apiProvider, apiFormat, apiAuthType, non-string customSystemPrompt),
and pruneCacheEntries with null/empty/non-object inputs.

Change-Id: I96269c69871e3c17ccb06a71ac9cfb9733a382f6
2026-04-27 15:30:33 +08:00
wujunchen
6f80d0de03 test: add generation.ts cancellationNoticeKey edge case tests
Tests cover all backend types (api, anthropic-api, claude-code, codex),
all generation phases (queued, reading, cache-check, generating, saving),
and null settings/job edge cases.

Change-Id: I722fb0cf1129c7cf45dfc5bf2544577652df843d
2026-04-27 15:29:29 +08:00
wujunchen
0f8625874e test: add comprehensive markdown.ts unit tests
Tests cover cardToMarkdown, cardToPlain, and cardsToMarkdown with
edge cases: empty/missing fields, anchor whitespace normalization,
null cards array, empty title fallback, plain text bullet formatting.

Change-Id: I1972705d385f0393c9fd1be7f80d6513f5328d06
2026-04-27 15:28:34 +08:00
wujunchen
1e2ccc0840 test: add comprehensive prompt.ts unit tests
Tests cover promptLanguageInstruction, promptSchemaExample,
renderPromptTemplate, and buildPrompts edge cases including
auto language, truncation markers, custom prompt variable
substitution, card count normalization, and fallback behavior.

Change-Id: I75d5b354df15456ddbd08d9d21846f74d6b6cea3
2026-04-27 15:27:19 +08:00
wujunchen
0264ef0b6c refactor: split direct-modules.test.js into 5 per-module test files
Replaces the monolithic direct-modules.test.js (780 lines) with focused
per-module test files sharing a common direct-test-setup.js. Each file
is under 165 lines. All existing test assertions preserved.

New files: direct-test-setup.js, direct-cache.test.js,
direct-providers.test.js, direct-settings.test.js, direct-i18n.test.js,
direct-streaming.test.js

Change-Id: Ie9cceaf3b9b71e618c0d7e524a5186b5c44079c8
2026-04-27 15:24:32 +08:00
wujunchen
7cb68658f5 refactor: split modules.test.js into 12 per-module test files
Replaces the monolithic modules.test.js (983 lines) with focused
per-module test files sharing a common test-setup.js. Each file is
under 225 lines. All existing test assertions preserved.

New files: test-setup.js, anchor.test.js, schema.test.js, cache.test.js,
cards-nav.test.js, markdown.test.js, vault-batch.test.js, i18n.test.js,
scroll.test.js, settings.test.js, providers.test.js, streaming.test.js,
cli.test.js

Change-Id: I37a3342a1588807277e679e6535cde361e650565
2026-04-27 15:21:03 +08:00
fancivez
b2716da4ca test: add getApiKey, cacheEntryMatches, normalizeCardCount edge case tests
Cover normalizeCardCount boundary values (0, negative, NaN, max 30, string).
Test getApiKey with direct key, env var, precedence, and trimming.
Test cacheEntryMatches for valid match, null entry, wrong hash, wrong schema.

Change-Id: I1113d923885e03f6b28d47721cc303bbbb6d70ed
2026-04-27 14:11:37 +08:00
fancivez
c111865fbd test: add resolveCardAnchors unit tests
Cover sorting by startLine, unanchored cards pushed to end, empty/null
input, all-unanchored relative order, and default level value.

Change-Id: I74c48041897e8e4fb44555aaa319e6ebde8f91b1
2026-04-27 14:11:37 +08:00
fancivez
0ef6395ed7 test: add parseApiHeaders unit tests covering all input formats
Export parseApiHeaders for testing. Cover: JSON object input, empty/whitespace,
line-based input, comments/blank lines, malformed JSON, array input fallback,
missing colon, non-string JSON values filtered, colons in values preserved.

Change-Id: Ia00898888c6a0b78a284b43f29e382cc4080ba88
2026-04-27 14:11:37 +08:00
fancivez
3a2c4e6a1c test: add vault.ts normalizeVaultPath and folderPathsForTarget tests
Cover normalizeVaultPath with clean paths, whitespace trimming, parent
traversal stripping, dot segments, null input, and empty segments.
Test folderPathsForTarget ancestor chain generation for various depths.

Change-Id: I4de1744301df1e42d35f0bd5702d5cc58fb9a277
2026-04-27 14:11:37 +08:00
fancivez
d6b89924f2 test: add i18n translate and resolveUiLanguage unit tests
Cover translate() with explicit languages, variable interpolation (single,
multiple, numeric), missing keys, null settings, unmatched placeholders.
Test resolveUiLanguage for explicit/auto/null settings.
Validate zh/en STRINGS parity (same key count, no missing keys).

Change-Id: I06c3bdc9d824eaa0e205b335b9856d872a1aefd1
2026-04-27 14:11:37 +08:00
fancivez
1efacf695c test: add schema.ts edge case coverage for repair and JSON extraction
Test repairTruncatedCardsJson with whitespace variations, escaped quotes,
braces in string values, all-truncated cards, and empty arrays.
Add collectJsonObjectCandidates tests for empty input, nested objects,
braces in strings, and unclosed braces.

Change-Id: I82392bff6eaf457934dbbe63e7c5788ea6a44e19
2026-04-27 14:11:37 +08:00
fancivez
be8ad72ba2 test: add comprehensive parseSseBuffer and deltaExtractor tests
Cover SSE parsing edge cases: single/multiple events, incomplete buffers,
CRLF line endings, [DONE] sentinel, multi-line data fields, malformed JSON
skipping, comment/event lines, data: without trailing space, empty buffer.
Test both OpenAI and Anthropic delta extractors with various input shapes.

Change-Id: I108352e07233490d23b86ed31b855692c360d4d1
2026-04-27 14:11:36 +08:00
fancivez
b3d774f4df test: add provider-parsers edge case coverage
Test all parser functions with empty responses, missing fields, null inputs,
legacy format fallbacks, multi-block concatenation, and tool-use rejection
for wrong tool names and missing content arrays.

Change-Id: Ie3c15aa636a6b421ca6bf4315313368c249f2782
2026-04-27 14:11:36 +08:00
fancivez
f7c4f42a5a test: add comprehensive CacheManager unit test coverage
Cover previously untested methods: get(), move() (success/failure/edge
cases), readFile() with corrupt JSON and missing entries, pruneIfNeeded(),
replaceCards() for missing entries, and scheduleSave()+flush() behavior.

Change-Id: Iee71e574d37925c2111eb70a292d5f59a80dc3c5
2026-04-27 14:11:36 +08:00
wujunchen
538d7bcedd test: add structured output fallback and non-retryable error tests
Cover the two key paths in requestJsonBodyWithStructuredFallback:
1. 400 with response_format mention → retry without structured output
2. 500 server error → no retry, error propagated

Change-Id: If8d0fc484e49238699444131cbadcf1b18bf992f
2026-04-27 12:44:42 +08:00
wujunchen
67a9ccdafb test: add applyApiProviderPreset model swap coverage
Cover the key scenarios for provider switching: model swapped when
matching previous preset, custom model preserved across switches,
empty model triggers swap, and input immutability verified.

Change-Id: I467604d51e8c11e990e54621347094cf914cb6b7
2026-04-27 12:43:29 +08:00
wujunchen
b02d0f4f30 test: add cache pruning interleaved with put test
Verify that when a new entry is added via put() and the cache exceeds
maxCacheEntries, the oldest entry (by timestamp) is pruned and the
pruned state is correctly persisted to disk.

Change-Id: Ia937800a97d7591877e1d2d1fe808b86858735e1
2026-04-27 12:30:56 +08:00
wujunchen
cfdd985f78 test: add batch state isolation and cancellation tests
Verify that two concurrent BatchRunState objects don't interfere with
each other (cancelling one doesn't affect the other), and that stats
accumulate correctly when processing is stopped mid-iteration.

Change-Id: Id32b27307beafe49594cfde6fa818b2f22b87c66
2026-04-27 12:29:42 +08:00
wujunchen
593817734b test: add streaming cancellation edge case tests
Cover pre-aborted signal (aborted before fetch starts) and mid-read
abort (signal aborted while reading response body). Both verify that
abort listeners are properly cleaned up after rejection.

Change-Id: I0760f6ed051de1504e9b7aeead01a10cfd12476e
2026-04-27 12:28:36 +08:00
wujunchen
09f39dde8d fix: make normalizeSettings return a new object instead of mutating input
normalizeSettings previously modified its input parameter in-place.
While callers already created copies before calling it, the function
signature was misleading. Now it creates a shallow copy internally and
operates on that, with a Readonly<PluginSettings> parameter type to
signal intent. Added a test verifying the original object is unchanged.

Change-Id: I7600ac37930bbcebb02aa7c84d8988cb7aa71196
2026-04-27 12:20:56 +08:00