Skip to content

fix: make models refresh publish safely#211

Merged
Astro-Han merged 1 commit into
devfrom
fix/models-refresh-reliability
Apr 24, 2026
Merged

fix: make models refresh publish safely#211
Astro-Han merged 1 commit into
devfrom
fix/models-refresh-reliability

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

Make models.dev refresh safer and visible to already-running provider reads. Refresh now validates candidate catalogs before publishing, writes cache updates through a temporary file plus rename, and tracks a catalog version so provider state rebuilds after successful refreshes or cache source resets.

Why

PawWork could fetch a newer models.dev catalog but keep serving stale provider state in the same running process. A failed or malformed refresh could also risk replacing a usable local cache. This PR makes refresh failure non-destructive while allowing newly published models to appear without requiring a restart.

Related Issue

No linked issue. This came from local investigation into stale models.dev refresh behavior.

How To Verify

cd packages/opencode
bun test test/provider/models-refresh.test.ts
bun test test/provider/provider.test.ts
bun run typecheck

Manual smoke, using temporary XDG_* directories so the real PawWork cache is not touched:

cd packages/opencode
XDG_CACHE_HOME=/tmp/pawwork-models-smoke/cache XDG_DATA_HOME=/tmp/pawwork-models-smoke/data XDG_CONFIG_HOME=/tmp/pawwork-models-smoke/config XDG_STATE_HOME=/tmp/pawwork-models-smoke/state PAWWORK_RUNTIME_NAMESPACE=pawwork-models-refresh-smoke MOONSHOT_API_KEY=dummy bun src/index.ts models moonshotai-cn --refresh

Observed moonshotai-cn/kimi-k2.6 after refresh.

Screenshots or Recordings

Not applicable. No visible UI changes.

Checklist

  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I listed the relevant verification steps, including tests when behavior changed
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, or generated/local file changes when relevant
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • New Features

    • Model catalogs now include version tracking.
    • Environment variable support added to override model catalog path.
    • New methods available for querying and retrieving catalog versions.
  • Bug Fixes

    • Model updates now use atomic file write operations.
  • Tests

    • Comprehensive test coverage added for model refresh behavior.

@Astro-Han Astro-Han added bug Something isn't working P2 Medium priority harness Model harness, prompts, tool descriptions, and session mechanics labels Apr 24, 2026
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b0005fc9-fc11-4b7c-a312-3517c820d940

📥 Commits

Reviewing files that changed from the base of the PR and between d11aea5 and af6ea3c.

📒 Files selected for processing (3)
  • packages/opencode/src/provider/models.ts
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/provider/models-refresh.test.ts
📝 Walkthrough

Walkthrough

Implements version-tracked catalog publishing for model providers with atomic file operations, validation schemas, and state versioning. Introduces provider state invalidation to reload cached instance state when model catalog versions change, and adds comprehensive test coverage for refresh behaviors.

Changes

