Skip to content

Commit 35d942e

Browse files
fix(metadata-protocol): publish-drafts widget probes run as the publishing caller (#25533)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuDWJsBGQpadCH8JR2xZQ5re)_ Fixes #25484 Clause-②: no Metadata-protocol: none ## What was wrong `POST /api/v1/packages/:id/publish-drafts` runs the ADR-0038 L3 build probes after it publishes. The dashboard-widget probe called `analytics.queryDataset(dataset, selection, undefined)`, with no execution context. The analytics service's object-level read admission refuses a read that has no context. So every widget whose dataset reads a permission-gated object came back as an error-tier `widget_query_failed` ("[Analytics] Access denied: reading ... is not permitted for this user"), and the fix text told the author to change a dataset that renders correctly. The probe's docblock says it takes "the SAME `queryDataset` path the dashboard renderer hits". The renderer passes the caller's context, and now the probe does too. ## The change - `packages/runtime/src/domains/packages.ts` (the publish-drafts door): the caller's `executionContext` now goes to `publishPackageDrafts` on every request. Before, it went only when the request named a `backfill`. The request type already declared `context?: unknown`, so no interface changes. - `packages/metadata-protocol/src/protocol.ts` (`publishPackageDrafts`): passes `request.context` to `runBuildProbes`. One added line plus a docblock sentence. - `packages/metadata-protocol/src/build-probes.ts`: `RunBuildProbesOptions` gains an optional `context`, and the widget probe runs `queryDataset` with it. The probe never falls back to a system context. A probe that read as the system would report a widget healthy even when its own publisher cannot open it. - Same file, a follow-on of the same change: `empty_query` ("returns NO data although object X has rows") is now judged over the rows that caller can see. Before, a system count sat beside a caller-scoped query, and that would report a row-scoped empty answer as broken. With no context, the plane behaves exactly as before: a context-less query and a system count. **What a publisher who cannot read the dataset sees:** the same error-tier `widget_query_failed`, with the access-denied message. Its severity is unchanged on purpose, because that widget will not render for that caller either. Only the false error caused by the missing context goes away. ## Tests - `packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts` (new) runs the real path: `HttpDispatcher.handlePackages` → the real `ObjectStackProtocolImplementation.publishPackageDrafts` → `runBuildProbes` → a real `AnalyticsService` whose `admitObjectRead` admits only the reader on the gated object. The draft store and the phase-1/phase-2 publish seams are the doubles `packages/objectql/src/build-probes.test.ts` already uses. - A reader gets no error-tier issue, and admission is asked about that caller and never about a system principal. - A publisher who cannot read gets exactly one error-tier `widget_query_failed`. - `packages/metadata-protocol/src/build-probes-caller-context.test.ts` (new): `queryDataset` receives the caller context, and the rows-exist count runs as that caller. With no context, a context-less query and a system count still produce `empty_query`. - `packages-publish-drafts-response-conformance.test.ts`: the pin "without the key, the protocol is sent neither a backfill nor a context" now reads "no backfill, but THIS caller's context (not a system one)". - **Ablation** (fix committed first, through `scripts/ablation-replace.mjs`): `queryDataset(dataset, selection, context)` was changed to pass an undefined global marker. `ablation-dist-preflight` found the marker present in 2 built files of `metadata-protocol/dist`. Both new runtime cases went red. The reader case was the expected red. The non-reader case went red too, but only on its "admission saw this caller" assertion. The issue it pins was still produced, because a context-less read is refused too. Restore: blob back equal to HEAD (`d23a96693a7e`) and `git diff HEAD` empty, rebuild, `--absent` preflight clean on all 24 built files, and the 2 cases green again. ## Acceptance notes - The publish-drafts `widget_query_failed` fix text ("Fix the dataset definition ... so the query compiles") also prints for a real access denial, where the dataset is fine and the publisher lacks read access. This change leaves that text alone, as the card scoped it. Recorded, not filed. - `protocol.ts` is being split under #25467. The one hunk here sits inside `publishPackageDrafts` (around line 27146). The open slice PR #25518 edits lines 301, 2301 and 9254-9450 only, so the two diffs do not overlap. - `scripts/engine-double-contract.pinned.json` gains the new test's engine double, written by `check-engine-double-contract.mjs --write` as that gate asked (new pinned coverage). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4df174b commit 35d942e

9 files changed

Lines changed: 318 additions & 15 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix: `POST /api/v1/packages/:id/publish-drafts` probes dashboard widgets as the publishing caller, so a correct dataset over a permission-gated object is no longer reported as an error-tier `widget_query_failed`
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The build probes that run after a package publish called the analytics service's `queryDataset` with no execution context. Read admission refuses a read with no context, so every dashboard widget bound to a dataset over a permission-gated object came back as `widget_query_failed` ("Access denied: reading … is not permitted for this user"), with a fix telling the author to change a dataset that renders correctly.
12+
13+
- **`@objectstack/runtime`:** the publish-drafts route now passes the caller's execution context to `publishPackageDrafts` on every request. Before, it passed one only when the request named a `backfill`.
14+
- **`@objectstack/metadata-protocol`:** `publishPackageDrafts` passes that context to `runBuildProbes`, which takes a new optional `context`. The widget probe runs `queryDataset` as that caller, the same path the dashboard renderer takes. "Empty although the object has rows" is now judged over the rows that caller can see.
15+
16+
The probe never falls back to a system context. A publisher who cannot read the dataset still gets the error-tier `widget_query_failed`, because that widget will not render for them either.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25484] The dashboard-widget plane reads as the caller it is handed.
5+
*
6+
* `queryDataset` receives the caller's context (the renderer's path), and the
7+
* "empty although the object has rows" comparison counts the rows THAT caller
8+
* can see. A system count beside a caller-scoped query would call a row-scoped
9+
* empty answer broken. With no context the plane is unchanged: a context-less
10+
* query, and a system count.
11+
*
12+
* The door-to-admission leg is pinned in `@objectstack/runtime`
13+
* (`packages-publish-drafts-probe-caller-context.test.ts`).
14+
*/
15+
16+
import { describe, expect, it, vi } from 'vitest';
17+
import { runBuildProbes, type ProbeEngine } from './build-probes.js';
18+
19+
const ITEMS: Record<string, unknown> = {
20+
'dashboard board': { name: 'board', widgets: [{ id: 'w1', dataset: 'ds', values: ['total'] }] },
21+
'dataset ds': { name: 'ds', object: 'task_item', measures: [{ name: 'total', aggregate: 'count' }] },
22+
};
23+
const getItem = async (type: string, name: string) => ITEMS[`${type} ${name}`];
24+
25+
const CALLER = { userId: 'u_row_scoped' };
26+
27+
/** Rows exist for the system; the row-scoped caller sees none. Honours `limit`. */
28+
function engineVisibleToSystemOnly(): ProbeEngine & { contexts: unknown[] } {
29+
const contexts: unknown[] = [];
30+
return {
31+
contexts,
32+
find: async (_object: string, query: unknown) => {
33+
const q = query as { limit?: unknown; context?: { isSystem?: boolean } } | undefined;
34+
contexts.push(q?.context);
35+
const rows = q?.context?.isSystem ? [{ id: 'r1' }, { id: 'r2' }] : [];
36+
return typeof q?.limit === 'number' ? rows.slice(0, q.limit) : rows;
37+
},
38+
};
39+
}
40+
41+
describe('runBuildProbes — the widget plane reads as the caller (#25484)', () => {
42+
it('hands queryDataset the caller context and counts the rows that caller sees', async () => {
43+
const engine = engineVisibleToSystemOnly();
44+
const analytics = { queryDataset: vi.fn(async (..._args: unknown[]) => ({ rows: [] })) };
45+
const report = await runBuildProbes({
46+
engine, getItem, analytics, context: CALLER,
47+
published: [{ type: 'dashboard', name: 'board' }],
48+
});
49+
50+
expect(analytics.queryDataset).toHaveBeenCalledOnce();
51+
expect(analytics.queryDataset.mock.calls[0]![2]).toBe(CALLER);
52+
expect(engine.contexts).toEqual([CALLER]);
53+
expect(report.checked.widgets).toBe(1);
54+
expect(report.issues).toEqual([]);
55+
});
56+
57+
it('with no context: a context-less query and a system count, as before', async () => {
58+
const engine = engineVisibleToSystemOnly();
59+
const analytics = { queryDataset: vi.fn(async (..._args: unknown[]) => ({ rows: [] })) };
60+
const report = await runBuildProbes({
61+
engine, getItem, analytics,
62+
published: [{ type: 'dashboard', name: 'board' }],
63+
});
64+
65+
expect(analytics.queryDataset.mock.calls[0]![2]).toBeUndefined();
66+
expect(engine.contexts).toEqual([{ isSystem: true }]);
67+
expect(report.issues.map((i) => [i.code, i.severity])).toEqual([['empty_query', 'error']]);
68+
});
69+
});

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ export interface RunBuildProbesOptions {
8181
analytics?: ProbeAnalytics;
8282
/** Threaded into engine/analytics reads (tenant scoping). */
8383
organizationId?: string | null;
84+
/**
85+
* The PUBLISHING caller's execution context. Widget probes run
86+
* `queryDataset` as this caller, because that is what the renderer does:
87+
* a widget is healthy when the person who sees it can read it, so the
88+
* probe asks read admission the question the dashboard will ask.
89+
*
90+
* ⛔ Never substitute a system context when it is absent. A probe that
91+
* reads as the system reports a widget healthy that its own publisher
92+
* cannot open, and that is the false clean this plane exists to prevent.
93+
* Absent, the read goes out context-less and read admission answers for
94+
* it — a refusal, reported as such.
95+
*/
96+
context?: unknown;
8497
}
8598

