fix(storage,workspace): post-review hardening
Review findings applied across the group: - storage hub: stale disposers no longer remove a successor registration; the package now default-exports the Storage service class per the service-package export shape. - json backend: failed publishes roll back the authoritative memory state (a rejected write can no longer resurface via get() or ride the next publish); close() drains in-flight writes and blocks in-flight opens; double-open rejects as a plain caller error instead of malformed-medium. - sqlite backend: loadAll builds records on a null prototype (__proto__ keys round-trip instead of polluting), user_version is stamped only after the schema is fully created, and corrupt record JSON rejects as malformed-medium instead of a bare SyntaxError. - domain form: writes persist before mutating authoritative memory or emitting; DomainChanged is a put/deleted discriminated union. - workspace: attach/detach idempotence decided on the write chain (stale snapshots no longer short-circuit), create() requires a directory, and startup fails loud on duplicate stored paths. Eleven regression tests pin the fixed behaviors.
This commit is contained in:
@@ -70,11 +70,49 @@ describe('json backend specifics', () => {
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rejects double-open of one unit', async () => {
|
||||
it('rejects double-open of one unit as a plain caller error', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await backend.kv.open(descriptor)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toMatchObject({ code: 'malformed-medium' })
|
||||
await expect(backend.kv.open(descriptor)).rejects.toThrowError(/already open/)
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rolls back memory when a publish fails', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
const unit = await backend.kv.open(descriptor)
|
||||
await unit.putRecord('t', 'k', { v: 'committed' })
|
||||
// Make the next publish fail: replace the unit file's parent with an
|
||||
// unwritable directory path via chmod.
|
||||
const { chmod } = await import('node:fs/promises')
|
||||
await chmod(root, 0o500)
|
||||
await expect(unit.putRecord('t', 'k', { v: 'rejected' })).rejects.toThrow()
|
||||
await expect(unit.putRecord('t', 'k2', { v: 'also rejected' })).rejects.toThrow()
|
||||
await chmod(root, 0o700)
|
||||
const snapshot = await unit.loadAll()
|
||||
expect(snapshot.tables['t']).toEqual({ k: { v: 'committed' } })
|
||||
// The next successful publish must not carry rejected writes to disk.
|
||||
await unit.putRecord('t', 'k3', { v: 'later' })
|
||||
const text = await readFile(join(root, 'shape.json'), 'utf8')
|
||||
expect(text).not.toContain('rejected')
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('close drains in-flight writes and blocks in-flight opens', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
const unit = await backend.kv.open(descriptor)
|
||||
const bigWrite = unit.putRecord('t', 'big', { blob: 'x'.repeat(4 * 1024 * 1024) })
|
||||
await unit.close()
|
||||
await expect(bigWrite).resolves.toBeUndefined()
|
||||
const onDisk = JSON.parse(await readFile(join(root, 'shape.json'), 'utf8'))
|
||||
expect(onDisk.tables.t.big).toBeDefined()
|
||||
|
||||
const backend2 = new JsonStorageBackend(root)
|
||||
const opening = backend2.kv.open(descriptor)
|
||||
const closing = backend2.close()
|
||||
await expect(opening.then((u) => u.putRecord('t', 'x', {}))).rejects.toMatchObject({ code: 'closed' })
|
||||
await closing
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user