fix(cli,service-storage): stop re-pointing the storage adapter every boot — and mean it when it warns (#4096) - #4166
Conversation
…boot — and mean it when it warns (#4096) Every `os dev` / `os serve` boot printed "storage adapter swapped (LocalStorageAdapter → LocalStorageAdapter). Existing files were NOT migrated and may be unreachable through the new adapter." The warning was telling the truth. `serve` constructed the plugin with `{ driver: 'local', root }` and `StorageServicePluginOptions` declares neither key, so both were dropped silently: the plugin applied its own `./storage` default, OS_STORAGE_ROOT changed nothing, and uploads landed in a directory nobody named. The `storage` settings namespace then corrected the root on its first read — its manifest default is `./.objectstack/data/uploads` — genuinely moving the backing store, every boot, forever. Three defects, three fixes: - serve passes options the plugin reads: `{ adapter: 'local', local: { rootDir } }`. OS_STORAGE_ROOT takes effect and local uploads land under `.objectstack/data/uploads` from the first byte. Extracted as `resolveStorageCapabilityArg` so the option SHAPE is pinned — a mismatch like this type-checks fine as an argument and does nothing at runtime. - A swap is skipped when nothing changed. The plugin records what the running adapter points at and compares resolved CONFIGURATIONS, rather than rebuilding whenever the settings namespace held any value at all (true on every boot once that namespace has persisted its own defaults). - The warning means what it says: it fires when the BACKING STORE moved, not when the adapter object was replaced. A credential rotation swaps so the new key takes effect and logs at info — same bucket, nothing stranded. A swap from a caller that resolved no target still warns; ignorance must not silence it. Path spellings are normalised, so the platform writing one default two ways (`./.objectstack/data/uploads` vs `.objectstack/data/uploads`) is not read as a migration between a directory and itself. Verified on examples/app-todo: boot diagnostics went from four warnings to three, the storage line is gone, and `./storage` is no longer created. 19 unit cases on the target resolver and the swap/warn split, 4 plugin-level cases on what a boot does and says, 7 on the CLI option shape. service-storage 241 tests, cli 917 tests, eslint clean, tsc error count unchanged from main. `config.storage` authored with the `driver`/`root` dialect is still forwarded verbatim and still unread by the plugin — the same mismatch one layer up, filed separately rather than papered over with a lenient alias here (Prime Directive #12). Closes #4096 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
Checked the docs-drift advisory rather than waving it through, and it turned up something that belongs in the record: no doc needs changing, because the docs were already right and the code was wrong. Three of them describe exactly the behaviour this PR restores.
The third row is the one worth pausing on: a documented backup procedure that silently captured nothing, on the exact deployment shape it is written for. The only visible symptom was the boot warning in #4096 — which, as the PR description covers, was accurate and was reporting this. That also settles the direction question. No documentation edits in this PR — the fix makes the existing pages true. Generated by Claude Code |
Closes #4096.
The issue's premise was half right — the warning was telling the truth
#4096 reports this on every clean boot and calls it spurious:
I built the "nothing changed, so don't swap" fix first — and the warning survived it. A probe at the decision point said why:
The backing store really was moving on every boot. The adapter was starting in the wrong directory and being corrected seconds later, and the warning was an accurate report of a real misconfiguration nobody could act on.
Root cause: option keys nothing reads
serveconstructed the plugin with{ driver: 'local', root }.StorageServicePluginOptionsdeclares neither key — it takes{ adapter, local: { rootDir } }. Both were dropped on the floor, so:./storagedefault;OS_STORAGE_ROOTdid nothing — a documented env var with no effect;./storage, not the.objectstack/data/uploads/the code comment promises;storagesettings namespace then corrected the root on its first read (its manifest default atstorage.manifest.ts:38is./.objectstack/data/uploads), swapping the adapter and warning — forever.A plain object literal passed to a parameter without an index signature type-checks fine and does nothing at runtime. Nothing was going to catch this except reading it.
Three defects, three fixes
servepasses what the plugin reads —{ adapter: 'local', local: { rootDir } }. Extracted asresolveStorageCapabilityArgso the option shape is pinned by tests, including an explicitnot.toHaveProperty('driver'|'root').info: same bucket, nothing stranded. Aswap()from a caller that resolved no target still warns — ignorance must not silence the one message that exists to be impossible to miss.Path spellings are normalised, so the platform writing one default two ways (
./.objectstack/data/uploadsin the settings manifest,.objectstack/data/uploadsin the CLI) is not read as a migration between a directory and itself.The fingerprint that decides "did anything change" includes S3 credentials and is never logged; the
locationthat goes into the warning text deliberately excludes them, and a test asserts it.Verification
examples/app-todo, real boot:adapter swappedoccurrences./storagecreatedTests
basePathchange (URLs move, bytes don't), kind change, same bucket on a different endpoint,./xvsx, and the unknown-caller case that must still warn.Confirmed to fail against the pre-fix code (3 plugin cases fail, one with the exact
adapter swapped (LocalStorageAdapter → LocalStorageAdapter)text).service-storage19 files / 241 tests;cli89 files / 917 tests;eslintclean;tscerror count unchanged frommain(42 pre-existing, none added).Left alone deliberately
config.storageauthored with thedriver/rootdialect is still forwarded verbatim and still not read by the plugin — the same mismatch one layer up, so a host writing{ driver: 's3', … }silently gets local disk. Fixing it means deciding whether the plugin accepts that dialect or the config schema is wrong; a lenient alias here would fossilize the wrong contract (AGENTS.md Prime Directive #12), so it is filed separately rather than papered over. The helper's doc comment says so at the call site.Generated by Claude Code