Skip to content

Commit 55a00ba

Browse files
fix(metadata-protocol): an object save or publish whose storage sync is refused says so on its answer (#30691)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DDUHyfgsW9WLX5BxP3bK55c)_ domain:server#5 · os-elon-musk · session_016WV4cyh75wtqD3bprL8aLT Fixes #30635 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:保存对象(发布模式)和单项发布在建表/改表被数据源拒绝时,照旧答 200 成功,但答复的 `advisories` 里多一条 `warning`(规则 `object-storage-sync-refused`),写明哪个对象的存储没跟上、怎么修。 - 为什么改:实测 main(`47c457b2`):数据源拒绝同步时两个入口都答 200,答复里没有任何迹象,管理员要等读写失败才发现表没建;原因只在服务器日志里。 - 风险与回滚:只在同步被拒时加一条提示;状态码、其它键、发布结果都不变,同步正常时答复字节不变。数据源自带的拒绝原文只在它声明为 4xx 拒绝时才引用,否则提示「原因在服务器日志」,与同类 200 报告一致。回滚即 revert 本 PR。 - 先看:`packages/metadata-protocol/src/protocol-registry-hydration.ts` 的 `objectStorageSyncRefusedNotice` 与 `ensureObjectStorage` 的 catch;`protocol-save.ts` 发布分支;`protocol-history-and-publish.ts` 的 `runPublishSideEffects`。 - 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.publish-side-effects-canonical-type.test.ts`,预期 12 passed(其中 #30635 六条)。 - 需要你做的:无,席位落地。 ## What changes - **`protocol-registry-hydration.ts`.** `ensureObjectStorage` now returns what it found: `undefined` when the sync ran (or the type has no storage), and one `RuntimeAuthoringIssue` when the driver refused it. The new exported `objectStorageSyncRefusedNotice(name, err)` builds it: `rule` `object-storage-sync-refused` (exported as `OBJECT_STORAGE_SYNC_REFUSED`), `severity` `warning`, `where` `object "NAME"`, `path` empty (the finding is about the item as a whole), a `message` naming the object and saying its storage was not created or altered, and a `hint` naming the two remedies the log line already names (publish again, or restart). The #22985 `error` line is unchanged, byte for byte, and still prints first. - **The driver's text.** It is quoted on the advisory only when the driver declared a client refusal (a 4xx `status`), through `clientFacingFailureText`, the rule every side-effect report that rides a 2xx on these doors already follows (the materializer's `materializeApplied.error`, the batch's `failures[].error`). Any other failure says the reason is in the server log, where the `error` line carries it whole. - **Save door** (`protocol-save.ts`, the `mode === 'publish'` branch): the advisory is appended to `saveNotices`, after the gate's findings and the save's own notices, so it rides the existing omitted-when-empty `advisories` key. - **Publish door** (`protocol-history-and-publish.ts`): `runPublishSideEffects` returns it as `storageRefused`, and `publishMetaItem` appends it to the same omitted-when-empty `advisories`. - **`protocol.ts`**: the two private pass-through signatures only (`ensureObjectStorage`'s return type, and one `storageRefused?` line on `runPublishSideEffects`' declared result). No body line. - **`.changeset/30635-object-save-reports-refused-storage-sync.md`**: `@objectstack/metadata-protocol` patch. No new key on any response schema: `SaveMetaItemResponseSchema` and `PublishMetaItemResponseSchema` already declare `advisories` (`RuntimeAuthoringIssue[]`, `severity` `warning` or `info`, `rule` a free string), and the save already uses it for a non-gate fact about the write (`withheld-key-dropped`). The status code is unchanged and the publish is still not failed. ## Tests `protocol.publish-side-effects-canonical-type.test.ts` gains a top-level `#30635` block in the file's existing harness (its stub engine already refuses a named object's sync, for #22985): 1. save door, refused sync: `success` and `state` `active`, exactly one `object-storage-sync-refused` warning with `path` empty and `where` `object "ticket"`; the undeclared driver text is not on it and the message points at the server log; the `error` line still carries the text. 2. save door, a declared (409) driver refusal: its text is quoted on the advisory. 3. CONTROL save door, clean sync: no `advisories` key at all. 4. CONTROL save door, draft save: no sync runs and no `advisories` key. 5. publish door, refused sync: `success`, exactly one warning, the declared text quoted. 6. CONTROL publish door, clean sync: no `advisories` key at all. **Before (measured at `origin/main` `47c457b2`):** cases 1, 2 and 5 red (`expected undefined to match object`: a refused sync answered `success: true` with no `advisories` entry at all), the three controls green. **After:** the file 12/12. Whole package on the final head `e01c8939`: `pnpm --filter @objectstack/metadata-protocol test` 317 files passed, 4 skipped; 22460 tests passed, 26 skipped. `typecheck` exit 0. **One more test file touched.** `protocol.runtime-authoring-gate.test.ts`'s engine double had no `syncObjectSchema`, so every publish in it ran a sync that threw (`syncObjectSchema is not a function`), which on `main` printed the #22985 `error` line unnoticed and now also adds the advisory; two of its pins assert "no advisories key" and went red. The double now has a sync that succeeds, so those pins read the gate's advisories alone. (The real engine always declares `syncObjectSchema`, `host-engine.ts`.) The `ensureObjectStorage` guard keeps its bare `return;`, which `protocol.object-registry-write-through-spelling.test.ts` counts in the source. <details><summary>Ablations: each through scripts/ablation-replace.mjs, anchor 1 to 0, restored to the HEAD blob, git diff HEAD empty</summary> | mutation | result | |:---|:---| | delete the save door's `saveNotices.push(storageRefused)` | cases 1 and 2 red, 10 green | | delete the publish door's `response.advisories = [..., effects.storageRefused]` | case 5 red, 11 green | | quote the raw driver text instead of `clientFacingFailureText` | case 1 red (the undeclared text reached the body), 11 green | The test imports `./protocol.js` from source, so no `dist/` leg applies. </details> ## Acceptance notes - **Not covered here, out of this card's file surface: the package-draft batch publish** (Studio's "publish whole app", `publishPackageDrafts`). It also runs the sync through `runPublishSideEffects`, and now receives `storageRefused`, but its body lives in `protocol.ts` (hot file, codemod in flight) and builds each `published[]` element before the side effects run, so reporting it there needs body edits. Until then that door reports a refused sync in the server log only, as before; the changeset says so. Handed to the seat. - **Follow-up, spec lane:** the `advisories` describe in `packages/spec/src/api/protocol.zod.ts` (save and publish response schemas) should name the new rule beside `withheld-key-dropped`. - **Follow-up, ui:** showing this warning after a save in Studio/Setup is a separate card, as the issue says. - **The data-loss refusal from #30632 (landed on `main` as `8d7f9065` while this PR was open) is withheld on the advisory as it stands:** `ExistingTableAdoptionRefusal` declares no `status`, so the advisory says the reason is in the server log rather than quoting its remedy text. If that prose should reach the caller, the driver declares it a client refusal (a 4xx `status`); that is the driver lane's call, not this PR's. - **Deviation from the dispatch wording:** the dispatch said the message names the driver's refusal text. It does so only for a declared client refusal, by the existing withhold rule above; quoting every driver text would put raw driver failures on a 2xx body, which the rule exists to prevent. ## Verification Local, on head `e01c8939` (merged with `origin/main` `b77c160a`): - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 60 families derived; `--ran`: 58 run, all exit 0; 2 NOT MEASURED (`check:dual-build-cjs-loads`, `check:lean-entry-closure`: exit 3, PREREQUISITE NOT MET, they need a whole-tree build; CI runs them). - `pnpm lint`: exit 0. - `pnpm --filter @objectstack/metadata-protocol test`: exit 0; `typecheck`: exit 0. - Re-run after the last commit: `check:engine-double-contract`, `check:durability-log-level`, `check:issue-citations`, `check:doc-authoring`, `check:nul-bytes`: exit 0. Then `origin/main` `a2437b56` was merged (head `9c02a3e7`). It brings `objectql`, `driver-sql`, `rest` and `plugin-auth` commits and nothing under `packages/metadata-protocol`, none of which this package imports, so the local readings above were not re-run (AGENTS.md multi-agent §10, scoped re-check). Everything else is CI's. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cyh75wtqD3bprL8aLT)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 06cccd3 commit 55a00ba

7 files changed

Lines changed: 220 additions & 4 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): an object save or publish whose storage sync the datasource refuses now says so on its answer
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`PUT /api/v1/meta/object/NAME` (a publish-mode save) and `POST /api/v1/meta/object/NAME/publish` sync the object's table after the item is written. When the datasource refused that sync, both answered `200` success with nothing about storage, and only the server log said the table was not created or altered. Both answers now carry one `warning` on their existing `advisories` key, with the rule id `object-storage-sync-refused`, naming the object and saying its storage did not follow. The datasource's own text is quoted when it declared a client refusal (a 4xx status); otherwise the advisory says the reason is in the server log, which keeps the `error` line it already printed. The status code and every other key are unchanged, the publish is still not failed, and an answer whose sync ran has no new key.
11+
12+
A client that groups or suppresses advisories by rule id sees one new id. The package-draft batch publish (Studio's "publish whole app") still reports a refused sync in the server log only.

‎packages/metadata-protocol/src/protocol-history-and-publish.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,9 @@ export class ProtocolHistoryAndPublishFacet {
374374
if (effects.seedApplied) response.seedApplied = effects.seedApplied;
375375
if (effects.materializeApplied) response.materializeApplied = effects.materializeApplied;
376376
if (effects.projectionApplied) response.projectionApplied = effects.projectionApplied;
377+
// [#30635] A refused storage sync rides the same omitted-when-empty
378+
// `advisories`, after the gate's findings; the publish is not failed.
379+
if (effects.storageRefused) response.advisories = [...(response.advisories ?? []), effects.storageRefused];
377380
// [#10219] LAST, and awaited: tell the host that ONE item went live, so
378381
// it can announce `metadata:reloaded` and boot-cached consumers re-sync
379382
// without a restart — the parity the batch door has had since #2576.
@@ -1050,11 +1053,13 @@ export class ProtocolHistoryAndPublishFacet {
10501053
seedApplied?: { success: boolean; inserted: number; updated: number; error?: string; errors?: unknown[]; landed?: SeedAppliedLandedParsed[] };
10511054
materializeApplied?: PublishMaterializeResult;
10521055
projectionApplied?: MutationProjectionOutcome;
1056+
storageRefused?: RuntimeAuthoringIssue;
10531057
}> {
10541058
const out: {
10551059
seedApplied?: { success: boolean; inserted: number; updated: number; error?: string; errors?: unknown[]; landed?: SeedAppliedLandedParsed[] };
10561060
materializeApplied?: PublishMaterializeResult;
10571061
projectionApplied?: MutationProjectionOutcome;
1062+
storageRefused?: RuntimeAuthoringIssue;
10581063
} = {};
10591064
// Drafts skipped the registry mutation; on publish we now refresh the
10601065
// runtime registry so live behaviour catches up immediately (matches
@@ -1090,7 +1095,11 @@ export class ProtocolHistoryAndPublishFacet {
10901095
// — but it is a fact about a repository read, load-bearing for a guard
10911096
// two files away, and nothing said so at either end. Folding at the
10921097
// producer retires the question instead of restating the invariant.
1093-
await host.ensureObjectStorage(args.singularType, args.name);
1098+
//
1099+
// [#30635] A refused sync comes back as the advisory the caller is
1100+
// told; set only when it was refused, so a clean result is unchanged.
1101+
const storageRefused = await host.ensureObjectStorage(args.singularType, args.name);
1102+
if (storageRefused) out.storageRefused = storageRefused;
10941103
// Publishing a `seed` is what makes its rows live — materialize them
10951104
// NOW (best-effort, never fails the publish) so every publish path
10961105
// (per-ref REST publish, the home banner, package publish-drafts)

‎packages/metadata-protocol/src/protocol-registry-hydration.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import {
5050
import type { HydrationOptions, ShippingPackages, UnboundOwnerRefusalState } from './protocol-package-bindings.js';
5151
import { bundleMemberDiscriminator } from './protocol-bundle-members.js';
5252
import type { ProtocolHost } from './protocol-host.js';
53+
import type { RuntimeAuthoringIssue } from './runtime-authoring-gate.js';
54+
import { clientFacingFailureText } from './protocol-client-failures.js';
5355

5456
export class ProtocolRegistryHydrationFacet {
5557
/**
@@ -1427,8 +1429,15 @@ export class ProtocolRegistryHydrationFacet {
14271429
* not quiet: a refused sync is reported at `error` with its consequence
14281430
* and fix (#22985), because the publish it follows has already answered
14291431
* success.
1432+
*
1433+
* [#30635] …and to the CALLER: a refused sync returns one `warning`
1434+
* advisory ({@link objectStorageSyncRefusedNotice}) that both doors running
1435+
* this sync put on the 2xx they answer (`saveMetaItem` in publish mode and
1436+
* `publishMetaItem`, through `runPublishSideEffects`). A sync that ran, or
1437+
* a type with no storage, returns `undefined`, so a clean answer is
1438+
* unchanged.
14301439
*/
1431-
static async ensureObjectStorage(host: Pick<ProtocolHost, 'engine' | 'ensureObjectStorage'>, type: string, name: string): Promise<void> {
1440+
static async ensureObjectStorage(host: Pick<ProtocolHost, 'engine' | 'ensureObjectStorage'>, type: string, name: string): Promise<RuntimeAuthoringIssue | undefined> {
14321441
// [#8862] The `&& type !== 'objects'` limb is GONE. Both call sites
14331442
// hand over a folded value — `saveMetaItem` after
14341443
// `canonicalizeMetaRequestType`, and `runPublishSideEffects` as
@@ -1446,6 +1455,7 @@ export class ProtocolRegistryHydrationFacet {
14461455
if (type !== 'object') return;
14471456
try {
14481457
await host.engine.syncObjectSchema(name);
1458+
return undefined;
14491459
} catch (err: any) {
14501460
// [#22985] `error`, not `warn`, by the one question AGENTS.md
14511461
// "Degradation log levels" turns this on: the publish still
@@ -1463,6 +1473,7 @@ export class ProtocolRegistryHydrationFacet {
14631473
+ 're-runs this sync, or restart, since the boot syncs every registered object\'s storage.',
14641474
err?.message ?? err,
14651475
);
1476+
return objectStorageSyncRefusedNotice(name, err);
14661477
}
14671478
}
14681479

@@ -1663,3 +1674,40 @@ export class ProtocolRegistryHydrationFacet {
16631674
return err;
16641675
}
16651676
}
1677+
1678+
/**
1679+
* [#30635] The rule id of the advisory a save or publish answers with when the
1680+
* object's storage sync was refused. Stable across releases: the key a client
1681+
* groups or suppresses the notice by.
1682+
*/
1683+
export const OBJECT_STORAGE_SYNC_REFUSED = 'object-storage-sync-refused';
1684+
1685+
/**
1686+
* [#30635] The caller's half of a refused object-storage sync: one `warning`
1687+
* on the answer's `advisories`, beside the #22985 `error` line the operator
1688+
* reads. The answer stays the success it already was (the item is published,
1689+
* registered and served); this says its storage did not follow, so the
1690+
* author does not find out only when reads and writes against it fail.
1691+
*
1692+
* `path` is empty: the finding is about the object as a whole. The driver's
1693+
* text is quoted only when the driver declared it a client refusal (a 4xx
1694+
* `status`), the rule every report riding a 2xx on these doors follows
1695+
* ({@link clientFacingFailureText}); any other failure is named as being in
1696+
* the server log, where the `error` line carries it whole.
1697+
*/
1698+
export function objectStorageSyncRefusedNotice(name: string, err: unknown): RuntimeAuthoringIssue {
1699+
const reason = clientFacingFailureText(err, 'the reason is in the server log');
1700+
return {
1701+
rule: OBJECT_STORAGE_SYNC_REFUSED,
1702+
severity: 'warning',
1703+
where: `object "${name}"`,
1704+
path: '',
1705+
message:
1706+
`object '${name}' was saved and is served, but its storage was NOT created or altered: `
1707+
+ `the datasource refused its schema sync (${reason}). Until the sync runs, reads and writes `
1708+
+ 'against it fail, or miss the columns this save added.',
1709+
hint:
1710+
"Resolve the datasource's refusal, then publish the object again (which re-runs the sync), "
1711+
+ 'or restart the server, which syncs the storage of every registered object.',
1712+
};
1713+
}

‎packages/metadata-protocol/src/protocol-save.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2070,7 +2070,12 @@ export class ProtocolSaveFacet {
20702070
// [#6602] The SAME scope the row was just written with.
20712071
organizationId: null,
20722072
});
2073-
await host.ensureObjectStorage(request.type, request.name);
2073+
// [#30635] A refused storage sync is told to the caller too:
2074+
// one `warning` on this answer's `advisories`, after the
2075+
// gate's findings and the save's own notices. The save is not
2076+
// failed (the row is written, registered and served).
2077+
const storageRefused = await host.ensureObjectStorage(request.type, request.name);
2078+
if (storageRefused) saveNotices.push(storageRefused);
20742079
}
20752080
// ADR-0010 — success audit (best-effort).
20762081
await host.recordMetadataAudit({

‎packages/metadata-protocol/src/protocol.publish-side-effects-canonical-type.test.ts‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,138 @@ describe('[#22985] a refused object-storage sync on publish is an error, once',
416416
expect(spy.error).not.toHaveBeenCalled();
417417
});
418418
});
419+
420+
// ---------------------------------------------------------------------------
421+
// [#30635] A refused object-storage sync is reported to the CALLER too, on
422+
// the answer's `advisories` — not only in the server log.
423+
// ---------------------------------------------------------------------------
424+
// The #22985 line above tells the operator; the admin or AI that saved the
425+
// object read a plain success and learned nothing until reads and writes
426+
// against the object failed. Both doors that run the sync now carry one
427+
// `warning` advisory naming the object, on the 2xx they already answer: the
428+
// save (`PUT /meta/object/NAME`, `saveMetaItem` in publish mode) and the
429+
// single-item publish (`POST /meta/object/NAME/publish`). Status and every
430+
// other key are unchanged, and a clean sync answers without the key at all.
431+
//
432+
// The driver's text is quoted only when the driver declared it a client
433+
// refusal (a 4xx `status`), the rule every 2xx side-effect report on these
434+
// doors follows (`clientFacingFailureText`); otherwise the advisory says the
435+
// reason is in the server log, where the #22985 line carries it whole.
436+
437+
/** The advisory's stable rule id — the key a client groups or suppresses it by. */
438+
const SYNC_REFUSED_RULE = 'object-storage-sync-refused';
439+
440+
describe('[#30635] a refused object-storage sync rides the answer as a warning advisory', () => {
441+
afterEach(() => {
442+
vi.restoreAllMocks();
443+
});
444+
445+
/** Quiets (and keeps) the #22985 log line, which is pinned above. */
446+
const quietConsole = () => vi.spyOn(console, 'error').mockImplementation(() => {});
447+
448+
/** A driver refusal that DECLARES itself a client refusal (4xx `status`). */
449+
function declareRefusal(engine: any, text: string): void {
450+
engine.syncObjectSchema = vi.fn(async () => {
451+
throw Object.assign(new Error(text), { status: 409 });
452+
});
453+
}
454+
455+
const syncRefused = (res: any) => (res.advisories ?? []).filter((a: any) => a.rule === SYNC_REFUSED_RULE);
456+
457+
it('save door: a refused sync answers success with one warning naming the object, and withholds undeclared driver text', async () => {
458+
const error = quietConsole();
459+
const { protocol, syncedObjects } = makeProtocol(new Set(['ticket']));
460+
461+
const res = await protocol.saveMetaItem({
462+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG,
463+
});
464+
465+
// The publish the answer stands for is not undone.
466+
expect(res.success).toBe(true);
467+
expect(res.state).toBe('active');
468+
expect(syncedObjects).toEqual([]);
469+
const [advisory, ...rest] = syncRefused(res);
470+
expect(rest).toEqual([]);
471+
expect(advisory).toMatchObject({
472+
rule: SYNC_REFUSED_RULE, severity: 'warning', path: '', where: 'object "ticket"',
473+
});
474+
expect(advisory.message).toContain("'ticket'");
475+
expect(typeof advisory.hint).toBe('string');
476+
// An undeclared driver failure is withheld from the 2xx body…
477+
expect(advisory.message).not.toContain('there is already an index');
478+
expect(advisory.message).toContain('server log');
479+
// …and still reaches the server log whole (#22985).
480+
expect(error.mock.calls.some((args) => args[1] === 'there is already an index named ticket')).toBe(true);
481+
});
482+
483+
it('save door: a DECLARED driver refusal is quoted on the advisory', async () => {
484+
quietConsole();
485+
const { protocol, engine } = makeProtocol();
486+
declareRefusal(engine, "Schema sync REFUSED for the existing table 'ticket': it holds 2 rows.");
487+
488+
const res = await protocol.saveMetaItem({
489+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG,
490+
});
491+
492+
expect(res.success).toBe(true);
493+
const [advisory] = syncRefused(res);
494+
expect(advisory?.severity).toBe('warning');
495+
expect(advisory?.message).toContain("Schema sync REFUSED for the existing table 'ticket': it holds 2 rows.");
496+
});
497+
498+
it('CONTROL save door: a clean sync answers with no `advisories` key at all', async () => {
499+
const { protocol, syncedObjects } = makeProtocol();
500+
501+
const res = await protocol.saveMetaItem({
502+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG,
503+
});
504+
505+
expect(res.success).toBe(true);
506+
expect(syncedObjects).toEqual(['ticket']);
507+
expect('advisories' in res).toBe(false);
508+
});
509+
510+
it('CONTROL save door: a draft save runs no sync and reports none', async () => {
511+
const { protocol, engine } = makeProtocol(new Set(['ticket']));
512+
513+
const res = await protocol.saveMetaItem({
514+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG, mode: 'draft',
515+
});
516+
517+
expect(res.success).toBe(true);
518+
expect(engine.syncObjectSchema).not.toHaveBeenCalled();
519+
expect('advisories' in res).toBe(false);
520+
});
521+
522+
it('publish door: a refused sync answers success with the same warning', async () => {
523+
quietConsole();
524+
const { protocol, engine } = makeProtocol();
525+
await protocol.saveMetaItem({
526+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG, mode: 'draft',
527+
});
528+
declareRefusal(engine, "Schema sync REFUSED for the existing table 'ticket': it holds 2 rows.");
529+
530+
const res = await protocol.publishMetaItem({ type: 'object', name: 'ticket' });
531+
532+
expect(res.success).toBe(true);
533+
const [advisory, ...rest] = syncRefused(res);
534+
expect(rest).toEqual([]);
535+
expect(advisory).toMatchObject({
536+
rule: SYNC_REFUSED_RULE, severity: 'warning', path: '', where: 'object "ticket"',
537+
});
538+
expect(advisory.message).toContain("Schema sync REFUSED for the existing table 'ticket': it holds 2 rows.");
539+
});
540+
541+
it('CONTROL publish door: a clean sync answers with no `advisories` key at all', async () => {
542+
const { protocol, syncedObjects } = makeProtocol();
543+
await protocol.saveMetaItem({
544+
type: 'object', name: 'ticket', item: objectBody('ticket'), packageId: PKG, mode: 'draft',
545+
});
546+
547+
const res = await protocol.publishMetaItem({ type: 'object', name: 'ticket' });
548+
549+
expect(res.success).toBe(true);
550+
expect(syncedObjects).toEqual(['ticket']);
551+
expect('advisories' in res).toBe(false);
552+
});
553+
});

‎packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ function makeStubEngine() {
175175
tableOf(table).delete(found.key);
176176
return { deleted: 1 };
177177
},
178+
// [#30635] A publish syncs the object's storage, and a refused sync
179+
// rides the answer's `advisories`. This double's sync succeeds, so the
180+
// advisories these pins read are the gate's alone.
181+
async syncObjectSchema() {
182+
return true;
183+
},
178184
registry: {
179185
registerItem: () => {},
180186
registerObject: () => {},

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10032,7 +10032,7 @@ export class ObjectStackProtocolImplementation implements
1003210032
}
1003310033

1003410034
/** Docblock and body: {@link ProtocolRegistryHydrationFacet.ensureObjectStorage} in `protocol-registry-hydration.ts` [#29985 codemod]. */
10035-
private async ensureObjectStorage(type: string, name: string): Promise<void> {
10035+
private async ensureObjectStorage(type: string, name: string): Promise<RuntimeAuthoringIssue | undefined> {
1003610036
// [#29985 codemod] The body moved to `protocol-registry-hydration.ts`; it reaches this class through `protocol-host.ts`.
1003710037
return ProtocolRegistryHydrationFacet.ensureObjectStorage(protocolHost(this), type, name);
1003810038
}
@@ -11340,6 +11340,7 @@ export class ObjectStackProtocolImplementation implements
1134011340
seedApplied?: { success: boolean; inserted: number; updated: number; error?: string; errors?: unknown[]; landed?: SeedAppliedLandedParsed[] };
1134111341
materializeApplied?: PublishMaterializeResult;
1134211342
projectionApplied?: MutationProjectionOutcome;
11343+
storageRefused?: RuntimeAuthoringIssue;
1134311344
}> {
1134411345
// [#29985 codemod] The body moved to `protocol-history-and-publish.ts`; it reaches this class through `protocol-host.ts`.
1134511346
return ProtocolHistoryAndPublishFacet.runPublishSideEffects(protocolHost(this), args);

0 commit comments

Comments
 (0)