From 7648eb7cca16da82928ec2c042398c06e2f94aca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 06:44:06 +0000 Subject: [PATCH 1/4] fix(cloud-connection): an install-local reseed over an intact baseline answers success with the skipped count A reseed whose every declared seed record is already present wrote nothing because there was nothing to write, and answered 422 RESEED_NO_ROWS "The package declares no seedable records for this runtime" over a package that declares them. It now answers 200 with inserted, updated, skipped and errors; RESEED_NO_ROWS stays for a run whose records errored, and, with its text, for a run in which the loader processed no record for this runtime. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude --- .../src/marketplace-install-local-plugin.ts | 25 +++++- .../marketplace-install-local-reseed.test.ts | 87 ++++++++++++++----- 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/packages/cloud-connection/src/marketplace-install-local-plugin.ts b/packages/cloud-connection/src/marketplace-install-local-plugin.ts index 50a71c2f32..53f51a7330 100644 --- a/packages/cloud-connection/src/marketplace-install-local-plugin.ts +++ b/packages/cloud-connection/src/marketplace-install-local-plugin.ts @@ -1843,6 +1843,14 @@ export class MarketplaceInstallLocalPlugin implements Plugin { * • A purge was undone * • The user wants a clean baseline back after editing demo rows * + * A success answers the loader's four counts — `inserted`, `updated`, + * `skipped`, `errors` — so the caller sees why nothing changed. A run over + * an intact baseline (every declared record already present, so all of + * them `skipped`) is a success with `inserted: 0`: being idempotent, the + * reseed has reached the state it exists to reach. `422 RESEED_NO_ROWS` is + * kept for a run that wrote nothing because records failed (`errors`), or + * because the loader had no record to process for this runtime. + * * Multi-tenant: requires an active organization on the session (same * rule as install seed path). A walled session with none is refused with * ADR-0123 D2 / D4's answer ({@link noActiveOrganizationRefusal}); the @@ -1888,16 +1896,25 @@ export class MarketplaceInstallLocalPlugin implements Plugin { const inserted = summary.seeded.inserted ?? 0; const updated = summary.seeded.updated ?? 0; + const skipped = summary.seeded.skipped ?? 0; const errors = summary.seeded.errors ?? 0; const wrote = inserted + updated > 0; + // The loader reconciles `inserted + updated + skipped + errored` + // against the records it processes for this runtime (`SeedLoadResult` + // in `@objectstack/spec/data`), so a clean run with nothing written and + // `skipped > 0` found every one of them already present: the intact + // baseline, which is a success. With `skipped` at 0 as well, it + // processed no record at all. + const intactBaseline = !wrote && errors === 0 && skipped > 0; // HONEST RESULT: the loader runs row-by-row and counts write failures // (locked DB, missing table, validation reject) into `errors` rather // than throwing. Previously this handler returned success — and flipped // `withSampleData` to true — even when every row failed, so the UI said // "done" while the database stayed empty. Treat a run that landed no - // rows as a failure and report why. - if (!wrote) { + // rows as a failure and report why — unless every row it would have + // written is already there. + if (!wrote && !intactBaseline) { return c.json({ success: false, error: { @@ -1910,7 +1927,8 @@ export class MarketplaceInstallLocalPlugin implements Plugin { }, 422); } - // Only mark the install as carrying sample data once rows actually landed. + // Only mark the install as carrying sample data once its rows are + // there: landed by this run, or found already present by it. try { entry.withSampleData = true; entry.sampleDataPurged = false; @@ -1923,6 +1941,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin { manifestId, inserted, updated, + skipped, errors, withSampleData: true, }, diff --git a/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts b/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts index 7b32928150..154e4a7e08 100644 --- a/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts @@ -11,6 +11,13 @@ * stayed empty (the "提示成功但没有数据" bug). These tests pin the corrected * behaviour: no rows written => failure + flag stays false; rows written => * success + flag flips. + * + * …and the converse: a reseed over an INTACT baseline — every declared record + * already present, so the loader skips all of them — wrote nothing because + * there was nothing to write. It used to answer `422 RESEED_NO_ROWS` "The + * package declares no seedable records for this runtime" over a package that + * declares 28. It is a success carrying `skipped`, and that refusal text is + * kept for the run that really processed no record. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; @@ -19,8 +26,8 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; // Controls what the (mocked) seed loader reports back. The handler under test -// only cares about result.summary.total{Inserted,Updated} + result.errors. -let seedResult: any = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] }; +// only cares about result.summary.total{Inserted,Updated,Skipped} + result.errors. +let seedResult: any = { summary: { totalInserted: 0, totalUpdated: 0, totalSkipped: 0 }, errors: [] }; vi.mock('@objectstack/runtime', () => ({ SeedLoaderService: class { @@ -100,7 +107,7 @@ const MANIFEST = { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'mil-reseed-')); - seedResult = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] }; + seedResult = { summary: { totalInserted: 0, totalUpdated: 0, totalSkipped: 0 }, errors: [] }; }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); vi.restoreAllMocks(); }); @@ -118,6 +125,10 @@ async function installAndGetRoutes() { return rawApp; } +const RESEED = 'POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data'; +/** The ledger's install-wide record of sample data, read from the ledger itself. */ +const recordedWithSampleData = () => new LocalManifestSource(dir).read('app.test.proj').entry?.withSampleData; + describe('reseed honest result', () => { it('FAILS (422) when the seed run wrote zero rows but errored', async () => { const rawApp = await installAndGetRoutes(); @@ -125,55 +136,85 @@ describe('reseed honest result', () => { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [{ message: 'database is locked' }, { message: 'database is locked' }], }; - const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!( - makeC({}, 'app.test.proj'), - ); + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); expect(res.status).toBe(422); expect(res.payload?.success).toBe(false); expect(res.payload?.error?.code).toBe('RESEED_NO_ROWS'); // The real failure reason is surfaced, not swallowed. expect(res.payload?.error?.message).toContain('database is locked'); - expect(res.payload?.error?.details).toMatchObject({ inserted: 0, updated: 0, errors: 2 }); + expect(res.payload?.error?.details).toEqual({ inserted: 0, updated: 0, errors: 2 }); + expect(recordedWithSampleData()).toBe(false); }); - it('FAILS (422) when the package seeds nothing (0 rows, 0 errors)', async () => { + it('FAILS (422) unchanged when records errored beside skipped ones and nothing was written', async () => { const rawApp = await installAndGetRoutes(); - seedResult = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] }; - const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!( - makeC({}, 'app.test.proj'), - ); + seedResult = { + summary: { totalInserted: 0, totalUpdated: 0, totalSkipped: 1 }, + errors: [{ message: 'one row rejected' }], + }; + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); expect(res.status).toBe(422); + expect(res.payload?.success).toBe(false); expect(res.payload?.error?.code).toBe('RESEED_NO_ROWS'); + expect(res.payload?.error?.message).toContain('one row rejected'); + expect(res.payload?.error?.details).toEqual({ inserted: 0, updated: 0, errors: 1 }); + expect(recordedWithSampleData()).toBe(false); }); - it('SUCCEEDS and flips withSampleData when rows actually land', async () => { + it('FAILS (422) with its own text when the loader processed no record for this runtime (0 rows, 0 skipped, 0 errors)', async () => { const rawApp = await installAndGetRoutes(); - seedResult = { summary: { totalInserted: 2, totalUpdated: 0 }, errors: [] }; - const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!( - makeC({}, 'app.test.proj'), + seedResult = { summary: { totalInserted: 0, totalUpdated: 0, totalSkipped: 0 }, errors: [] }; + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); + expect(res.status).toBe(422); + expect(res.payload?.success).toBe(false); + expect(res.payload?.error?.code).toBe('RESEED_NO_ROWS'); + expect(res.payload?.error?.message).toBe( + 'Reseed wrote no rows. The package declares no seedable records for this runtime.', ); + expect(res.payload?.error?.details).toEqual({ inserted: 0, updated: 0, errors: 0 }); + expect(recordedWithSampleData()).toBe(false); + }); + + it('SUCCEEDS (200) over an intact baseline: every declared record already present, all of them skipped', async () => { + const rawApp = await installAndGetRoutes(); + expect(recordedWithSampleData()).toBe(false); + seedResult = { summary: { totalInserted: 0, totalUpdated: 0, totalSkipped: 2 }, errors: [] }; + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); + expect(res.status).toBe(200); + expect(res.payload).toEqual({ + success: true, + data: { manifestId: 'app.test.proj', inserted: 0, updated: 0, skipped: 2, errors: 0, withSampleData: true }, + }); + // The rows are there, so the install-wide record says so. + expect(recordedWithSampleData()).toBe(true); + }); + + it('SUCCEEDS and flips withSampleData when rows actually land', async () => { + const rawApp = await installAndGetRoutes(); + seedResult = { summary: { totalInserted: 2, totalUpdated: 0, totalSkipped: 0 }, errors: [] }; + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); expect(res.status).toBe(200); expect(res.payload?.success).toBe(true); - expect(res.payload?.data).toMatchObject({ inserted: 2, updated: 0, withSampleData: true }); + expect(res.payload?.data).toEqual({ + manifestId: 'app.test.proj', inserted: 2, updated: 0, skipped: 0, errors: 0, withSampleData: true, + }); // The ledger's install-time record flips. Read from the ledger itself: // the GET listing no longer serves this record — it answers from the // caller's own rows (#21775), and the seed loader here is a stub that // writes none. - expect(new LocalManifestSource(dir).read('app.test.proj').entry?.withSampleData).toBe(true); + expect(recordedWithSampleData()).toBe(true); }); it('partial success (some rows + some errors) still reports the error count', async () => { const rawApp = await installAndGetRoutes(); seedResult = { - summary: { totalInserted: 1, totalUpdated: 0 }, + summary: { totalInserted: 1, totalUpdated: 0, totalSkipped: 1 }, errors: [{ message: 'one row rejected' }], }; - const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!( - makeC({}, 'app.test.proj'), - ); + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.proj')); expect(res.status).toBe(200); expect(res.payload?.success).toBe(true); - expect(res.payload?.data?.errors).toBe(1); + expect(res.payload?.data).toMatchObject({ inserted: 1, skipped: 1, errors: 1 }); }); }); From 8836cfc61a95968f07b7ec5be2edb748abe22013 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 06:45:24 +0000 Subject: [PATCH 2/4] test(dogfood): pin the install-local reseed over an intact baseline, after a purge, and with no record for this runtime Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude --- .../marketplace-install-local-reseed.test.ts | 14 ++ ...cal-reseed-intact-baseline.dogfood.test.ts | 217 ++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts diff --git a/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts b/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts index 154e4a7e08..5adb185ed0 100644 --- a/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-reseed.test.ts @@ -175,6 +175,20 @@ describe('reseed honest result', () => { expect(recordedWithSampleData()).toBe(false); }); + it('a package with no seed dataset at all never reaches the loader: 400 RESEED_SKIPPED, unchanged', async () => { + const rawApp = await installAndGetRoutes(); + const { data: _none, ...noData } = MANIFEST; + const installRes = await rawApp.routes.get('POST /api/v1/marketplace/install-local')!( + makeC({ manifest: { ...noData, id: 'app.test.nodata' } }), + ); + expect(installRes.payload?.success).toBe(true); + const res = await rawApp.routes.get(RESEED)!(makeC({}, 'app.test.nodata')); + expect(res.status).toBe(400); + expect(res.payload?.success).toBe(false); + expect(res.payload?.error?.code).toBe('RESEED_SKIPPED'); + expect(res.payload?.error?.message).toContain('no-datasets'); + }); + it('SUCCEEDS (200) over an intact baseline: every declared record already present, all of them skipped', async () => { const rawApp = await installAndGetRoutes(); expect(recordedWithSampleData()).toBe(false); diff --git a/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts b/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts new file mode 100644 index 0000000000..37a71abdb8 --- /dev/null +++ b/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts @@ -0,0 +1,217 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// GOLDEN REGRESSION — an install-local reseed over an INTACT baseline is a +// success that says why nothing changed, not a refusal naming a false cause +// (#21776). +// +// ## What was measured before the fix +// +// The showcase booted, the CRM example installed from its built artifact (its +// 28 seed rows landed), then, as the admin, with every one of them in place: +// +// POST …/install-local/com.example.crm/reseed-sample-data {} +// -> 422 RESEED_NO_ROWS "Reseed wrote no rows. The package declares no +// seedable records for this runtime." +// +// The package declares 28 seed records; the loader skipped all 28 as already +// present. The handler counted only `inserted + updated`, so an idempotent +// reseed over the state it exists to reach was reported as a broken package. +// +// ## What this file pins, on a real boot +// +// 1. over the intact baseline the reseed answers 200 with +// `{ inserted: 0, updated: 0, skipped: 28, errors: 0 }`, and no row moved; +// 2. after a purge the reseed answers 200 with `inserted: 28`; +// 3. a package whose seed datasets all apply to another environment — the +// loader has no record to process for this runtime — still answers +// `422 RESEED_NO_ROWS` with its existing text, which is then true. +// +// Boots fixture stacks of its own (an extra plugin, a ledger directory), so it +// stays out of `SHARED_SHOWCASE`. + +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import showcaseStack from '@objectstack/example-showcase'; +import crmStack from '@objectstack/example-crm'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { MarketplaceInstallLocalPlugin } from '@objectstack/cloud-connection'; +import type { IObjectQLEngine } from '@objectstack/spec/contracts'; +import type { ExecutionContext } from '@objectstack/spec/kernel'; +import { buildShapedArtifact } from './build-shaped-artifact.js'; + +const SYS = { isSystem: true } as ExecutionContext; +const CRM = 'com.example.crm'; +const BASE = '/marketplace/install-local'; +/** The CRM example's seeded objects and their seed-row counts (3 + 3 + 12 + 5 + 5). */ +const SEEDED: Record = { + crm_account: 3, crm_contact: 3, crm_opportunity: 12, crm_lead: 5, crm_activity: 5, +}; +const SEED_TOTAL = Object.values(SEEDED).reduce((a, b) => a + b, 0); +const EMPTY = Object.fromEntries(Object.keys(SEEDED).map((o) => [o, 0])); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const rowsOf = (r: any): any[] => (Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : []); + +type Dataset = { object: string; records: unknown[]; env?: string[] }; + +/** + * The install body `os package install .json` sends: the whole + * artifact, with the manifest's id and version lifted to the top level + * (`packages/cli/src/commands/package/install.ts`). `mapData` rewrites the + * artifact's seed datasets. + */ +function installBody(mapData: (d: Dataset) => Dataset = (d) => d): { manifest: Record } { + const { artifact } = buildShapedArtifact(crmStack as unknown as Record); + const manifest = artifact.manifest as { id?: string; version?: string }; + const data = (artifact.data as Dataset[]).map(mapData); + return { manifest: { ...artifact, data, id: manifest.id, version: manifest.version } }; +} + +/** Seed-row counts per CRM object, read as the system. */ +async function countSeeded(ql: IObjectQLEngine): Promise> { + const out: Record = {}; + for (const object of Object.keys(SEEDED)) out[object] = rowsOf(await ql.find(object, { context: SYS })).length; + return out; +} + +/** `object#id@updated_at` for every seed-object row: a reseed that rewrote a row moves its stamp. */ +async function rowStamps(ql: IObjectQLEngine): Promise { + const stamps: string[] = []; + for (const object of Object.keys(SEEDED)) { + for (const row of rowsOf(await ql.find(object, { context: SYS }))) { + stamps.push(`${object}#${row.id}@${String(row.updated_at)}`); + } + } + return stamps.sort(); +} + +async function json(res: Response): Promise<{ status: number; body: any }> { // eslint-disable-line @typescript-eslint/no-explicit-any + return { status: res.status, body: await res.json().catch(() => null) }; +} + +describe('dogfood: install-local reseed over an intact baseline, then after a purge', () => { + let stack: VerifyStack; + let storageDir: string; + let ql: IObjectQLEngine; + let install: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let intact: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let purge: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let afterPurge: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let afterInstall: Record; + let stampsBefore: string[]; + let stampsAfterIntact: string[]; + let afterIntactCounts: Record; + let purgedCounts: Record; + let reseededCounts: Record; + + beforeAll(async () => { + storageDir = mkdtempSync(join(tmpdir(), 'dogfood-install-local-reseed-intact-')); + stack = await bootStack(showcaseStack, { + extraPlugins: [new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir })], + }); + ql = stack.kernel.getService('objectql'); + const token = await stack.signIn(); + + install = await json(await stack.apiAs(token, 'POST', BASE, installBody())); + afterInstall = await countSeeded(ql); + stampsBefore = await rowStamps(ql); + + intact = await json(await stack.apiAs(token, 'POST', `${BASE}/${CRM}/reseed-sample-data`, {})); + afterIntactCounts = await countSeeded(ql); + stampsAfterIntact = await rowStamps(ql); + + purge = await json(await stack.apiAs(token, 'POST', `${BASE}/${CRM}/purge-sample-data`, {})); + purgedCounts = await countSeeded(ql); + + afterPurge = await json(await stack.apiAs(token, 'POST', `${BASE}/${CRM}/reseed-sample-data`, {})); + reseededCounts = await countSeeded(ql); + }, 300_000); + + afterAll(async () => { + await stack?.stop?.(); + if (storageDir) rmSync(storageDir, { recursive: true, force: true }); + }); + + it('PRECONDITION: the install landed all 28 seed rows', () => { + expect(install.status, JSON.stringify(install.body)).toBe(200); + expect(install.body?.data?.seeded).toMatchObject({ mode: 'inline', inserted: SEED_TOTAL }); + expect(afterInstall).toEqual(SEEDED); + expect(stampsBefore).toHaveLength(SEED_TOTAL); + }); + + it('over the intact baseline the reseed answers 200 { inserted: 0, skipped: 28 } and no row moved', () => { + expect(intact.status, JSON.stringify(intact.body)).toBe(200); + expect(intact.body).toEqual({ + success: true, + data: { manifestId: CRM, inserted: 0, updated: 0, skipped: SEED_TOTAL, errors: 0, withSampleData: true }, + }); + expect(afterIntactCounts).toEqual(SEEDED); + expect(stampsAfterIntact).toEqual(stampsBefore); + }); + + it('after a purge the reseed answers 200 with inserted: 28', () => { + expect(purge.status, JSON.stringify(purge.body)).toBe(200); + expect(purge.body?.data).toMatchObject({ deleted: SEED_TOTAL, errors: 0 }); + expect(purgedCounts).toEqual(EMPTY); + + expect(afterPurge.status, JSON.stringify(afterPurge.body)).toBe(200); + expect(afterPurge.body).toEqual({ + success: true, + data: { manifestId: CRM, inserted: SEED_TOTAL, updated: 0, skipped: 0, errors: 0, withSampleData: true }, + }); + expect(reseededCounts).toEqual(SEEDED); + }); +}); + +describe('dogfood: install-local reseed of a package with no seed record for this runtime', () => { + /** Every seed environment except the one vitest runs in (`NODE_ENV=test`). */ + const ELSEWHERE = ['prod', 'dev']; + let stack: VerifyStack; + let storageDir: string; + let ql: IObjectQLEngine; + let body: { manifest: Record }; + let install: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let reseed: { status: number; body: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + let counts: Record; + + beforeAll(async () => { + storageDir = mkdtempSync(join(tmpdir(), 'dogfood-install-local-reseed-none-')); + stack = await bootStack(showcaseStack, { + extraPlugins: [new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir })], + }); + ql = stack.kernel.getService('objectql'); + const token = await stack.signIn(); + + body = installBody((d) => ({ ...d, env: ELSEWHERE })); + install = await json(await stack.apiAs(token, 'POST', BASE, body)); + reseed = await json(await stack.apiAs(token, 'POST', `${BASE}/${CRM}/reseed-sample-data`, {})); + counts = await countSeeded(ql); + }, 300_000); + + afterAll(async () => { + await stack?.stop?.(); + if (storageDir) rmSync(storageDir, { recursive: true, force: true }); + }); + + it('PRECONDITION: the package declares its 28 records, every dataset for another environment', () => { + expect(process.env.NODE_ENV).toBe('test'); + const data = body.manifest.data as Dataset[]; + expect(data.reduce((n, d) => n + d.records.length, 0)).toBe(SEED_TOTAL); + expect(data.every((d) => !d.env?.includes('test'))).toBe(true); + expect(install.status, JSON.stringify(install.body)).toBe(200); + expect(install.body?.data?.seeded).toMatchObject({ mode: 'inline', inserted: 0, updated: 0, skipped: 0, errors: 0 }); + }); + + it('the reseed answers 422 RESEED_NO_ROWS with its existing text, and writes nothing', () => { + expect(reseed.status, JSON.stringify(reseed.body)).toBe(422); + expect(reseed.body?.success).toBe(false); + expect(reseed.body?.error?.code).toBe('RESEED_NO_ROWS'); + expect(reseed.body?.error?.message).toBe( + 'Reseed wrote no rows. The package declares no seedable records for this runtime.', + ); + expect(counts).toEqual(EMPTY); + }); +}); From dbf5e0370e7559a62260e18ffb2589831f2fd7e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 06:49:05 +0000 Subject: [PATCH 3/4] test(dogfood): the no-record-for-this-runtime pin scopes its datasets away from bootStack's own environment Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude --- ...tall-local-reseed-intact-baseline.dogfood.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts b/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts index 37a71abdb8..6df75b41cd 100644 --- a/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts +++ b/packages/qa/dogfood/test/install-local-reseed-intact-baseline.dogfood.test.ts @@ -167,8 +167,12 @@ describe('dogfood: install-local reseed over an intact baseline, then after a pu }); describe('dogfood: install-local reseed of a package with no seed record for this runtime', () => { - /** Every seed environment except the one vitest runs in (`NODE_ENV=test`). */ - const ELSEWHERE = ['prod', 'dev']; + /** + * Every seed environment except this runtime's: `bootStack` forces + * `NODE_ENV=development` (`packages/verify/src/harness.ts`), which the seed + * loader reads as `dev`. + */ + const ELSEWHERE = ['prod', 'test']; let stack: VerifyStack; let storageDir: string; let ql: IObjectQLEngine; @@ -197,10 +201,10 @@ describe('dogfood: install-local reseed of a package with no seed record for thi }); it('PRECONDITION: the package declares its 28 records, every dataset for another environment', () => { - expect(process.env.NODE_ENV).toBe('test'); + expect(process.env.NODE_ENV).toBe('development'); const data = body.manifest.data as Dataset[]; expect(data.reduce((n, d) => n + d.records.length, 0)).toBe(SEED_TOTAL); - expect(data.every((d) => !d.env?.includes('test'))).toBe(true); + expect(data.every((d) => !d.env?.includes('dev'))).toBe(true); expect(install.status, JSON.stringify(install.body)).toBe(200); expect(install.body?.data?.seeded).toMatchObject({ mode: 'inline', inserted: 0, updated: 0, skipped: 0, errors: 0 }); }); From 278fd339cf332d622a10a04f702f5b8fe9cdbde4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 06:52:58 +0000 Subject: [PATCH 4/4] chore(changeset): cloud-connection minor for the reseed over an intact baseline Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude --- .changeset/21776-reseed-intact-baseline.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/21776-reseed-intact-baseline.md diff --git a/.changeset/21776-reseed-intact-baseline.md b/.changeset/21776-reseed-intact-baseline.md new file mode 100644 index 0000000000..3ed3bbb7d7 --- /dev/null +++ b/.changeset/21776-reseed-intact-baseline.md @@ -0,0 +1,11 @@ +--- +"@objectstack/cloud-connection": minor +--- + +An install-local reseed over sample rows that are all still in place answers success, with the loader's `skipped` count, instead of a refusal naming a false cause (#21776). + +Clause-②: yes (widening) + +- **Intact baseline.** `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`, run while every seed record the package declares is already present, answers `200 { success: true, data: { manifestId, inserted: 0, updated: 0, skipped: N, errors: 0, withSampleData: true } }`. Before, it answered `422 RESEED_NO_ROWS`, "Reseed wrote no rows. The package declares no seedable records for this runtime.", over a package that declares them. The reseed is idempotent, so a run that finds every row in place has reached its goal. The install's record of sample data is set the same way as when rows land. +- **`skipped` on every success.** A successful reseed now answers all four of the loader's counts: `inserted`, `updated`, `skipped` and `errors`. Before, `skipped` was not in the response. +- **Unchanged refusals.** `422 RESEED_NO_ROWS` still answers a run that wrote nothing because records failed, with the error count and the first error, and its `details` are still `{ inserted, updated, errors }`. It also still answers, with the same text, a run in which the loader had no record to process for this runtime: for example, every dataset is scoped to another environment (`Seed.env`). That text now states a true cause. A package with no seed dataset at all still answers `400 RESEED_SKIPPED` (`no-datasets`).