diff --git a/src/app/stores/DatasetStore.test.ts b/src/app/stores/DatasetStore.test.ts index a9297a8..4c19377 100644 --- a/src/app/stores/DatasetStore.test.ts +++ b/src/app/stores/DatasetStore.test.ts @@ -112,6 +112,21 @@ describe('URL dataset edits & refresh', () => { expect(ds.name).toBe('Remote'); // preserved }); + test('a URL→inline conversion removes the url/fetchedAt keys (not left undefined)', () => { + const id = seedUrl(); + store().startEdit(); + store().updateForm({ source: 'inline', input: '[{"x":1},{"x":2}]' }); + const later = new Date('2026-07-01T00:00:00Z'); + expect(store().save(later)).toBe(true); + const ds = store().datasets.find((d) => d.id === id)!; + expect(ds.source).toBe('inline'); + expect(ds.rowCount).toBe(2); + // The remote origin is shed entirely — the keys are gone, not present-but-undefined, + // so an inline record carries no stale url/fetchedAt. + expect('url' in ds).toBe(false); + expect('fetchedAt' in ds).toBe(false); + }); + test('refreshDataset is a no-op for an inline dataset', () => { store().add( createDataset({ name: 'Inline', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }), diff --git a/src/app/stores/DatasetStore.ts b/src/app/stores/DatasetStore.ts index 02de3c5..7ef75bf 100644 --- a/src/app/stores/DatasetStore.ts +++ b/src/app/stores/DatasetStore.ts @@ -291,11 +291,9 @@ export const useDatasetStore = create((set, get) => ({ data: resolved.data, format: resolved.format, source: resolved.source, - // TODO: `update` shallow-merges, so these set the keys to `undefined` - // rather than removing them — a URL→inline record keeps `url`/`fetchedAt` - // present-but-undefined. Invisible today (rendering/profiling key off - // `source`/`data == null`, and JSON export drops undefined), but it breaks - // the "inline records carry no url/fetchedAt keys" invariant on this path. + // A URL→inline conversion must shed the remote origin; `update` deletes + // keys set to `undefined`, so these are removed rather than left + // present-but-undefined. url: undefined, fetchedAt: undefined, comment: form.comment, @@ -415,7 +413,18 @@ export const useDatasetStore = create((set, get) => ({ update: (id, patch, now) => { const modified = patch.modified ?? (now ?? new Date()).toISOString(); set((s) => ({ - datasets: s.datasets.map((d) => (d.id === id ? { ...d, ...patch, modified } : d)), + datasets: s.datasets.map((d) => { + if (d.id !== id) return d; + const merged = { ...d, ...patch, modified }; + // A patch key set to `undefined` removes it, rather than leaving a + // present-but-undefined key — so a URL→inline conversion drops `url`/ + // `fetchedAt` cleanly, keeping the "inline records carry no remote-origin + // keys" invariant (docs/architecture/07 §6). + for (const key of Object.keys(patch) as (keyof Dataset)[]) { + if (patch[key] === undefined) delete merged[key]; + } + return merged; + }), })); },