Skip to content

Commit 210ef60

Browse files
feat(cloud-connection): install-local's reseed and sample-data count reach an unmanaged copy, and retry a failed first load (#31425) (#31525)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTVkGrf2CjQTF79hBERgqiJ)_ domain:server#5 · os-project-manager · session_01FEkQ5ktJb5rQ5n2GUXSQMR Fixes #31425 Clause-②: yes (widening) — `POST /api/v1/marketplace/install-local/:id/reseed-sample-data` and `GET …/:id/sample-data` now serve the base of an unmanaged copy this door recorded, which they answered `404` before; nothing accepted today is refused; one contract-review-tier review owed before the queue Metadata-protocol: none Skills-docs: one move — `content/docs/deployment/marketplace-handbook.mdx` (step 6, Sample data) states that the doors act on a copy by its base id and what uninstalling a copy does to its rows; no `skills/**` page teaches these doors. ## 维护者速读 - 改了什么:以"无托管副本"方式安装并带示例数据的应用,现在可以用副本的 base id 调"重新灌示例数据"(reseed)和"示例数据计数"(count)两个入口。两扇门和"清空示例数据"(purge)读同一份种子收据,用的是同一个读取函数。第一次灌数据失败(`seed-receipt-unread` / `seed-error`)后,也可以用 reseed 从存储的收据重试,不再只有再装一次换来的 409。 - 为什么改:副本没有安装台账条目,所以以前 reseed 和 count 对副本一律回 404,第一次灌数据失败后也没有办法补救。 - 风险与回滚:只放宽。按副本记录判定,只有本入口自己复制出来的 base 才能被这两扇门访问,其它 base 照旧 404(包括 Studio 建的、带种子项的 base)。收据读不出来时回 503,不会当成"没有示例数据"。卸载副本的行为不变:默认删表,`keepData` 时保留所有行。没有新增台账条目,也没有第二个收据读取者。回滚即还原本 PR。 - 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 中的 `handleReseed` 和 `handleSampleDataCount` 的副本分支、`unmanagedCopySeedDatasets`(原 purge 的读取函数改名)、`seedUnmanagedCopy`、`unreadableCopyReceipt`。 - 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run src/marketplace-install-local-install-modes.test.ts`,预期 63 个用例全绿,其中 #31425 新增 7 个。 - 需要你做的:无。两个前提问题已写入报告的 `open_questions`,席位已同意本 PR 的处理方式。 ## What changes `packages/cloud-connection/src/marketplace-install-local-plugin.ts`: 1. **One receipt reader, three doors.** The purge's read of a copy's seed receipt is renamed `unmanagedCopySeedDatasets`, and the reseed and the count now ask it as well. Its checks are unchanged: an installed writable base, this door's record of the copy, the record tied to the base row in the package store, and the receipt's `seed` items read through the protocol. There is no new ledger entry and no second reader. 2. **Reseed of a copy** (no ledger entry): - `404` for any base this door did not record; - `400 RESEED_SKIPPED` for a copy made without the sample data; - `503 SERVICE_UNAVAILABLE` for a receipt that could not be read; - otherwise a run of the datasets through `seedUnmanagedCopy`, the install's own copy seed run (the walled-organization rule, `runInlineSeed`, rows owned by the caller), answered by `reseedAnswer`. That is the ledger reseed's shaping, factored out: the same four counts, the intact-baseline success, `RESEED_NO_ROWS`. A copy has no ledger flags to write. 3. **Retry.** A copy whose first load at install failed (`seed-receipt-unread`, `seed-error`) is still recorded with `seeded: true`, so the reseed loads it from the stored receipt. 4. **Count of a copy.** `404` as above; `count: 0` for a copy made without the sample data; the purge's `503` for an unread receipt; otherwise the purge's match over the receipt's datasets. 5. **`unreadableCopyReceipt`** is the purge's existing 503 answer, shared by the three doors: the door's record file to repair, or "retry once the stores answer". The store's text goes to the log only. 6. **Doc comments.** These now say what the doors do for a copy, what uninstalling a copy does, and what remains about the store's `null`: - the module header (`seedSampleData`); - `handleReseed` and `handleSampleDataCount`; - `unmanagedCopyInstance`; - `marketplace-install-local-unmanaged-copies.ts`. `content/docs/deployment/marketplace-handbook.mdx`, step 6 (Sample data): one paragraph. ## The four halves of the card ### 1. Uninstalling a copy — keep the rows, no code change The card offers "purges its sample rows (as a managed uninstall's cleanup does)" or "keeps them and says so in the answer". Measured first: - **A managed install-local uninstall keeps every row.** Its docblock says "Its tables and rows are untouched", and the handbook says "Records stored in the app's objects stay in the database". So the card's parenthetical does not hold: there is no managed cleanup that purges sample rows. - **A copy is uninstalled through `DELETE /api/v1/packages/:baseId`** (`deletePackage`). This door has no uninstall for a copy: its `DELETE …/install-local/:id` answers `404` without a ledger entry. Measured on a real boot (showcase composition, the CRM example as the copy with its sample data, a throwaway probe that is not committed): ``` install 200 {"mode":"inline","inserted":28,…} after install {"crm_account":4,"crm_contact":3,"crm_opportunity":12,"crm_lead":5,"crm_activity":5} — default — uninstall 200 …"deletedCount":30… purge after uninstall 404 reinstall without sample data 200 after reinstall {"crm_account":0,"crm_contact":0,"crm_opportunity":0,"crm_lead":0,"crm_activity":0} — ?keepData=true — uninstall 200 …"deletedCount":30… purge after uninstall 404 reinstall without sample data 200 after reinstall {"crm_account":4,"crm_contact":3,"crm_opportunity":12,"crm_lead":5,"crm_activity":5} crm_account names ["Acme Corp","Globex Ltd","Initech","User Authored Co"] purge of the reinstall 400 NOTHING_TO_PURGE ``` So by default the copy's tables go with every row in them, the sample rows included. The rows outlive the uninstall only under `keepData: true`, which is the caller asking to keep every row. Four axes: - **Real need.** The measured default already leaves no sample row behind. The only outliving case is one the caller chose. - **Long term.** One uninstall semantics across both doors, with no hidden data delete: a managed uninstall keeps rows, and a copy follows the verb's declared `keepData`. - **AI-safety.** A purge hidden inside an uninstall that was asked to keep data is exactly the silent behaviour the axis warns against. - **Startup focus.** No new surface. The seat agreed to keep the rows, with no field added to `deletePackage`'s answer in this PR. The handbook now says so and says to purge first. ### 2. Reseed and the count reach a recorded copy through the same receipt — done See *What changes*. There is no new ledger entry and no second receipt reader. ### 3. A failed first load can be retried — done Both failure reasons are pinned: the receipt unreadable at install, and the seed run throwing. In each case the reseed then loads 3 rows. ### 4. Seat 3's scope addition: the store's `null` at install — measured and stated The package store's `get` catches an ordinary driver error, logs it and answers `null` (`service-package`, `get`). Right after the import, the door therefore cannot tell a passing error from "no row". The #30808 pin measures this on the current tree: with an ordinary fault injected at install, the answer carries `copyRecord: { written: false, … }`, no record is written, and the purge answers 404. The reseed and the count now go through the same reader, so they answer 404 for that copy too. Re-recording later, as the scope addition asks, needs the base row and the receipt to prove the base is a copy. They do not: the import stores no marker on the base manifest (#30808's own docblock), and a Studio-authored base with published seed items reads the same. A re-record from them would be a re-record on an id alone, which is what the scope addition rules out. The seat agreed to leave the binding out of this PR. The follow-up I named is for the import to answer the base row's `version` and `created_at` itself, so the door never re-reads the store at install. That is a metadata-protocol card, which the seat files. ## Tests `packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts`, inside #30808's copy harness (the real `SeedLoaderService` over an in-memory engine; the import, receipt and package store as doubles). There are seven new cases, and no new test file: - the reseed of a purged copy puts the 3 rows back, owned by the reseeder, and the lookup resolves; a second reseed is `200` with `inserted: 0, skipped: 3`; nothing is ledgered; - the count after the install is 3 with a customer row beside it, and 0 after the purge; - a first load with `seed-receipt-unread` is retried by the reseed and lands 3 rows; - a first load with `seed-error` (the metadata read throws once) is retried the same way; - reseed and count of a Studio base with published seed items, and of an id that is no base: `404`, nothing written; - a copy made without the sample data: reseed `400 RESEED_SKIPPED` ("No sample data was copied into …"), count 0; - an unreadable receipt: reseed and count `503 SERVICE_UNAVAILABLE`, the store's text not quoted, nothing written. The harness gains `reseed` and `count` beside `purge`, and the metadata read is a spy so one load can throw. <details><summary>Red on main (the new cases run against the unfixed plugin at base 2ae3e30, before the fix was written)</summary> ``` × the reseed of a recorded copy puts the purged sample rows back from the receipt, owned by the reseeder; nothing is ledgered × the count of a recorded copy is the purge's match: every sample row after the install, none after the purge × a first load whose receipt could not be read (`seed-receipt-unread`) is retried by the reseed, from the stored receipt × a first load that threw (`seed-error`) is retried by the reseed, from the stored receipt × a copy made without the sample data: the reseed says no sample data was copied (400), and the count is 0 × an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing AssertionError: {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"No marketplace install for com.example.crm-copy."}}: expected 404 to be 200 Tests 6 failed | 52 passed (58) ``` The counts are of the suite at 2ae3e30, before #31475 added five cases to it. The seventh case (any other base answers 404) passes on main by construction. It is the negative pin, and ablation 3 shows it red. </details> <details><summary>Ablations (fix committed at 7bb1588, each restored)</summary> All three ran through `node scripts/ablation-replace.mjs` on `marketplace-install-local-plugin.ts`, against the copy suite. The suite imports the plugin from `src/`, so no build applies. 1. The count reads an unread receipt as `count: 0`: ``` ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 5d41b24e356e × an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing Tests 1 failed | 57 passed (58) ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty ``` 2. The reseed reads an unread receipt as a skipped run (400): ``` ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 84c801a8af03 × an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing Tests 1 failed | 57 passed (58) ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty ``` 3. The shared reader reaches a base with no record of this door's (the receipt read on the id alone): ``` ok mutation landed: anchor 1 -> 0, blob cf4f8a8 -> 7f2d6d85a54d × a Studio-authored writable base with published seed items is refused at purge: 404, nothing deleted × a copy made before the record existed is not reached: no record, 404, nothing deleted × a record that is not kept is said in the install answer (`copyRecord`), and the copy is then out of the purge's reach × uninstalling the copy removes its record through the protocol's uninstall cleanups; the purge then answers 404 × reseed and count reach no other base: a Studio base with published seed items, and an id that is no base, answer 404 and write nothing Tests 5 failed | 53 passed (58) ok restored: blob == HEAD (cf4f8a8) and `git diff HEAD` is empty ``` </details> ## Verification Final head `6cf7abe7`: the fix 7bb1588, one merge of `origin/main` (ff76c20), and a second merge (6cf7abe). The second merge was needed because #31475 (#30964) landed on the same two files and conflicted. Both sides were kept: the audit helper beside `unreadableCopyReceipt`, and the harness's `grantsOf` / `auditEvents` beside `reseed` / `count` / `metadataGetObject`. Every exit code was captured before any pipe. - **Full build**: `pnpm turbo run build --concurrency=2` under the verify lock, VERDICT command-exit 0 (120 of 120), on 6cf7abe. - **Derived gate union**, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on 6cf7abe: 99 families, the same list as on ff76c20, and every one exit 0 on its first run. `--ran` reports "99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero — all 99 recorded an exit code and none of them is 3)". `check:dts-closure`, `check:dual-build-cjs-loads` and `check:type-check-debt` ran after the build. The union was also run whole on ff76c20, 99 of 99 exit 0. - **Facade gate**, `pnpm check:facade-delegation-only`, run by hand: exit 0. - **Package**: `pnpm --filter @objectstack/cloud-connection test` on 6cf7abe: Test Files 75 passed, Tests 1024 passed (exit 0). `typecheck` (src and `tsconfig.test.json`): exit 0. - **Lint**: `eslint --no-inline-config` on the three touched `.ts` files: exit 0. - **Full dogfood battery**, `pnpm --filter @objectstack/dogfood test` on ff76c20 (before the second merge) under the verify lock: VERDICT command-exit 0, Test Files 439 passed / 3 skipped (442), Tests 3629 passed / 41 skipped (3670). It was not re-run after the conflict merge, because this PR has no narrowing arm. - CI-only families (the Test Core shards, the type-check lanes, Build Docs) are read from this PR's check-runs. ## Acceptance notes - **Clause-② from the diff.** It is `yes (widening)` only: two doors serve a copy they refused (`404`), and nothing they accepted is refused now. The claim's possible narrowing arm was for the store-null half, which changes no answer here. - **Locale of a copy's reseed.** At install, the seed locale falls back to the source package's `i18n.defaultLocale`. The reseed has no source body: the copy keeps none. So it hands over the copy base's own manifest from the registry, and the organization's and the caller's locale still come first. - **Governed surface.** None. The diff is one source file, the record module's docblock, one test file, one docs page and one changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47bc621 commit 210ef60

