From 63141e606a086b921b4a97229df07d48c92b84bb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 18:32:30 +0000 Subject: [PATCH 1/7] fix(metadata-protocol): run publish-drafts widget probes as the publishing caller The dashboard-widget build probe called queryDataset with no execution context, so read admission refused every dataset over a permission-gated object and a correct widget was reported as an error-tier widget_query_failed. The publish-drafts door now always hands the protocol the caller's execution context, the protocol threads it into the probes, and the widget probe queries (and judges "object has rows") as that caller. Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../metadata-protocol/src/build-probes.ts | 40 ++++++++++++++++--- packages/metadata-protocol/src/protocol.ts | 5 ++- packages/runtime/src/domains/packages.ts | 14 ++++--- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/metadata-protocol/src/build-probes.ts b/packages/metadata-protocol/src/build-probes.ts index 287ab83e987..d23a96693a7 100644 --- a/packages/metadata-protocol/src/build-probes.ts +++ b/packages/metadata-protocol/src/build-probes.ts @@ -81,6 +81,19 @@ export interface RunBuildProbesOptions { analytics?: ProbeAnalytics; /** Threaded into engine/analytics reads (tenant scoping). */ organizationId?: string | null; + /** + * The PUBLISHING caller's execution context. Widget probes run + * `queryDataset` as this caller, because that is what the renderer does: + * a widget is healthy when the person who sees it can read it, so the + * probe asks read admission the question the dashboard will ask. + * + * ⛔ Never substitute a system context when it is absent. A probe that + * reads as the system reports a widget healthy that its own publisher + * cannot open, and that is the false clean this plane exists to prevent. + * Absent, the read goes out context-less and read admission answers for + * it — a refusal, reported as such. + */ + context?: unknown; } type Rec = Record; @@ -103,17 +116,22 @@ function resultRows(result: unknown): unknown[] { return []; } -/** True when the object has at least one row (probe read, isSystem). */ +/** + * True when the object has at least one row. A system read by default; the + * widget plane passes the caller's context so "the object has rows" is judged + * over the rows that caller can see — the same rows its dataset query saw. + */ async function hasRows( engine: ProbeEngine, objectName: string, organizationId?: string | null, + readContext: unknown = { isSystem: true }, ): Promise { const rows = await engine.find(objectName, { fields: ['id'], limit: 1, ...(organizationId ? { where: { organization_id: organizationId } } : {}), - context: { isSystem: true }, + context: readContext, }); return Array.isArray(rows) && rows.length > 0; } @@ -139,7 +157,7 @@ async function hasRows( export async function runBuildProbes(opts: RunBuildProbesOptions): Promise { const issues: RuntimeBuildIssue[] = []; const checked = { seeds: 0, views: 0, widgets: 0, objects: 0 }; - const { engine, getItem, published, analytics, organizationId } = opts; + const { engine, getItem, published, analytics, organizationId, context } = opts; // Memoized active-item reads (a dashboard and its widgets share datasets). const itemCache = new Map(); @@ -340,9 +358,21 @@ export async function runBuildProbes(opts: RunBuildProbesOptions): Promise Date: Fri, 23 Oct 2026 18:33:41 +0000 Subject: [PATCH 2/7] test(runtime): publish-drafts widget probes read as the publishing caller Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- ...ublish-drafts-probe-caller-context.test.ts | 163 ++++++++++++++++++ ...ublish-drafts-response-conformance.test.ts | 5 +- 2 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts diff --git a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts new file mode 100644 index 00000000000..62f3abdee7e --- /dev/null +++ b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts @@ -0,0 +1,163 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25484] `POST /packages/:id/publish-drafts` probes a published dashboard's + * widgets AS THE PUBLISHING CALLER. + * + * The probe docblock promises "the SAME `queryDataset` path the dashboard + * renderer hits", and the renderer passes the caller's context. The probe + * passed none, so the analytics service's object-level read admission refused + * every dataset over a permission-gated object and a correct widget came back + * as an error-tier `widget_query_failed` ("Access denied: reading … is not + * permitted for this user") telling the author to fix a dataset that was fine. + * + * What is real here: the door (`HttpDispatcher.handlePackages`), the protocol's + * `publishPackageDrafts`, `runBuildProbes`, and an `AnalyticsService` whose + * `admitObjectRead` judges the context it is handed — the gate that refused. + * What is a double: the draft store and the phase-1/phase-2 publish seams + * (the same seams `packages/objectql/src/build-probes.test.ts` stubs), and the + * driver the analytics strategy reads. + * + * The two legs are each other's control: the reader's publish must come back + * clean, and the non-reader's must still carry the refusal, so neither "the + * probe stopped running" nor "the probe now reads as the system" can pass. + */ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DatasetSchema } from '@objectstack/spec/ui'; +import type { ExecutionContext } from '@objectstack/spec/kernel'; +import { AnalyticsService } from '@objectstack/service-analytics'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; +import { HttpDispatcher } from '../http-dispatcher.js'; + +/** The object read admission walls off from everyone but the reader. */ +const GATED = 'task_item'; + +const DATASET = DatasetSchema.parse({ + name: 'task_metrics', + label: 'Task metrics', + object: GATED, + dimensions: [], + measures: [{ name: 'task_count', label: 'Tasks', aggregate: 'count' }], +}); + +const ITEMS: Record = { + 'dashboard task_board': { + name: 'task_board', + widgets: [{ id: 'w_tasks', dataset: 'task_metrics', values: ['task_count'] }], + }, + 'dataset task_metrics': DATASET, +}; + +/** Both callers may publish; only the reader may read `task_item`. */ +const caller = (userId: string) => ({ + request: {}, + executionContext: { + userId, + systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], + }, +}) as any; +const READER = 'u_reader'; +const NON_READER = 'u_publisher_without_read'; + +function makeDoor() { + const admissionContexts: Array = []; + const analytics = new AnalyticsService({ + queryCapabilities: () => ({ nativeSql: false, objectqlAggregate: true, inMemory: false }), + admitObjectRead: (object, context) => { + admissionContexts.push(context); + return object !== GATED || context?.userId === READER; + }, + executeAggregate: async () => [{ task_count: 7 }], + }); + + const protocol = new ObjectStackProtocolImplementation({} as never); + const p = protocol as any; + p.ensureOverlayIndex = async () => {}; + p.getOverlayRepo = () => ({ + listDrafts: async () => [ + { type: 'dataset', name: 'task_metrics' }, + { type: 'dashboard', name: 'task_board' }, + ], + get: async (ref: any, opts: any) => + opts?.state === 'draft' ? { body: ITEMS[`${ref.type} ${ref.name}`], hash: 'h' } : null, + }); + vi.spyOn(p, 'promoteDraftForPublish').mockImplementation(async (req: any) => ({ + singularType: req.type, + orgId: null, + advisories: [], + result: { version: 'h', seq: 1, item: { body: ITEMS[`${req.type} ${req.name}`] }, packageId: null }, + })); + vi.spyOn(p, 'runPublishSideEffects').mockResolvedValue({}); + p.getMetaItem = async ({ type, name }: any) => ({ item: ITEMS[`${type} ${name}`] }); + p.getMetaItems = async () => ({ items: [] }); + const engineFinds: Array<{ object: string; context: unknown }> = []; + p.engine = { + find: async (object: string, query: any) => { + engineFinds.push({ object, context: query?.context }); + return [{ id: 'r1' }]; + }, + findOne: async () => null, + }; + p.getServicesRegistry = () => new Map([['analytics', analytics]]); + + const kernel: any = { + getService: (name: string) => { + if (name === 'protocol') return Promise.resolve(protocol); + if (name === 'objectql') { + return Promise.resolve({ + insert: vi.fn(), find: vi.fn(), update: vi.fn(), + registry: { getAllPackages: vi.fn().mockReturnValue([]) }, + }); + } + if (name === 'metadata') return Promise.resolve({ getObject: vi.fn() }); + return null; + }, + context: { getService: () => null, trigger: vi.fn() }, + }; + return { dispatcher: new HttpDispatcher(kernel), admissionContexts, engineFinds }; +} + +async function publishAs(userId: string) { + const door = makeDoor(); + const result = await door.dispatcher.handlePackages( + '/com.tasks/publish-drafts', 'POST', {}, {}, caller(userId), + ); + expect(result.response?.status).toBe(200); + const data: any = (result.response as any)?.body?.data; + expect(data?.outcome).toBe('published'); + return { ...door, probes: data?.probes }; +} + +afterEach(() => vi.restoreAllMocks()); + +describe('publish-drafts widget probes run as the publishing caller (#25484)', () => { + it('a correct dataset over a gated object the caller CAN read yields no error-tier issue', async () => { + const { probes, admissionContexts, engineFinds } = await publishAs(READER); + + expect(probes?.checked?.widgets).toBe(1); + expect(probes.issues.filter((i: any) => i.severity === 'error')).toEqual([]); + // Admission was asked about the caller, never about a system principal. + expect(admissionContexts.length).toBeGreaterThan(0); + for (const ctx of admissionContexts) { + expect(ctx?.userId).toBe(READER); + expect((ctx as any)?.isSystem).toBeUndefined(); + } + // No system read was used to judge this widget either. + expect(engineFinds.filter((f) => (f.context as any)?.isSystem)).toEqual([]); + }); + + it('a caller who CANNOT read the dataset still gets the error-tier widget_query_failed', async () => { + const { probes, admissionContexts } = await publishAs(NON_READER); + + expect(probes?.checked?.widgets).toBe(1); + const failed = probes.issues.filter((i: any) => i.code === 'widget_query_failed'); + expect(failed).toHaveLength(1); + expect(failed[0]).toMatchObject({ + layer: 'runtime', + severity: 'error', + artifact: { type: 'dashboard', name: 'task_board' }, + ref: { type: 'dataset', name: 'task_metrics', member: 'w_tasks' }, + }); + expect(admissionContexts.every((ctx) => ctx?.userId === NON_READER)).toBe(true); + }); +}); diff --git a/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts b/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts index 49f135eab6c..c02c2c8bff4 100644 --- a/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts +++ b/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts @@ -312,12 +312,13 @@ describe('publish-drafts backfill crosses the door (#24253)', () => { expect(PublishPackageDraftsResponseSchema.parse(data).backfillApplied).toEqual(stopped); }); - it('without the key, the protocol is sent neither a backfill nor a context', async () => { + it('without the key, the protocol is sent no backfill but still THIS caller\'s context (#25484: the build probes read as it)', async () => { const { publishPackageDrafts } = await publishDrafts({ apps: [] }); const sent = publishPackageDrafts.mock.calls[0]![0]; expect('backfill' in sent).toBe(false); - expect('context' in sent).toBe(false); + expect(sent.context).toEqual(PKG_ADMIN().executionContext); + expect(sent.context?.isSystem).toBeUndefined(); }); it('the protocol\'s request refusal answers 400 VALIDATION_ERROR with its issues, nothing flipped or announced', async () => { From b88d53079d656443d3aaa438558fa024cdb94ff8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 18:44:34 +0000 Subject: [PATCH 3/7] test(metadata-protocol): widget probe reads and counts as the caller; changeset Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../25484-build-probe-caller-context.md | 16 +++++ .../src/build-probes-caller-context.test.ts | 69 +++++++++++++++++++ ...ublish-drafts-probe-caller-context.test.ts | 3 +- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/25484-build-probe-caller-context.md create mode 100644 packages/metadata-protocol/src/build-probes-caller-context.test.ts diff --git a/.changeset/25484-build-probe-caller-context.md b/.changeset/25484-build-probe-caller-context.md new file mode 100644 index 00000000000..b1dbe9fadfc --- /dev/null +++ b/.changeset/25484-build-probe-caller-context.md @@ -0,0 +1,16 @@ +--- +'@objectstack/metadata-protocol': patch +'@objectstack/runtime': patch +--- + +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` + +Clause-②: no +Metadata-protocol: none + +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. + +- **`@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`. +- **`@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. + +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. diff --git a/packages/metadata-protocol/src/build-probes-caller-context.test.ts b/packages/metadata-protocol/src/build-probes-caller-context.test.ts new file mode 100644 index 00000000000..a9ff9c25203 --- /dev/null +++ b/packages/metadata-protocol/src/build-probes-caller-context.test.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25484] The dashboard-widget plane reads as the caller it is handed. + * + * `queryDataset` receives the caller's context (the renderer's path), and the + * "empty although the object has rows" comparison counts the rows THAT caller + * can see. A system count beside a caller-scoped query would call a row-scoped + * empty answer broken. With no context the plane is unchanged: a context-less + * query, and a system count. + * + * The door-to-admission leg is pinned in `@objectstack/runtime` + * (`packages-publish-drafts-probe-caller-context.test.ts`). + */ + +import { describe, expect, it, vi } from 'vitest'; +import { runBuildProbes, type ProbeEngine } from './build-probes.js'; + +const ITEMS: Record = { + 'dashboard board': { name: 'board', widgets: [{ id: 'w1', dataset: 'ds', values: ['total'] }] }, + 'dataset ds': { name: 'ds', object: 'task_item', measures: [{ name: 'total', aggregate: 'count' }] }, +}; +const getItem = async (type: string, name: string) => ITEMS[`${type} ${name}`]; + +const CALLER = { userId: 'u_row_scoped' }; + +/** Rows exist for the system; the row-scoped caller sees none. Honours `limit`. */ +function engineVisibleToSystemOnly(): ProbeEngine & { contexts: unknown[] } { + const contexts: unknown[] = []; + return { + contexts, + find: async (_object: string, query: unknown) => { + const q = query as { limit?: unknown; context?: { isSystem?: boolean } } | undefined; + contexts.push(q?.context); + const rows = q?.context?.isSystem ? [{ id: 'r1' }, { id: 'r2' }] : []; + return typeof q?.limit === 'number' ? rows.slice(0, q.limit) : rows; + }, + }; +} + +describe('runBuildProbes — the widget plane reads as the caller (#25484)', () => { + it('hands queryDataset the caller context and counts the rows that caller sees', async () => { + const engine = engineVisibleToSystemOnly(); + const analytics = { queryDataset: vi.fn(async () => ({ rows: [] })) }; + const report = await runBuildProbes({ + engine, getItem, analytics, context: CALLER, + published: [{ type: 'dashboard', name: 'board' }], + }); + + expect(analytics.queryDataset).toHaveBeenCalledOnce(); + expect(analytics.queryDataset.mock.calls[0]![2]).toBe(CALLER); + expect(engine.contexts).toEqual([CALLER]); + expect(report.checked.widgets).toBe(1); + expect(report.issues).toEqual([]); + }); + + it('with no context: a context-less query and a system count, as before', async () => { + const engine = engineVisibleToSystemOnly(); + const analytics = { queryDataset: vi.fn(async () => ({ rows: [] })) }; + const report = await runBuildProbes({ + engine, getItem, analytics, + published: [{ type: 'dashboard', name: 'board' }], + }); + + expect(analytics.queryDataset.mock.calls[0]![2]).toBeUndefined(); + expect(engine.contexts).toEqual([{ isSystem: true }]); + expect(report.issues.map((i) => [i.code, i.severity])).toEqual([['empty_query', 'error']]); + }); +}); diff --git a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts index 62f3abdee7e..0d0c420ba31 100644 --- a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts +++ b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts @@ -94,7 +94,8 @@ function makeDoor() { p.engine = { find: async (object: string, query: any) => { engineFinds.push({ object, context: query?.context }); - return [{ id: 'r1' }]; + const rows = [{ id: 'r1' }, { id: 'r2' }]; + return typeof query?.limit === 'number' ? rows.slice(0, query.limit) : rows; }, findOne: async () => null, }; From 5e25f33a719efe7c526a4897e03185ef40643252 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 18:55:47 +0000 Subject: [PATCH 4/7] test(runtime): answer the commit-row insert in the probe-context harness Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../packages-publish-drafts-probe-caller-context.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts index 0d0c420ba31..599223f4baf 100644 --- a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts +++ b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts @@ -98,6 +98,8 @@ function makeDoor() { return typeof query?.limit === 'number' ? rows.slice(0, query.limit) : rows; }, findOne: async () => null, + // The ADR-0067 commit row the batch writes; not under test here. + insert: async (_object: string, row: unknown) => row, }; p.getServicesRegistry = () => new Map([['analytics', analytics]]); From 861e41fbbc70a6a116b7afcc7bc4752de261a7d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 19:34:39 +0000 Subject: [PATCH 5/7] test(runtime): route the probe harness findOne through the engine predicate; pin it Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../packages-publish-drafts-probe-caller-context.test.ts | 9 +++++++-- scripts/engine-double-contract.pinned.json | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts index 599223f4baf..c2feb47fcbc 100644 --- a/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts +++ b/packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts @@ -26,6 +26,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { DatasetSchema } from '@objectstack/spec/ui'; import type { ExecutionContext } from '@objectstack/spec/kernel'; import { AnalyticsService } from '@objectstack/service-analytics'; +import { assertEngineFindOnePredicate } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { HttpDispatcher } from '../http-dispatcher.js'; @@ -97,7 +98,11 @@ function makeDoor() { const rows = [{ id: 'r1' }, { id: 'r2' }]; return typeof query?.limit === 'number' ? rows.slice(0, query.limit) : rows; }, - findOne: async () => null, + // The ADR-0067 pre-publish capture: never published, so no active row. + findOne: async (object: string, query: unknown) => { + assertEngineFindOnePredicate(object, query as never); + return null; + }, // The ADR-0067 commit row the batch writes; not under test here. insert: async (_object: string, row: unknown) => row, }; @@ -153,7 +158,7 @@ describe('publish-drafts widget probes run as the publishing caller (#25484)', ( const { probes, admissionContexts } = await publishAs(NON_READER); expect(probes?.checked?.widgets).toBe(1); - const failed = probes.issues.filter((i: any) => i.code === 'widget_query_failed'); + const failed = probes.issues.filter((i: any) => i.code === 'widget_query_failed'); // adr0112-ok: D6c build-probe diagnostics code expect(failed).toHaveLength(1); expect(failed[0]).toMatchObject({ layer: 'runtime', diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index d589a3930b3..718f05270d8 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -4241,6 +4241,11 @@ "verb": "update", "pinned": 1 }, + { + "file": "packages/runtime/src/domains/packages-publish-drafts-probe-caller-context.test.ts", + "verb": "findOne", + "pinned": 1 + }, { "file": "packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts", "verb": "delete", From bb15e5f1c34fd84534d4fbf54e559a06cdbab130 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 20:18:47 +0000 Subject: [PATCH 6/7] test(metadata-protocol): type the queryDataset spy's arguments Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../metadata-protocol/src/build-probes-caller-context.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/metadata-protocol/src/build-probes-caller-context.test.ts b/packages/metadata-protocol/src/build-probes-caller-context.test.ts index a9ff9c25203..66a95e7e172 100644 --- a/packages/metadata-protocol/src/build-probes-caller-context.test.ts +++ b/packages/metadata-protocol/src/build-probes-caller-context.test.ts @@ -41,7 +41,7 @@ function engineVisibleToSystemOnly(): ProbeEngine & { contexts: unknown[] } { describe('runBuildProbes — the widget plane reads as the caller (#25484)', () => { it('hands queryDataset the caller context and counts the rows that caller sees', async () => { const engine = engineVisibleToSystemOnly(); - const analytics = { queryDataset: vi.fn(async () => ({ rows: [] })) }; + const analytics = { queryDataset: vi.fn(async (..._args: unknown[]) => ({ rows: [] })) }; const report = await runBuildProbes({ engine, getItem, analytics, context: CALLER, published: [{ type: 'dashboard', name: 'board' }], @@ -56,7 +56,7 @@ describe('runBuildProbes — the widget plane reads as the caller (#25484)', () it('with no context: a context-less query and a system count, as before', async () => { const engine = engineVisibleToSystemOnly(); - const analytics = { queryDataset: vi.fn(async () => ({ rows: [] })) }; + const analytics = { queryDataset: vi.fn(async (..._args: unknown[]) => ({ rows: [] })) }; const report = await runBuildProbes({ engine, getItem, analytics, published: [{ type: 'dashboard', name: 'board' }], From 1bb247c7b6a2a311172b8a2bebd71970571975d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 20:59:42 +0000 Subject: [PATCH 7/7] test(runtime): the org-scope pin reads the caller context the probes now carry Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude --- .../src/domains/packages-seed-apply-org-scope.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts b/packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts index bdd4241b2ab..c7ae6e9c546 100644 --- a/packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts +++ b/packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts @@ -390,8 +390,12 @@ describe('#15068 · 0 · the publish-then-read path really runs', () => { const { publishRequest } = await publishThenRead({ activeOrganizationId: ORG }); // The door threads no organization into `publishPackageDrafts` (nor - // into `applyPublishedSeeds`), even with one active. - expect(publishRequest()).toEqual({ packageId: PKG }); + // into `applyPublishedSeeds`), even with one active. [#25484] The + // caller's execution context does ride along, verbatim: the build + // probes read a widget's dataset AS that caller (the renderer's path), + // and nothing in the publish derives a write scope from it. + const caller = PKG_ADMIN(ORG).executionContext; + expect(publishRequest()).toEqual({ packageId: PKG, context: caller }); expect(publishRequest()).not.toHaveProperty('organizationId'); }); });