8699
type Rec = Record<string, unknown>;
@@ -103,17 +116,22 @@ function resultRows(result: unknown): unknown[] {
103116
return [];
104117
}
105118

106-
/** True when the object has at least one row (probe read, isSystem). */
119+
/**
120+
* True when the object has at least one row. A system read by default; the
121+
* widget plane passes the caller's context so "the object has rows" is judged
122+
* over the rows that caller can see — the same rows its dataset query saw.
123+
*/
107124
async function hasRows(
108125
engine: ProbeEngine,
109126
objectName: string,
110127
organizationId?: string | null,
128+
readContext: unknown = { isSystem: true },
111129
): Promise<boolean> {
112130
const rows = await engine.find(objectName, {
113131
fields: ['id'],
114132
limit: 1,
115133
...(organizationId ? { where: { organization_id: organizationId } } : {}),
116-
context: { isSystem: true },
134+
context: readContext,
117135
});
118136
return Array.isArray(rows) && rows.length > 0;
119137
}
@@ -139,7 +157,7 @@ async function hasRows(
139157
export async function runBuildProbes(opts: RunBuildProbesOptions): Promise<BuildProbeReport> {
140158
const issues: RuntimeBuildIssue[] = [];
141159
const checked = { seeds: 0, views: 0, widgets: 0, objects: 0 };
142-
const { engine, getItem, published, analytics, organizationId } = opts;
160+
const { engine, getItem, published, analytics, organizationId, context } = opts;
143161

144162
// Memoized active-item reads (a dashboard and its widgets share datasets).
145163
const itemCache = new Map<string, unknown | undefined>();
@@ -340,9 +358,21 @@ export async function runBuildProbes(opts: RunBuildProbesOptions): Promise<Build
340358

341359
const objectName = typeof dataset.object === 'string' ? dataset.object : undefined;
342360
try {
343-
const result = await analytics.queryDataset(dataset, selection, undefined);
361+
// [#25484] As the publishing caller — the renderer's path. With
362+
// no context here, read admission refused every dataset over a
363+
// permission-gated object, and a correct widget was reported as
364+
// an error-tier `widget_query_failed`. A caller who genuinely
365+
// cannot read the dataset is still refused, and still reported.
366+
const result = await analytics.queryDataset(dataset, selection, context);
344367
const rows = resultRows(result);
345-
if (rows.length === 0 && objectName && (await hasRows(engine, objectName, organizationId))) {
368+
// "Empty although the object has rows" is judged over the rows
369+
// THIS caller can see: a system count would call a row-scoped
370+
// empty answer broken.
371+
const rowsContext = context && typeof context === 'object' ? context : undefined;
372+
if (
373+
rows.length === 0 && objectName
374+
&& (await hasRows(engine, objectName, organizationId, rowsContext))
375+
) {
346376
issues.push({
347377
layer: 'runtime',
348378
severity: 'error',

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25015,7 +25015,9 @@ export class ObjectStackProtocolImplementation implements
2501525015
* [#24253] The caller's execution context: the principal the
2501625016
* `backfill` writes run as, so object permissions, RLS/FLS and record
2501725017
* hooks apply exactly as they do to that caller's own edit. Required
25018-
* when `backfill` names anything; nothing else here reads it.
25018+
* when `backfill` names anything. [#25484] The build probes read as it
25019+
* too: a dashboard widget is probed through `queryDataset` as this
25020+
* caller, the renderer's path, never as the system.
2501925021
*/
2502025022
context?: unknown;
2502125023
}): Promise<{
@@ -25899,6 +25901,7 @@ export class ObjectStackProtocolImplementation implements
2589925901
published,
2590025902
...(analytics && typeof analytics.queryDataset === 'function' ? { analytics } : {}),
2590125903
organizationId: null,
25904+
...(request.context !== undefined ? { context: request.context } : {}),
2590225905
});
2590325906
} catch {
2590425907
probes = undefined;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25484] `POST /packages/:id/publish-drafts` probes a published dashboard's
5+
* widgets AS THE PUBLISHING CALLER.
6+
*
7+
* The probe docblock promises "the SAME `queryDataset` path the dashboard
8+
* renderer hits", and the renderer passes the caller's context. The probe
9+
* passed none, so the analytics service's object-level read admission refused
10+
* every dataset over a permission-gated object and a correct widget came back
11+
* as an error-tier `widget_query_failed` ("Access denied: reading … is not
12+
* permitted for this user") telling the author to fix a dataset that was fine.
13+
*
14+
* What is real here: the door (`HttpDispatcher.handlePackages`), the protocol's
15+
* `publishPackageDrafts`, `runBuildProbes`, and an `AnalyticsService` whose
16+
* `admitObjectRead` judges the context it is handed — the gate that refused.
17+
* What is a double: the draft store and the phase-1/phase-2 publish seams
18+
* (the same seams `packages/objectql/src/build-probes.test.ts` stubs), and the
19+
* driver the analytics strategy reads.
20+
*
21+
* The two legs are each other's control: the reader's publish must come back
22+
* clean, and the non-reader's must still carry the refusal, so neither "the
23+
* probe stopped running" nor "the probe now reads as the system" can pass.
24+
*/
25+
import { afterEach, describe, expect, it, vi } from 'vitest';
26+
import { DatasetSchema } from '@objectstack/spec/ui';
27+
import type { ExecutionContext } from '@objectstack/spec/kernel';
28+
import { AnalyticsService } from '@objectstack/service-analytics';
29+
import { assertEngineFindOnePredicate } from '@objectstack/metadata-core';
30+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
31+
import { HttpDispatcher } from '../http-dispatcher.js';
32+
33+
/** The object read admission walls off from everyone but the reader. */
34+
const GATED = 'task_item';
35+
36+
const DATASET = DatasetSchema.parse({
37+
name: 'task_metrics',
38+
label: 'Task metrics',
39+
object: GATED,
40+
dimensions: [],
41+
measures: [{ name: 'task_count', label: 'Tasks', aggregate: 'count' }],
42+
});
43+
44+
const ITEMS: Record<string, unknown> = {
45+
'dashboard task_board': {
46+
name: 'task_board',
47+
widgets: [{ id: 'w_tasks', dataset: 'task_metrics', values: ['task_count'] }],
48+
},
49+
'dataset task_metrics': DATASET,
50+
};
51+
52+
/** Both callers may publish; only the reader may read `task_item`. */
53+
const caller = (userId: string) => ({
54+
request: {},
55+
executionContext: {
56+
userId,
57+
systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'],
58+
},
59+
}) as any;
60+
const READER = 'u_reader';
61+
const NON_READER = 'u_publisher_without_read';
62+
63+
function makeDoor() {
64+
const admissionContexts: Array<ExecutionContext | undefined> = [];
65+
const analytics = new AnalyticsService({
66+
queryCapabilities: () => ({ nativeSql: false, objectqlAggregate: true, inMemory: false }),
67+
admitObjectRead: (object, context) => {
68+
admissionContexts.push(context);
69+
return object !== GATED || context?.userId === READER;
70+
},
71+
executeAggregate: async () => [{ task_count: 7 }],
72+
});
73+
74+
const protocol = new ObjectStackProtocolImplementation({} as never);
75+
const p = protocol as any;
76+
p.ensureOverlayIndex = async () => {};
77+
p.getOverlayRepo = () => ({
78+
listDrafts: async () => [
79+
{ type: 'dataset', name: 'task_metrics' },
80+
{ type: 'dashboard', name: 'task_board' },
81+
],
82+
get: async (ref: any, opts: any) =>
83+
opts?.state === 'draft' ? { body: ITEMS[`${ref.type} ${ref.name}`], hash: 'h' } : null,
84+
});
85+
vi.spyOn(p, 'promoteDraftForPublish').mockImplementation(async (req: any) => ({
86+
singularType: req.type,
87+
orgId: null,
88+
advisories: [],
89+
result: { version: 'h', seq: 1, item: { body: ITEMS[`${req.type} ${req.name}`] }, packageId: null },
90+
}));
91+
vi.spyOn(p, 'runPublishSideEffects').mockResolvedValue({});
92+
p.getMetaItem = async ({ type, name }: any) => ({ item: ITEMS[`${type} ${name}`] });
93+
p.getMetaItems = async () => ({ items: [] });
94+
const engineFinds: Array<{ object: string; context: unknown }> = [];
95+
p.engine = {
96+
find: async (object: string, query: any) => {
97+
engineFinds.push({ object, context: query?.context });
98+
const rows = [{ id: 'r1' }, { id: 'r2' }];
99+
return typeof query?.limit === 'number' ? rows.slice(0, query.limit) : rows;
100+
},
101+
// The ADR-0067 pre-publish capture: never published, so no active row.
102+
findOne: async (object: string, query: unknown) => {
103+
assertEngineFindOnePredicate(object, query as never);
104+
return null;
105+
},
106+
// The ADR-0067 commit row the batch writes; not under test here.
107+
insert: async (_object: string, row: unknown) => row,
108+
};
109+
p.getServicesRegistry = () => new Map<string, unknown>([['analytics', analytics]]);
110+
111+
const kernel: any = {
112+
getService: (name: string) => {
113+
if (name === 'protocol') return Promise.resolve(protocol);
114+
if (name === 'objectql') {
115+
return Promise.resolve({
116+
insert: vi.fn(), find: vi.fn(), update: vi.fn(),
117+
registry: { getAllPackages: vi.fn().mockReturnValue([]) },
118+
});
119+
}
120+
if (name === 'metadata') return Promise.resolve({ getObject: vi.fn() });
121+
return null;
122+
},
123+
context: { getService: () => null, trigger: vi.fn() },
124+
};
125+
return { dispatcher: new HttpDispatcher(kernel), admissionContexts, engineFinds };
126+
}
127+
128+
async function publishAs(userId: string) {
129+
const door = makeDoor();
130+
const result = await door.dispatcher.handlePackages(
131+
'/com.tasks/publish-drafts', 'POST', {}, {}, caller(userId),
132+
);
133+
expect(result.response?.status).toBe(200);
134+
const data: any = (result.response as any)?.body?.data;
135+
expect(data?.outcome).toBe('published');
136+
return { ...door, probes: data?.probes };
137+
}
138+
139+
afterEach(() => vi.restoreAllMocks());
140+
141+
describe('publish-drafts widget probes run as the publishing caller (#25484)', () => {
142+
it('a correct dataset over a gated object the caller CAN read yields no error-tier issue', async () => {
143+
const { probes, admissionContexts, engineFinds } = await publishAs(READER);
144+
145+
expect(probes?.checked?.widgets).toBe(1);
146+
expect(probes.issues.filter((i: any) => i.severity === 'error')).toEqual([]);
147+
// Admission was asked about the caller, never about a system principal.
148+
expect(admissionContexts.length).toBeGreaterThan(0);
149+
for (const ctx of admissionContexts) {
150+
expect(ctx?.userId).toBe(READER);
151+
expect((ctx as any)?.isSystem).toBeUndefined();
152+
}
153+
// No system read was used to judge this widget either.
154+
expect(engineFinds.filter((f) => (f.context as any)?.isSystem)).toEqual([]);
155+
});
156+
157+
it('a caller who CANNOT read the dataset still gets the error-tier widget_query_failed', async () => {
158+
const { probes, admissionContexts } = await publishAs(NON_READER);
159+
160+
expect(probes?.checked?.widgets).toBe(1);
161+
const failed = probes.issues.filter((i: any) => i.code === 'widget_query_failed'); // adr0112-ok: D6c build-probe diagnostics code
162+
expect(failed).toHaveLength(1);
163+
expect(failed[0]).toMatchObject({
164+
layer: 'runtime',
165+
severity: 'error',
166+
artifact: { type: 'dashboard', name: 'task_board' },
167+
ref: { type: 'dataset', name: 'task_metrics', member: 'w_tasks' },
168+
});
169+
expect(admissionContexts.every((ctx) => ctx?.userId === NON_READER)).toBe(true);
170+
});
171+
});

‎packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,13 @@ describe('publish-drafts backfill crosses the door (#24253)', () => {
312312
expect(PublishPackageDraftsResponseSchema.parse(data).backfillApplied).toEqual(stopped);
313313
});
314314

315-
it('without the key, the protocol is sent neither a backfill nor a context', async () => {
315+
it('without the key, the protocol is sent no backfill but still THIS caller\'s context (#25484: the build probes read as it)', async () => {
316316
const { publishPackageDrafts } = await publishDrafts({ apps: [] });
317317

318318
const sent = publishPackageDrafts.mock.calls[0]![0];
319319
expect('backfill' in sent).toBe(false);
320-
expect('context' in sent).toBe(false);
320+
expect(sent.context).toEqual(PKG_ADMIN().executionContext);
321+
expect(sent.context?.isSystem).toBeUndefined();
321322
});
322323

323324
it('the protocol\'s request refusal answers 400 VALIDATION_ERROR with its issues, nothing flipped or announced', async () => {

‎packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,12 @@ describe('#15068 · 0 · the publish-then-read path really runs', () => {
390390
const { publishRequest } = await publishThenRead({ activeOrganizationId: ORG });
391391

392392
// The door threads no organization into `publishPackageDrafts` (nor
393-
// into `applyPublishedSeeds`), even with one active.
394-
expect(publishRequest()).toEqual({ packageId: PKG });
393+
// into `applyPublishedSeeds`), even with one active. [#25484] The
394+
// caller's execution context does ride along, verbatim: the build
395+
// probes read a widget's dataset AS that caller (the renderer's path),
396+
// and nothing in the publish derives a write scope from it.
397+
const caller = PKG_ADMIN(ORG).executionContext;
398+
expect(publishRequest()).toEqual({ packageId: PKG, context: caller });
395399
expect(publishRequest()).not.toHaveProperty('organizationId');
396400
});
397401
});

0 commit comments

Comments
 (0)