diff --git a/apps/console/src/__tests__/MSWServer.test.tsx b/apps/console/src/__tests__/MSWServer.test.tsx index 607e53b9e..09d01d2dc 100644 --- a/apps/console/src/__tests__/MSWServer.test.tsx +++ b/apps/console/src/__tests__/MSWServer.test.tsx @@ -95,4 +95,35 @@ describe('MSW Server Integration', () => { expect(record.priority).toBe('high'); } }); + + // ── Stable seed-data IDs ────────────────────────────────────────────── + // Seed records carry an explicit `_id`. After kernel bootstrap and + // syncDriverIds(), `id` should equal the seed `_id`, NOT a random + // driver-generated value. This ensures URLs with record IDs remain + // valid across page refreshes. + + it('should preserve seed _id as canonical id (stable across refreshes)', async () => { + const driver = getDriver(); + const opportunities = await driver!.find('opportunity', { object: 'opportunity' }); + expect(opportunities.length).toBeGreaterThan(0); + + // Seed data defines _id "101" for the first opportunity. + // After syncDriverIds, id must equal _id (both "101"). + const targetOpportunity = opportunities.find((r: any) => r._id === '101'); + expect(targetOpportunity).toBeDefined(); + expect(targetOpportunity.id).toBe('101'); + expect(targetOpportunity._id).toBe('101'); + }); + + it('should fetch a seed record by _id via HTTP', async () => { + // GET /data/opportunity/101 — uses the stable seed _id. + // Response may be wrapped in { success, data: { record } } (HttpDispatcher) + // or returned as { record } (direct protocol). + const res = await fetch('http://localhost/api/v1/data/opportunity/101'); + expect(res.ok).toBe(true); + const body = await res.json(); + const record = body.data?.record ?? body.record; + expect(record).toBeDefined(); + expect(record.name).toBe('ObjectStack Enterprise License'); + }); }); diff --git a/apps/console/src/mocks/createKernel.ts b/apps/console/src/mocks/createKernel.ts index 1fa636e51..fbb03c7cb 100644 --- a/apps/console/src/mocks/createKernel.ts +++ b/apps/console/src/mocks/createKernel.ts @@ -102,14 +102,23 @@ async function installBrokerShim(kernel: ObjectKernel): Promise { * generated identity as `id`. Seed data may also carry its own * `_id` that differs from the driver-assigned `id`. * - * This helper ensures every record has `_id === id` so that - * protocol lookups via `_id` match the driver-assigned `id`. + * When seed data provides an explicit `_id`, that value is promoted + * to `id` so that record identifiers remain stable across page + * refreshes (the driver would otherwise generate a new timestamp-based + * `id` every time the in-memory kernel reboots). + * + * When no explicit `_id` exists, `_id` is derived from the + * driver-assigned `id` so protocol lookups still work. */ function syncDriverIds(driver: InMemoryDriver): void { const db = (driver as any).db as Record; for (const records of Object.values(db)) { for (const record of records) { - if (record.id) { + if (record._id != null && record._id !== record.id) { + // Seed data carries an explicit _id → promote it to canonical id + record.id = record._id; + } else if (record.id) { + // No explicit seed _id → derive _id from driver-assigned id record._id = record.id; } } diff --git a/packages/data-objectstack/src/index.ts b/packages/data-objectstack/src/index.ts index e1b2831cc..871a8fd1b 100644 --- a/packages/data-objectstack/src/index.ts +++ b/packages/data-objectstack/src/index.ts @@ -289,6 +289,10 @@ export class ObjectStackAdapter implements DataSource { $top: 1, }; const result = await this.rawFindWithPopulate(resource, findParams); + // Handle array responses (some servers return data as flat arrays) + if (Array.isArray(result)) { + return result[0] || null; + } const resultObj = result as { records?: T[]; value?: T[] }; const records = resultObj.records || resultObj.value || []; return records[0] || null;