| … | … | @@ -0,0 +1,66 @@ |
| 1 | // A corrupt tree-meta file must not take the store reader down. |
| 2 | // |
| 3 | // Regression: getMeta guarded a MISSING file but not an EMPTY one, and JSON.parse('') throws |
| 4 | // "Unexpected end of JSON input". ONE zero-byte meta file on stortron therefore broke |
| 5 | // `pgh store show` and `pgh store pin` for EVERY hash — the reader throws before it can even say |
| 6 | // which file is at fault. hyper-social #24 recorded the consequence: rollback pins could not be |
| 7 | // taken on that host, which is exactly the safety net you want before a risky deploy. |
| 8 | // |
| 9 | // Meta is a CACHE (pulledAt/source/pinned/schema), never the tree, so degrading to "no meta" is |
| 10 | // always safe. _readRefs in the same class already tolerated this; getMeta was the outlier. |
| 11 | import test from 'brittle' |
| 12 | import fs from 'fs' |
| 13 | import os from 'os' |
| 14 | import path from 'path' |
| 15 | import { TreeStore } from '../lib/store.js' |
| 16 | |
| 17 | const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'pgh-meta-')) |
| 18 | const HASH = 'a'.repeat(64) |
| 19 | |
| 20 | function storeWith (metaContent) { |
| 21 | const root = tmp() |
| 22 | const s = new TreeStore(root) |
| 23 | s.init() |
| 24 | if (metaContent !== null) fs.writeFileSync(s.metaPath(HASH), metaContent) |
| 25 | return s |
| 26 | } |
| 27 | |
| 28 | test('an EMPTY meta file reads as no-meta rather than throwing', (t) => { |
| 29 | const s = storeWith('') |
| 30 | t.is(s.getMeta(HASH), null, 'the exact stortron case: zero bytes') |
| 31 | }) |
| 32 | |
| 33 | test('a TRUNCATED meta file reads as no-meta', (t) => { |
| 34 | const s = storeWith('{"pulledAt": 17') |
| 35 | t.is(s.getMeta(HASH), null, 'a half-written file (crash mid-write) does not throw') |
| 36 | }) |
| 37 | |
| 38 | test('whitespace-only reads as no-meta', (t) => { |
| 39 | t.is(storeWith(' \n ').getMeta(HASH), null) |
| 40 | }) |
| 41 | |
| 42 | test('non-object JSON reads as no-meta', (t) => { |
| 43 | t.is(storeWith('42').getMeta(HASH), null, 'valid JSON that is not a meta object is still no-meta') |
| 44 | t.is(storeWith('null').getMeta(HASH), null) |
| 45 | }) |
| 46 | |
| 47 | test('a MISSING meta file still reads as no-meta (unchanged)', (t) => { |
| 48 | t.is(storeWith(null).getMeta(HASH), null) |
| 49 | }) |
| 50 | |
| 51 | test('a VALID meta file still round-trips — the guard must not swallow good data', (t) => { |
| 52 | const s = storeWith(JSON.stringify({ pulledAt: 123, source: 'x', pinned: true })) |
| 53 | const m = s.getMeta(HASH) |
| 54 | t.is(m.pulledAt, 123) |
| 55 | t.is(m.pinned, true, 'pinned survives') |
| 56 | }) |
| 57 | |
| 58 | // The payoff: pin() already tolerates a null meta, so a corrupt entry HEALS on the next pin |
| 59 | // instead of needing a hand-edit on the box. |
| 60 | test('pin() repairs a corrupt meta rather than failing', (t) => { |
| 61 | const s = storeWith('') |
| 62 | s.pin(HASH) |
| 63 | const m = s.getMeta(HASH) |
| 64 | t.ok(m, 'meta is readable again') |
| 65 | t.is(m.pinned, true, 'and the pin actually took — this is what #24 could not do') |
| 66 | }) |