Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: a pooled real better-sqlite3 engine and a fresh AuthManager per case, each case's SCIM provision, ban, last-administrator and sign-in sequence its subject; 1.8 s in a local run; slowest CI reading 10.3 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up

/**
* [#14360] SCIM `active: false` disables the account again — end to end,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine (better-sqlite3 :memory:) and AuthManager in each of the 13 real-engine cases, its sign-ups (a production scrypt hash per new password) and the stamp or probe they produce the case's subject, beside 15 pure-function cases; 3.3 s in a local run; slowest CI reading 11.9 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up

/**
* [#12751] The walled owner-verified stamp — maintainer ruling 2026-08-28
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine and SecurityPlugin per case across three postures, 60 cases that each write and read back the same holder's assignments, so no boot can be shared; 4.3 s in a local run; slowest CI reading 10.9 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up

/**
* [ADR-0090 D12, amended 2026-10-17] The organization administrator's
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: a real ObjectQL engine and SecurityPlugin per (driver, using, check) policy on two SQL driver families (better-sqlite3 and sqlite-wasm, both :memory:), already shared by every cell that names the policy and warmed at module scope, each policy the subject of its cells; 104 cases, 1.6 s in a local run; slowest CI reading 12.8 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up

/**
* [#21109, ruling A] The row-level write `check` judges a `date`, `datetime` or
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine and SecurityPlugin per (driver, clause, predicate) on two SQL driver families, 120 boots for 94 cases, each case asserting on the whole stored table of its own boot (nothing stored, the row unchanged), so no boot can be shared; 4.4 s in a local run; slowest CI reading 11.9 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up

/**
* [#19886 stage 2e] A row-level policy that ORDERS a field against a bound fails
Expand Down
41 changes: 28 additions & 13 deletions packages/rest/src/meta-draft-read-builder-gate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 20 — spawns the real producer once per single-cause fixture: one real-stack boot (better-sqlite3 :memory:, the real sys_metadata objects, protocol and routes) seeded with seven saves and shared by the 21 cases, which only read, each case's door reads its subject; slowest CI reading 10.3 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up; read before the 21 identical boots became one

/**
* [#20338] A PENDING draft — unpublished authoring work — is read only by a
Expand Down Expand Up @@ -47,7 +48,7 @@
* arrive ungated, is `meta-draft-read-door-census.test.ts`.
*/

import { describe, it, expect, afterEach, vi } from 'vitest';
import { describe, it, expect, afterAll, beforeAll, beforeEach, vi } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
Expand Down Expand Up @@ -115,7 +116,7 @@ const DATASET_DRAFT_ONLY = { ...DATASET, name: 'pipeline', label: 'Pipeline' };
const DRAFT_ONLY_TEXT = ['Atlas (draft)', 'nav_atlas_launch_plan', 'Beacon', 'nav_beacon_home'];

