Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import z from 'schemastery'
|
|
import InvariantService from '@deepseek-ai/dsh-invariants'
|
|
import * as SettingsInvariant from '../src/invariant.ts'
|
|
import { settingsNamespace } from '../src/index.ts'
|
|
import { MemorySettings } from './memory.ts'
|
|
|
|
async function setup(withProvider: boolean): Promise<Context> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(InvariantService)
|
|
await ctx.plugin(SettingsInvariant)
|
|
if (withProvider) await ctx.plugin(MemorySettings)
|
|
return ctx
|
|
}
|
|
|
|
describe('settings invariants', () => {
|
|
it('fails a settings/updated emission without a live settings service', async () => {
|
|
const ctx = await setup(false)
|
|
expect(() => {
|
|
ctx.emit('settings/updated', settingsNamespace('ghost'), { a: 1 }, { a: 2 }, 'provider')
|
|
}).toThrow(/without a live settings service/)
|
|
})
|
|
|
|
it('fails a settings/updated emission for an unregistered namespace', async () => {
|
|
const ctx = await setup(true)
|
|
expect(() => {
|
|
ctx.emit('settings/updated', settingsNamespace('ghost'), { a: 1 }, { a: 2 }, 'provider')
|
|
}).toThrow(/unregistered/)
|
|
})
|
|
|
|
it('fails a settings/updated emission without a resolved-value change', async () => {
|
|
const ctx = await setup(true)
|
|
ctx.settings.register(settingsNamespace('ui-theme'), z.object({
|
|
theme: z.string().default('dark'),
|
|
}))
|
|
expect(() => {
|
|
ctx.emit('settings/updated', settingsNamespace('ui-theme'), { theme: 'dark' }, { theme: 'dark' }, 'update')
|
|
}).toThrow(/without a resolved-value change/)
|
|
})
|
|
|
|
it('fails a settings/updated emission whose value diverges from the authoritative state', async () => {
|
|
const ctx = await setup(true)
|
|
ctx.settings.register(settingsNamespace('ui-theme'), z.object({
|
|
theme: z.string().default('dark'),
|
|
}))
|
|
// Fabricated next ≠ the service's current resolved value ({theme: 'dark'}).
|
|
expect(() => {
|
|
ctx.emit('settings/updated', settingsNamespace('ui-theme'), { theme: 'forged' }, { theme: 'dark' }, 'update')
|
|
}).toThrow(/authoritative/)
|
|
})
|
|
})
|