5 files changed

Lines changed: 346 additions & 51 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
feat(cloud-connection): install-local's reseed and sample-data count reach an unmanaged copy, and retry a failed first load (#31425)
6+
7+
Clause-②: yes (widening) — `POST /api/v1/marketplace/install-local/:id/reseed-sample-data` and `GET …/:id/sample-data` now serve the base of an unmanaged copy this door made with `seedSampleData: true`, which they answered `404` before. Nothing is narrowed.
8+
Metadata-protocol: none
9+
10+
- **Reseed.** For a copy, the reseed loads the sample data again from the copy's seed receipt, through the same read of it the purge uses: the rows it creates are owned by the caller, it answers the same four counts, and a run over an intact baseline succeeds with `inserted: 0`. A copy made without the sample data answers `400 RESEED_SKIPPED`, and a receipt that could not be read answers `503 SERVICE_UNAVAILABLE`, never "no sample data".
11+
- **Retry.** A copy whose first load failed at install (`seeded.mode: 'skipped'` with `seed-receipt-unread` or `seed-error`) is loaded by the reseed from the stored receipt, instead of a re-run of the install that the import refuses `409`.
12+
- **Count.** For a copy, the count is the rows a purge of the copy would delete now; `count: 0` for a copy made without the sample data.
13+
- **Unchanged.** Any other base, a Studio-authored one with published seed items included, still answers `404` on all three doors. Uninstalling a copy is `DELETE /api/v1/packages/:baseId`, which drops its tables with their rows unless `keepData: true` keeps them all; this door does not purge on uninstall.