const liveEngines: ObjectQL[] = [];
afterEach(async () => {
afterAll(async () => {
while (liveEngines.length) {
try { await liveEngines.pop()?.destroy(); } catch { /* noop */ }
}
Expand Down Expand Up @@ -218,6 +219,20 @@ async function boot() {
return { item, list, drafts, datasetQuery, queryDataset };
}

/**
* One boot, seeded once, for every case: after the seeding every case only
* reads (`GET` on the doors, and the dataset door, whose analytics service is
* the `queryDataset` spy and stores nothing), so no case writes what another
* reads. The spy is cleared before each case, as a boot of its own left it.
*/
let shared: Awaited<ReturnType<typeof boot>>;
beforeAll(async () => {
shared = await boot();
});
beforeEach(() => {
shared?.queryDataset.mockClear();
});

const navIds = (doc: any): string[] => (doc?.navigation ?? []).map((e: any) => e.id);
const envelope = (res: any) => ({ status: res.statusCode, code: res.body?.error?.code ?? res.body?.code });
const text = (res: any): string => JSON.stringify(res.body ?? null);
Expand All @@ -233,7 +248,7 @@ const ITEM_SWITCHES: Record<string, Record<string, string>> = {
describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', () => {
for (const [label, query] of Object.entries(ITEM_SWITCHES)) {
it(`${label}: a member without an authoring capability reads the PUBLISHED app — the plain read's answer, byte for byte`, async () => {
const { item } = await boot();
const { item } = shared;
const plain = await item('member', 'app', 'atlas');
const res = await item('member', 'app', 'atlas', query);

Expand All @@ -245,7 +260,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', (
}, 60_000);

it(`${label}: a draft-only app answers the member the plain read's absence — 404, never the draft`, async () => {
const { item } = await boot();
const { item } = shared;
const plain = await item('member', 'app', 'beacon');
const res = await item('member', 'app', 'beacon', query);

Expand All @@ -256,7 +271,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', (
}, 60_000);

it(`${label}: every builder reads the draft (the control) — Studio, Setup and a metadata author alike`, async () => {
const { item } = await boot();
const { item } = shared;
for (const who of BUILDERS) {
const res = await item(who, 'app', 'atlas', query);
expect(res.statusCode, who).toBe(200);
Expand All @@ -271,7 +286,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', (
}

it('?state=draft tells a member nothing about whether a draft exists: an app with none answers the same published read, never NO_DRAFT', async () => {
const { item } = await boot();
const { item } = shared;
const plain = await item('member', 'app', 'quiet');
const res = await item('member', 'app', 'quiet', { state: 'draft' });

Expand All @@ -285,7 +300,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', (

describe('[#20338] GET /meta/:type — ?preview=draft', () => {
it('a member reads the published list — the plain list, byte for byte: no draft-only item, no draft label', async () => {
const { list } = await boot();
const { list } = shared;
const plain = await list('member', 'app');
const res = await list('member', 'app', { preview: 'draft' });

Expand All @@ -297,7 +312,7 @@ describe('[#20338] GET /meta/:type — ?preview=draft', () => {
}, 60_000);

it('every builder reads the draft overlay (the control): the pending draft wins, and the draft-only app is listed', async () => {
const { list } = await boot();
const { list } = shared;
for (const who of BUILDERS) {
const res = await list(who, 'app', { preview: 'draft' });
expect(res.statusCode, who).toBe(200);
Expand All @@ -317,7 +332,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => {

for (const [label, bodyFlag, query] of SWITCHES) {
it(`${label}: a member's query runs over the PUBLISHED dataset and never asks for draft rows`, async () => {
const { datasetQuery, queryDataset } = await boot();
const { datasetQuery, queryDataset } = shared;
const res = await datasetQuery('member', { datasetName: 'revenue', selection, ...bodyFlag }, query);

expect(res.statusCode).toBe(200);
Expand All @@ -328,23 +343,23 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => {
}, 60_000);

it(`${label}: a draft-only dataset answers the member the door's own 404, and nothing is queried`, async () => {
const { datasetQuery, queryDataset } = await boot();
const { datasetQuery, queryDataset } = shared;
const res = await datasetQuery('member', { datasetName: 'pipeline', selection, ...bodyFlag }, query);

expect(envelope(res)).toEqual({ status: 404, code: 'NOT_FOUND' });
expect(queryDataset).not.toHaveBeenCalled();
}, 60_000);

it(`${label}: an INLINE dataset posted by a member runs over live data, never the pending seed draft's rows`, async () => {
const { datasetQuery, queryDataset } = await boot();
const { datasetQuery, queryDataset } = shared;
const res = await datasetQuery('member', { dataset: { ...DATASET }, selection, ...bodyFlag }, query);

expect(res.statusCode).toBe(200);
expect((queryDataset.mock.calls[0] as unknown as unknown[])[3]).toBeUndefined();
}, 60_000);

it(`${label}: every builder's query runs over the draft dataset, with the draft rows asked for (the control)`, async () => {
const { datasetQuery, queryDataset } = await boot();
const { datasetQuery, queryDataset } = shared;
for (const who of BUILDERS) {
queryDataset.mockClear();
const res = await datasetQuery(who, { datasetName: 'revenue', selection, ...bodyFlag }, query);
Expand All @@ -362,7 +377,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => {

describe('[#20338] one predicate: every draft door admits exactly the callers GET /meta/_drafts admits', () => {
it('for each caller, /meta/_drafts refuses them 403 exactly when the item, list and dataset doors serve them the published version', async () => {
const { item, list, drafts, datasetQuery, queryDataset } = await boot();
const { item, list, drafts, datasetQuery, queryDataset } = shared;
for (const who of ['member', ...BUILDERS] as const) {
const listing = await drafts(who);
const admitted = listing.statusCode !== 403;
Expand Down
Loading
Loading