Skip to content

Commit 34eda28

Browse files
committed
fix(app-shell): a rolled-back Studio publish shows its banner again
formatPublishFailures compared the rolled-back sibling code to a lower-case 'batch_aborted', while publishPackageDrafts emits 'BATCH_ABORTED'. The banner branch never ran: the author got one parallel line per draft. BATCH_ABORTED_CODE is now 'BATCH_ABORTED', compared exactly (no case-folding), and tied to the spec's error-code ledger with a type-only 'satisfies RegisteredErrorCode'. The test fixtures use the producer's spelling, and a new pin covers the producer's rollback answer plus a causal-only control. publishRefusal.ts's header sentence describing the old bug is corrected (comment only). Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent fff07fb commit 34eda28

4 files changed

Lines changed: 69 additions & 15 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
A rolled-back Studio publish shows its "Nothing was published" banner again (objectui#11985).
6+
7+
When a package publish fails, the server rolls the whole batch back and marks every draft that went down with the failing one with the code `BATCH_ABORTED`. Studio's publish message compared that code to a lower-case `batch_aborted`, so the match never happened: instead of the banner, the cause and a count of the aborted drafts, the author got one parallel line per draft, each sibling reading as if it had failed on its own. Studio's publish toasts and the AI chat page's publish summary now match `BATCH_ABORTED` exactly as the server sends it, so they lead with the banner, name the item that caused the rollback and count the rest. A publish that fails without rolling anything back still lists each failed draft with its own error.
8+
9+
Nothing is added to the package entry: no export, prop, type member or language-pack key.

‎packages/app-shell/src/preview/publishRefusal.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
* When every element is aborted (the producer could not attribute the
2121
* failure), the first one's sentence is still the server's own.
2222
*
23-
* Not `formatPublishFailures` (`studio-design/metadataError.ts`), and measured
24-
* rather than assumed: it tells a rolled-back sibling by the pre-ADR-0112
25-
* spelling `batch_aborted` compared with `===`, so on the producer's
26-
* `BATCH_ABORTED` it lists every sibling as a parallel error and never reaches
27-
* its own rolled-back banner; and its banner is English, where the package
28-
* sheet answers in its own localized rows. `errorCodeIs` matches both spellings.
23+
* Not `formatPublishFailures` (`studio-design/metadataError.ts`): its banner is
24+
* English, where the package sheet answers in its own localized rows. The two
25+
* also tell a rolled-back sibling apart differently: `errorCodeIs` here ignores
26+
* case, while `formatPublishFailures` matches the producer's `BATCH_ABORTED`
27+
* exactly (objectui#11985).
2928
*/
3029

3130
import { errorCodeIs } from '@object-ui/types';

‎packages/app-shell/src/views/studio-design/metadataError.test.ts‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ describe('formatPublishFailures', () => {
2424
});
2525

2626
// framework 15.1+ (ADR-0067 D2) — the batch is all-or-nothing; `failed[]`
27-
// carries the causal item + batch_aborted markers for the rolled-back rest.
27+
// carries the causal item + BATCH_ABORTED markers for the rolled-back rest.
2828
it('15.1+ all-or-nothing: one rolled-back banner anchored on the causal item', () => {
2929
const out = formatPublishFailures([
30-
{ type: 'object', name: 'crm_lead', error: 'not published — the batch is all-or-nothing…', code: 'batch_aborted' },
30+
{ type: 'object', name: 'crm_lead', error: 'not published — the batch is all-or-nothing…', code: 'BATCH_ABORTED' },
3131
{
32-
type: 'object', name: 'crm_deal', error: 'failed spec validation', code: 'invalid_metadata',
32+
type: 'object', name: 'crm_deal', error: 'failed spec validation', code: 'INVALID_METADATA',
3333
issues: [{ path: 'fields.amount.type', message: 'Required' }],
3434
},
35-
{ type: 'view', name: 'lead_list', error: 'not published — …', code: 'batch_aborted' },
35+
{ type: 'view', name: 'lead_list', error: 'not published — …', code: 'BATCH_ABORTED' },
3636
]);
3737
expect(out).toContain('Nothing was published — the batch rolled back');
3838
// causal item with its real error and field-anchored issues…
@@ -45,10 +45,49 @@ describe('formatPublishFailures', () => {
4545

4646
it('all entries aborted (defensive): banner still renders with one sample', () => {
4747
const out = formatPublishFailures([
48-
{ type: 'object', name: 'a', error: 'not published — …', code: 'batch_aborted' },
49-
{ type: 'object', name: 'b', error: 'not published — …', code: 'batch_aborted' },
48+
{ type: 'object', name: 'a', error: 'not published — …', code: 'BATCH_ABORTED' },
49+
{ type: 'object', name: 'b', error: 'not published — …', code: 'BATCH_ABORTED' },
5050
]);
5151
expect(out).toContain('Nothing was published');
5252
expect(out).toContain('object/a');
5353
});
5454
});
55+
56+
// objectui#11985 — the fixtures above once spelled the sibling code
57+
// `batch_aborted`, a spelling `publishPackageDrafts` never emits, so the banner
58+
// branch was green in tests and dead in Studio. These two cases use the
59+
// producer's rollback answer verbatim: a causal item, then each sibling with
60+
// the producer's own sentence and `code: 'BATCH_ABORTED'`.
61+
describe('formatPublishFailures on the producer rollback answer (objectui#11985)', () => {
62+
const sibling = (type: string, name: string) => ({
63+
type,
64+
name,
65+
error:
66+
'not published — the batch is all-or-nothing (ADR-0067 D2) and object/lead failed; the transaction rolled back',
67+
code: 'BATCH_ABORTED',
68+
});
69+
70+
it('a rolled-back batch leads with the banner, names the cause and counts the aborted drafts', () => {
71+
const out = formatPublishFailures([
72+
{ type: 'object', name: 'lead', error: 'failed spec validation', code: 'INVALID_METADATA' },
73+
sibling('view', 'lead_list'),
74+
sibling('page', 'lead_home'),
75+
]);
76+
const lines = out.split('\n');
77+
expect(lines).toHaveLength(3);
78+
expect(lines[0]).toContain('Nothing was published');
79+
expect(lines[1]).toBe('object/lead: failed spec validation');
80+
expect(lines[2]).toContain('2 other drafts aborted');
81+
// The siblings are counted, not listed as parallel errors.
82+
expect(out).not.toContain('view/lead_list');
83+
expect(out).not.toContain('page/lead_home');
84+
});
85+
86+
it('control: a causal failure with no aborted sibling keeps its own error and no banner', () => {
87+
const out = formatPublishFailures([
88+
{ type: 'object', name: 'lead', error: 'failed spec validation', code: 'INVALID_METADATA' },
89+
]);
90+
expect(out).not.toContain('Nothing was published');
91+
expect(out).toBe('object/lead: failed spec validation');
92+
});
93+
});

‎packages/app-shell/src/views/studio-design/metadataError.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '@object-ui/data-objectstack';
2828
import { resolveFlowTriggerKind } from '@objectstack/spec/automation';
2929
import { DashboardWidgetSchema, resolveI18nLabel, type I18nLabel } from '@objectstack/spec/ui';
30+
import type { RegisteredErrorCode } from '@objectstack/spec/api';
3031
import type { NavTargetLabelResolver } from '@object-ui/layout';
3132
import type { MetadataSelection } from '../metadata-admin/preview-registry.js';
3233
import { t, tFormat, translateValidationMessage } from '../metadata-admin/i18n.js';
@@ -44,7 +45,7 @@ export interface PublishFailure {
4445
type: string;
4546
name: string;
4647
error: string;
47-
/** Machine code — `batch_aborted` marks a draft rolled back with the batch (ADR-0067 D2). */
48+
/** Machine code — `BATCH_ABORTED` marks a draft rolled back with the batch (ADR-0067 D2). */
4849
code?: string;
4950
issues?: MetadataValidationIssue[];
5051
}
@@ -53,8 +54,14 @@ export interface PublishFailure {
5354
* framework 15.1+ (ADR-0067 D2): package publishes are ALL-OR-NOTHING. A
5455
* failed batch reports every draft in `failed[]` — the causal item with its
5556
* real error, the rest with this code — and `publishedCount: 0`.
57+
*
58+
* Spelled exactly as `publishPackageDrafts` emits it and as the spec's
59+
* error-code ledger registers it, and compared exactly: ⛔ no case-folding
60+
* (objectui#11985). The `satisfies` ties the literal to the ledger at compile
61+
* time, so a code the spec does not register does not compile; the import is
62+
* type-only and adds no runtime bytes.
5663
*/
57-
export const BATCH_ABORTED_CODE = 'batch_aborted';
64+
export const BATCH_ABORTED_CODE = 'BATCH_ABORTED' satisfies RegisteredErrorCode;
5865

5966
/**
6067
* Format the `failed[]` from a publish response (the server returns 200 with
@@ -63,7 +70,7 @@ export const BATCH_ABORTED_CODE = 'batch_aborted';
6370
* Two server generations produce two shapes (both handled):
6471
* - **15.1+ all-or-nothing** (ADR-0067 D2): the batch rolled back atomically —
6572
* render ONE rolled-back banner anchored on the causal item(s), not N
66-
* parallel errors (`batch_aborted` entries are consequences, not causes).
73+
* parallel errors (`BATCH_ABORTED` entries are consequences, not causes).
6774
* - **pre-15.1 partial publish**: each failed draft gets a heading and, when
6875
* the failure was a validation error, its field-anchored issues indented
6976
* below.

0 commit comments

Comments
 (0)