‎content/docs/deployment/marketplace-handbook.mdx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,13 @@ the detail.
226226
offers **Add sample data**, then **Re-seed sample data**, and **Purge sample
227227
data**, which deletes only the records your seed created. Behind them are
228228
`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data` and
229-
`…/purge-sample-data`, both requiring `manage_metadata`.
229+
`…/purge-sample-data`, both requiring `manage_metadata`. For an unmanaged
230+
copy installed with its sample data, call them with the copy's base id: they
231+
act on the sample data the copy holds, and the reseed also loads it again
232+
after a first load that failed. Uninstalling the copy
233+
(`DELETE /api/v1/packages/:baseId`) drops its tables with every row in them;
234+
with `?keepData=true` every row stays, sample rows included, and no door
235+
tells them apart any more, so purge first if you want them gone.
230236
7. **Enable and disable.** **Setup → Installed Apps** lists the apps installed
231237
from the marketplace, each package of a suite as its own row. An admin with
232238
`manage_metadata` may disable a writable package, and a **module of an

‎packages/cloud-connection/src/marketplace-install-local-install-modes.test.ts‎

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,8 @@ describe('install-local — an unmanaged copy\'s sample data, asked for with `se
780780
}
781781
return { sourcePackageId: source.id, targetPackageId, namespace: source.namespace, copiedCount: copied.length, copied, strippedLocks: [] };
782782
});
783+
// [#31425] The metadata service's object read the seed run's dependency graph asks; a spy, so a case can make one load throw.
784+
const metadataGetObject = vi.fn(async (name: string) => objects.get(name));
783785
const cleanups = new Map<string, (args: { packageId: string }) => Promise<{ success: boolean; removed: number; error?: string }>>();
784786
// [#30964] The `audit` slot's package lifecycle writer, as the door reaches it.
785787
const auditEvents: any[] = [];
@@ -812,7 +814,7 @@ describe('install-local — an unmanaged copy\'s sample data, asked for with `se
812814
manifest: { register },
813815
auth: installerAuthService(INSTALLER),
814816
objectql: grantsOf(withInstallerGrants(engine, INSTALLER)),
815-
metadata: { getObject: async (name: string) => objects.get(name) },
817+
metadata: { getObject: metadataGetObject },
816818
protocol: {
817819
importUnmanagedPackage, getMetaItem,
818820
registerUninstallCleanup: (name: string, cleanup: any) => { cleanups.set(name, cleanup); },
@@ -841,13 +843,15 @@ describe('install-local — an unmanaged copy\'s sample data, asked for with `se
841843
return {
842844
install: (body: unknown) => routes.get(`POST ${ROUTE}`)!(makeC(body)),
843845
purge: (manifestId: string) => routes.get(`POST ${ROUTE}/:manifestId/purge-sample-data`)!(purgeC(manifestId)),
846+
reseed: (manifestId: string) => routes.get(`POST ${ROUTE}/:manifestId/reseed-sample-data`)!(purgeC(manifestId)),
847+
count: (manifestId: string) => routes.get(`GET ${ROUTE}/:manifestId/sample-data`)!(purgeC(manifestId)),
844848
register,
845849
logger,
846850
/** [#30964] The in-process door init() registered. */
847851
door: services['marketplace-install-local'] as { install(r: Request): Promise<Response> } | undefined,
848852
};
849853
}
850-
return { tables, writes, seedRows, packages, packageRows, packageGet, storeFault, auditEvents, importUnmanagedPackage, getMetaItem, cleanups, boot };
854+
return { tables, writes, seedRows, packages, packageRows, packageGet, storeFault, auditEvents, importUnmanagedPackage, getMetaItem, metadataGetObject, cleanups, boot };
851855
}
852856

853857
it('off by default: the import is not asked for the sample data, no row lands, and the answer says it was skipped', async () => {
@@ -1156,6 +1160,126 @@ describe('install-local — an unmanaged copy\'s sample data, asked for with `se
11561160
expect((await h.purge('com.example.crm-copy')).status).toBe(404);
11571161
});
11581162

1163+
// ── [#31425] Reseed and the count reach a recorded copy through the purge's receipt; a failed first load is retried there ──
1164+
1165+
it('the reseed of a recorded copy puts the purged sample rows back from the receipt, owned by the reseeder; nothing is ledgered', async () => {
1166+
const w = world();
1167+
const h = await w.boot();
1168+
await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1169+
await h.purge('com.example.crm-copy');
1170+
expect(w.tables.crm_account).toEqual([]);
1171+
1172+
const res = await h.reseed('com.example.crm-copy');
1173+
1174+
expect(res.status, JSON.stringify(res.payload)).toBe(200);
1175+
expect(res.payload).toEqual({
1176+
success: true,
1177+
data: { manifestId: 'com.example.crm-copy', inserted: 3, updated: 0, skipped: 0, errors: 0, withSampleData: true },
1178+
});
1179+
const acme = w.tables.crm_account!.find((r) => r.name === 'Acme')!;
1180+
expect(w.tables.crm_account!.map((r) => r.name).sort()).toEqual(['Acme', 'Globex']);
1181+
expect(w.tables.crm_contact).toEqual([expect.objectContaining({ email: 'ada@acme.test', account: acme.id })]);
1182+
for (const row of [...w.tables.crm_account!, ...w.tables.crm_contact!]) {
1183+
expect({ owner_id: row.owner_id, created_by: row.created_by }).toEqual({ owner_id: INSTALLER, created_by: INSTALLER });
1184+
}
1185+
// Idempotent: a second reseed over the intact baseline writes nothing and succeeds.
1186+
const again = await h.reseed('com.example.crm-copy');
1187+
expect(again.status, JSON.stringify(again.payload)).toBe(200);
1188+
expect(again.payload.data).toMatchObject({ inserted: 0, skipped: 3, errors: 0 });
1189+
expect(new LocalManifestSource(dir).list().entries).toEqual([]);
1190+
});
1191+
1192+
it('the count of a recorded copy is the purge\'s match: every sample row after the install, none after the purge', async () => {
1193+
const w = world();
1194+
const h = await w.boot();
1195+
await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1196+
w.tables.crm_account!.push({ id: 'u1', name: 'Initech', owner_id: INSTALLER });
1197+
1198+
const counted = await h.count('com.example.crm-copy');
1199+
1200+
expect(counted.status, JSON.stringify(counted.payload)).toBe(200);
1201+
expect(counted.payload).toEqual({ success: true, data: { manifestId: 'com.example.crm-copy', count: 3, unidentified: 0 } });
1202+
await h.purge('com.example.crm-copy');
1203+
expect((await h.count('com.example.crm-copy')).payload.data).toEqual({ manifestId: 'com.example.crm-copy', count: 0, unidentified: 0 });
1204+
});
1205+
1206+
it('a first load whose receipt could not be read (`seed-receipt-unread`) is retried by the reseed, from the stored receipt', async () => {
1207+
const w = world();
1208+
const h = await w.boot();
1209+
w.getMetaItem.mockRejectedValueOnce(new Error('sys_metadata is unreachable'));
1210+
const installed = await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1211+
expect(installed.payload.data.seeded).toMatchObject({ mode: 'skipped', reason: expect.stringContaining('seed-receipt-unread') });
1212+
expect(w.tables.crm_account ?? []).toEqual([]);
1213+
1214+
const res = await h.reseed('com.example.crm-copy');
1215+
1216+
expect(res.status, JSON.stringify(res.payload)).toBe(200);
1217+
expect(res.payload.data).toMatchObject({ inserted: 3, errors: 0 });
1218+
expect(w.tables.crm_account!.map((r) => r.name).sort()).toEqual(['Acme', 'Globex']);
1219+
});
1220+
1221+
it('a first load that threw (`seed-error`) is retried by the reseed, from the stored receipt', async () => {
1222+
const w = world();
1223+
const h = await w.boot();
1224+
w.metadataGetObject.mockRejectedValueOnce(new Error('the metadata service is restarting'));
1225+
const installed = await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1226+
expect(installed.payload.data.seeded).toMatchObject({ mode: 'skipped', reason: expect.stringContaining('seed-error') });
1227+
expect(w.tables.crm_account ?? []).toEqual([]);
1228+
1229+
const res = await h.reseed('com.example.crm-copy');
1230+
1231+
expect(res.status, JSON.stringify(res.payload)).toBe(200);
1232+
expect(res.payload.data).toMatchObject({ inserted: 3, errors: 0 });
1233+
});
1234+
1235+
it('reseed and count reach no other base: a Studio base with published seed items, and an id that is no base, answer 404 and write nothing', async () => {
1236+
const w = world();
1237+
const h = await w.boot();
1238+
await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1239+
await h.purge('com.example.crm-copy');
1240+
w.packages.set('com.example.studio', { manifest: { id: 'com.example.studio', namespace: 'studio' } });
1241+
w.tables.sys_metadata!.push({
1242+
id: 'studio-seed', type: 'seed', name: 'crm_account_seed', package_id: 'com.example.studio', state: 'active',
1243+
metadata: { object: 'crm_account', externalId: 'name', records: [{ name: 'Acme' }] },
1244+
} as Row);
1245+
const before = w.writes.length;
1246+
for (const id of ['com.example.studio', 'com.example.nothing']) {
1247+
for (const res of [await h.reseed(id), await h.count(id)]) {
1248+
expect(res.status, `${id}: ${JSON.stringify(res.payload)}`).toBe(404);
1249+
expect(res.payload.error.code).toBe('RESOURCE_NOT_FOUND');
1250+
}
1251+
}
1252+
expect(w.writes.length).toBe(before);
1253+
});
1254+
1255+
it('a copy made without the sample data: the reseed says no sample data was copied (400), and the count is 0', async () => {
1256+
const w = world();
1257+
const h = await w.boot();
1258+
await h.install({ manifest: seededCrm(), installMode: 'unmanaged' });
1259+
const res = await h.reseed('com.example.crm-copy');
1260+
expect(res.status, JSON.stringify(res.payload)).toBe(400);
1261+
expect(res.payload.error.code).toBe('RESEED_SKIPPED');
1262+
expect(res.payload.error.message).toContain('No sample data was copied into \'com.example.crm-copy\'');
1263+
expect(w.writes).toEqual([]);
1264+
expect((await h.count('com.example.crm-copy')).payload.data).toEqual({ manifestId: 'com.example.crm-copy', count: 0, unidentified: 0 });
1265+
});
1266+
1267+
it('an unreadable receipt is never read as "no sample data": the reseed and the count answer 503 and write nothing', async () => {
1268+
const w = world();
1269+
const h = await w.boot();
1270+
await h.install({ manifest: seededCrm(), installMode: 'unmanaged', seedSampleData: true });
1271+
await h.purge('com.example.crm-copy');
1272+
const before = w.writes.length;
1273+
for (const ask of [h.reseed, h.count]) {
1274+
w.getMetaItem.mockRejectedValueOnce(new Error('sys_metadata is unreachable'));
1275+
const res = await ask('com.example.crm-copy');
1276+
expect(res.status, JSON.stringify(res.payload)).toBe(503);
1277+
expect(res.payload.error.code).toBe('SERVICE_UNAVAILABLE');
1278+
expect(res.payload.error.message).not.toContain('sys_metadata is unreachable');
1279+
}
1280+
expect(w.writes.length).toBe(before);
1281+
});
1282+
11591283
// ── [#30964, cloud#3624 ruling A「两边审计」] the environment's audit row of each copy, and the in-process door ──
11601284

11611285
it('a copy that lands hands the audit slot one package_unmanaged_import event: the new base, its namespace, the count, success', async () => {

0 commit comments

Comments
 (0)