Cohort / File(s) Summary
Catalog Publishing & Versioning
packages/opencode/src/provider/models.ts
Introduces Zod schemas for catalog validation, atomic temp-file writes with cleanup, catalogVersion tracking, and new exports version() and getWithVersion() with retry logic until catalog stability. Updates Data and refresh() to honor OPENCODE_MODELS_PATH env override and increment versions.
Provider State Invalidation
packages/opencode/src/provider/provider.ts
Adds State.modelsVersion tracking and introduces Provider.currentState() to detect model catalog version changes, triggering provider state reload. Updates list(), getProvider(), getModel(), getLanguage(), closest(), and defaultModel() to read from currentState() instead of cached reads; enhances error handling for missing providers.
Refresh Behavior Tests
packages/opencode/test/provider/models-refresh.test.ts
New comprehensive test suite covering success and failure scenarios for ModelsDev.refresh(), including validation errors, network failures, atomic write errors, environment overrides, and connected provider overlay merging.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Provider
    participant ModelsDev
    participant FileSystem
    
    Client->>Provider: list() / getModel() / etc.
    Provider->>Provider: currentState()
    Provider->>ModelsDev: version()
    ModelsDev-->>Provider: current catalogVersion
    
    alt Version mismatch detected
        Provider->>ModelsDev: getWithVersion()
        ModelsDev->>FileSystem: read catalog
        ModelsDev->>ModelsDev: validate schemas
        ModelsDev->>ModelsDev: invoke fromModelsDevProvider
        ModelsDev-->>Provider: catalog + new version
        Provider->>Provider: reload InstanceState
    end
    
    Provider-->>Client: providers from currentState
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A version-tracked refresh, so tidy and spry,
Atomic writes tumble through temp files on high,
State invalidation catches the change,
Provider reloads in versioned exchange,
From catalog chaos to clarity, my!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: making the models refresh process safer through atomic writes and validation before publishing.
Description check ✅ Passed The description includes all required template sections: summary, why, related issue, how to verify with commands, screenshots statement, and completed checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/models-refresh-reliability

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/provider/models.ts`:
- Around line 292-305: The getWithVersion function currently has an unbounded
while(true) retry that could loop forever if version keeps changing; add a retry
limit (e.g., MAX_RETRIES constant) inside getWithVersion, increment a counter
each loop, and if the counter exceeds the limit, call Data.reset() (as currently
done on mismatch) and throw an explicit error or return a clear failure result
so callers can handle it; keep the existing behavior (compare before/after
version, call withPawWorkProviders) for successful attempts and ensure the
counter is reset or local to the function so retries are bounded.
- Around line 113-179: PublishModel and PublishProvider largely duplicate Model
and Provider with only small optionality differences; extract shared field
definitions into a common base and build strict and publish variants from it to
avoid drift. Create a BaseModelFields object (or BaseModel zod partial) for
repeated keys and use Model = z.object({ ...BaseModelFields, release_date:
z.string(), /* required fields */ }) and PublishModel = z.object({
...BaseModelFields, release_date: z.string().optional(), /* optional overrides
*/ }).passthrough(); do the same for Provider/PublishProvider (reuse
BaseProviderFields) and replace duplicated inline definitions in
PublishModel/PublishProvider with spreads of the shared bases and only change
optionality where needed.

In `@packages/opencode/src/provider/provider.ts`:
- Around line 1416-1421: The current Provider.currentState implementation races:
two callers can both see existing.modelsVersion !== ModelsDev.version() and both
call InstanceState.invalidate(state) then rebuild, wasting CPU; update
Provider.currentState to serialize invalidate-and-reload (e.g., replace the
manual check+invalidate with an Effect.cached wrapper around the reload logic or
add a Ref<boolean> guard) so only the first caller performs
InstanceState.invalidate(state) and subsequent callers await the cached reload
via InstanceState.get(state); touch the function named Provider.currentState and
calls to InstanceState.get / InstanceState.invalidate and reference
ModelsDev.version when implementing the cache or guard.

In `@packages/opencode/test/provider/models-refresh.test.ts`:
- Around line 20-27: The tests leave the module-private catalogVersion
incremented across runs; add a test-only reset and call it in the afterEach to
avoid cumulative versioning. Update the module that defines
catalogVersion/version() (the same module providing version()) to export a
resetCatalogVersion() or resetForTests() function that sets catalogVersion back
to its initial value, then call that new reset function from the test afterEach
alongside ModelsDev.Data.reset() so each test starts with a clean
catalogVersion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a844503-b6c1-4830-ab08-76275996ec1e

📥 Commits

Reviewing files that changed from the base of the PR and between 1b0a8b8 and d11aea5.

📒 Files selected for processing (3)
  • packages/opencode/src/provider/models.ts
  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/provider/models-refresh.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-app
  • GitHub Check: typecheck
  • GitHub Check: unit-desktop
  • GitHub Check: unit-app
  • GitHub Check: unit-opencode
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: analyze-js-ts
  • GitHub Check: e2e-artifacts
🧰 Additional context used
📓 Path-based instructions (2)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/models.ts
  • packages/opencode/test/provider/models-refresh.test.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/provider/models-refresh.test.ts
🧠 Learnings (16)
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/src/provider/provider.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `InstanceState` from `src/effect/instance-state.ts` for per-directory or per-project state that needs per-instance cleanup; do work directly in the `InstanceState.make` closure where `ScopedCache` handles run-once semantics

Applied to files:

  • packages/opencode/src/provider/provider.ts
📚 Learning: 2026-04-21T16:00:44.910Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 102
File: packages/opencode/src/config/model-id.ts:9-11
Timestamp: 2026-04-21T16:00:44.910Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/config/model-id.ts`), `ConfigModelID` uses a regex that requires a non-empty provider segment and a non-empty model segment separated by `/`, but intentionally allows additional slashes within the model segment. This is because `Provider.parseModel` supports model IDs like `synthetic/hf:zai-org/GLM-4.7`. Do not flag the permissive slash handling as a bug.

Applied to files:

  • packages/opencode/src/provider/provider.ts
  • packages/opencode/src/provider/models.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/provider/provider.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/src/provider/provider.ts
  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/provider/models.ts
📚 Learning: 2026-04-22T09:32:52.806Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:52.806Z
Learning: For tests under `packages/opencode/test/provider/` (e.g., `*.test.ts`), keep the test harness pattern consistent across the whole file: if the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) instead of `testEffect(...)`, do not partially convert individual tests to `testEffect` while leaving others on the async helper pattern. If the harness pattern must change, migrate the entire file as a cohesive harness migration (ideally as a separate PR) to avoid internal inconsistency.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `init` option in `tmpdir` to define custom setup functions that can return extra data accessible via `tmp.extra`, and use the `dispose` option for custom cleanup logic.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
📚 Learning: 2026-04-20T17:03:40.214Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 73
File: packages/opencode/src/cli/cmd/tui/context/sync.tsx:486-489
Timestamp: 2026-04-20T17:03:40.214Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/cli/cmd/tui/context/sync.tsx`), `sync.ready` returning `true` when `process.env.OPENCODE_FAST_BOOT` is set is intentional. The plugin-facing data properties `state.config` (initialized to `{}`) and `state.provider` (initialized to `[]`) expose safe-empty defaults, so they are safe to access before bootstrap completes. Do not flag these as needing null-guards or conditional patterns to match `vcs` — the difference is intentional because `vcs` starts as `undefined` while the others have initialized defaults. Changing this would alter the plugin API contract without a concrete failing case.

Applied to files:

  • packages/opencode/test/provider/models-refresh.test.ts
🔇 Additional comments (12)
packages/opencode/src/provider/provider.ts (3)

1609-1615: LGTM!

Good defensive check. After a catalog refresh that removes a provider, this now throws a structured ModelNotFoundError instead of crashing on undefined access in resolveSDK.


1177-1184: LGTM!

The access pattern correctly reflects the new getWithVersion() return shape with providers and version fields.


963-970: LGTM!

The modelsVersion field enables tracking when the cached state was built, allowing currentState() to detect when a refresh has occurred.

packages/opencode/src/provider/models.ts (4)

227-236: LGTM!

Correct atomic write pattern: temp file with unique suffix → write → rename. The cleanup in catch with force: true handles the case where the temp file wasn't created. This prevents partial writes from corrupting the cache.


307-330: Version increment logic is correct but subtle.

The multiple catalogVersion++ calls serve different purposes:

  • Lines 309-310: Override path changed externally, force provider rebuild
  • Lines 314-315: Fresh cache detected before lock, signal potential external change
  • Lines 319-320: Fresh cache inside lock (another process updated), signal change
  • Line 324: publishCandidate increments after successful publish

This is correct—each increment signals "providers may need to rebuild." Consider adding a brief comment explaining the invariant that any path triggering Data.reset() should also increment catalogVersion.


219-225: Dynamic import avoids circular dependency at module load time.

The dynamic import("./provider") is intentional to break the circular reference (provider.ts statically imports models.ts). The validation ensures catalogs can be transformed to runtime providers before committing to disk.


269-283: LGTM!

Good separation of concerns: validation failure (line 280) logs and returns {} (no valid data to serve), while write failure (line 276) logs but still returns the valid catalog. This ensures a valid fetched catalog is usable even if cache persistence fails.

packages/opencode/test/provider/models-refresh.test.ts (5)

77-86: LGTM!

withTestInstance correctly uses tmpdir and Instance.provide to establish isolated test context, consistent with the established pattern in provider tests (per learnings).


180-183: Type assertion to mutate Filesystem.write is acceptable for test mocking.

The cast (Filesystem as { write: typeof Filesystem.write }).write works around the readonly property. This is a standard pattern for test mocks. The cleanup in afterEach restores the original.


261-274: LGTM!

Excellent test coverage for the provider-removal edge case. This validates that getLanguage throws ModelNotFoundError when the provider is removed from the catalog during refresh, rather than crashing on undefined access.


276-298: LGTM!

Good integration test verifying that the overlay pattern (merging ModelsDev.get() with Provider.list()) correctly reflects refreshed catalog state.


91-101: LGTM!

Good baseline test establishing the happy path: valid candidate → cache updated → version incremented.

Comment thread packages/opencode/src/provider/models.ts
Comment thread packages/opencode/src/provider/models.ts
Comment thread packages/opencode/src/provider/provider.ts
Comment thread packages/opencode/test/provider/models-refresh.test.ts

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a versioning system for the models catalog to ensure the runtime state remains synchronized with the underlying data. Key changes include the introduction of Zod schemas for catalog validation, atomic file operations for safe updates, and a mechanism in the provider layer to invalidate stale state based on catalog versioning. Review feedback suggests removing a redundant reset call in the versioned getter and enhancing error reporting in getLanguage by adding provider suggestions when a lookup fails.

Comment thread packages/opencode/src/provider/models.ts
Comment thread packages/opencode/src/provider/provider.ts
@Astro-Han Astro-Han force-pushed the fix/models-refresh-reliability branch from d11aea5 to af6ea3c Compare April 24, 2026 06:49
@Astro-Han Astro-Han merged commit 720e347 into dev Apr 24, 2026
23 checks passed
@Astro-Han Astro-Han deleted the fix/models-refresh-reliability branch April 24, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant