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
4 changes: 4 additions & 0 deletions .changeset/2092-tests-on-sql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Tests only, nothing ships: every test that ran on `@objectstack/driver-memory` now runs on the SQL driver (`driver-sqlite-wasm`), and the app no longer lists `driver-memory` as a dependency of its own (#2092).
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"@objectstack/account": "18.0.0-next.0",
"@objectstack/cli": "18.0.0-next.0",
"@objectstack/core": "18.0.0-next.0",
"@objectstack/driver-memory": "18.0.0-next.0",
"@objectstack/driver-sql": "18.0.0-next.0",
"@objectstack/driver-sqlite-wasm": "18.0.0-next.0",
"@objectstack/metadata": "18.0.0-next.0",
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions test/activity-seed-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AnalyticsService } from '@objectstack/service-analytics';
import { resolveSeedRecord } from '@objectstack/formula';
import stack from './helpers/composed-stack';
Expand Down Expand Up @@ -407,9 +407,14 @@ const accountIsActiveDefault = (): boolean => {
return field!.defaultValue === true;
};

const makeEngine = () =>
ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
/**
* A real SQLite database. `ObjectQL.create` connects the datasource but emits
* no DDL, so the engine's own `syncSchemas()` creates the tables the way the
* runtime's boot does.
*/
const makeEngine = async () => {
const ql = await ObjectQL.create({
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
// Only the columns the four datasets read. The object suite owns the full
// schemas; the contract under test here is "which rows do the widget
Expand Down Expand Up @@ -460,6 +465,9 @@ const makeEngine = () =>
},
} as never,
});
await ql.syncSchemas();
return ql;
};

describe('every Sales Activity widget returns a number over the shipped seeds (#671)', () => {
let ql: Awaited<ReturnType<typeof makeEngine>>;
Expand Down
5 changes: 3 additions & 2 deletions test/attendee-type-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { SysUser } from '@objectstack/platform-objects';
import stack from './helpers/composed-stack';
import { ATTENDEE_RESOLUTIONS } from '../src/sales/objects/event_attendee.object';
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('the acceptance surface, on a real engine', () => {

beforeEach(async () => {
ql = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_event_attendee: EVENT_ATTENDEE,
crm_event: byName('crm_event'),
Expand All @@ -160,6 +160,7 @@ describe('the acceptance surface, on a real engine', () => {
sys_user: SysUser,
} as never,
})) as never;
await ql.syncSchemas();
api = ql.createContext({ isSystem: true });

const event = await api.object('crm_event').insert({
Expand Down
15 changes: 11 additions & 4 deletions test/audit-stamp-readonly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AutomationEngine, installBuiltinNodes } from '@objectstack/service-automation';
import { cel } from '@objectstack/spec';
import type * as Automation from '@objectstack/spec/automation';
Expand Down Expand Up @@ -76,7 +76,7 @@ let ql: any;

beforeAll(async () => {
ql = await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_account: shippedFields('crm_account'),
crm_opportunity: shippedFields('crm_opportunity'),
Expand All @@ -85,6 +85,7 @@ beforeAll(async () => {
crm_campaign_member: shippedFields('crm_campaign_member'),
} as never,
});
await ql.syncSchemas();
});

afterAll(async () => {
Expand Down Expand Up @@ -170,9 +171,15 @@ const readBack = (object: string, id: string): Promise<AnyRec> =>
// ─────────────────────────────────────────────────────── #1666 · approval ──

describe('#1666 — crm_opportunity approval stamps are declared readonly', () => {
/** A closable deal, inserted under a system context so the seed itself is not the measurement. */
/**
* A closable deal, inserted under a system context so the seed itself is not
* the measurement. Each deal gets its own account: `crm_account.name` is
* declared unique, and the SQL driver enforces it (`driver-memory`, which ran
* this file until #2092, did not).
*/
let accounts = 0;
async function seedOpportunity(): Promise<string> {
const account = await ql.insert('crm_account', { name: 'Acme Industrial' }, { context: sysCtx });
const account = await ql.insert('crm_account', { name: `Acme Industrial ${++accounts}` }, { context: sysCtx });
const row = await ql.insert(
'crm_opportunity',
{
Expand Down
15 changes: 7 additions & 8 deletions test/campaign-member-cascade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import stack from './helpers/composed-stack';

/**
Expand Down Expand Up @@ -107,20 +107,18 @@ describe('crm_campaign_member declares cascade on both party lookups', () => {
* metadata-only assertion can show that a delete now succeeds, and no hook
* harness runs the referential pass at all.
*
* `InMemoryDriver` is used for the same reason `object-validation-predicates`
* uses it: it stores only the columns a row was written with, so the merged
* record handed to the validator has ABSENT keys — the shape that made the rule
* fire in the first place. A column-complete driver would still reproduce this
* bug (the cleared column is `null`, not absent, and `isBlank(null)` is true),
* but the sparse one exercises the harder shape.
* On a real SQLite database, column-complete: the cleared column is `null`,
* and `isBlank(null)` is true, so this reproduces the bug as it was found on
* `driver-memory` (whose merged record had the key ABSENT). The absent-key
* shape itself is swept per predicate in `object-validation-predicates`.
*/
describe('deleting an enrolled person', () => {
let ql: AnyRec;
let api: AnyRec;

beforeEach(async () => {
ql = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_campaign_member: CAMPAIGN_MEMBER,
crm_campaign: byName('crm_campaign'),
Expand All @@ -129,6 +127,7 @@ describe('deleting an enrolled person', () => {
crm_account: byName('crm_account'),
} as never,
})) as never;
await ql.syncSchemas();
api = ql.createContext({ isSystem: true });
});
afterEach(async () => {
Expand Down
8 changes: 5 additions & 3 deletions test/case-assignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { bootStack, type VerifyStack } from '@objectstack/verify';
import { extractHookBody } from '@objectstack/cli/hook-body';
import stack, { localePacks } from './helpers/composed-stack';
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('the transfer gate cannot see a beforeInsert owner_id stamp on crm_case
beforeAll(async () => {
seen = [];
ql = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_case: {
name: 'crm_case',
Expand All @@ -150,6 +150,7 @@ describe('the transfer gate cannot see a beforeInsert owner_id stamp on crm_case
},
} as never,
})) as never;
await ql.syncSchemas();

// The SAME seam `@objectstack/plugin-security` registers the #3004 owner_id
// write guard on. A RECORDER, not a gate: what is being measured is
Expand Down Expand Up @@ -540,7 +541,7 @@ describe('the transfer gate on the UPDATE door: which escalation seam it can see
beforeAll(async () => {
seen = [];
ql = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_case: {
name: 'crm_case',
Expand All @@ -553,6 +554,7 @@ describe('the transfer gate on the UPDATE door: which escalation seam it can see
},
} as never,
})) as never;
await ql.syncSchemas();

// Same recorder, same seam `@objectstack/plugin-security` registers the
// #3004 guard on. Visibility is what is being measured, not the verdict.
Expand Down
11 changes: 7 additions & 4 deletions test/churn-health-score-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AnalyticsService } from '@objectstack/service-analytics';
import { resolveSeedRecord } from '@objectstack/formula';
import stack from './helpers/composed-stack';
Expand Down Expand Up @@ -161,9 +161,9 @@ const objectByName = new Map(objects.map((o) => [String(o.name), o]));
const accountIsActiveDefault = (): boolean =>
(objectByName.get('crm_account')?.fields?.is_active as AnyRec | undefined)?.defaultValue === true;

const makeEngine = () =>
ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
const makeEngine = async () => {
const ql = await ObjectQL.create({
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
// Only the columns the four churn blocks read. The object suite owns the
// full schema; the contract here is "which rows does each block select".
Expand All @@ -183,6 +183,9 @@ const makeEngine = () =>
},
} as never,
});
await ql.syncSchemas();
return ql;
};

/** The synthetic row that isolates the claim from the seeds — header, point 3. */
const ACTIVE_AND_CHURNING = 'Zzz Actively-Worked Churning Account';
Expand Down
2 changes: 1 addition & 1 deletion test/decorative-field-sweep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ describe('the roll-up actually computes, and keeps computing (#1182)', () => {
beforeAll(async () => {
process.env.OS_REGISTRY_LOG ??= 'silent';
kernel = new ObjectKernel({ logger: { level: 'silent' } } as never);
await kernel.use(new DefaultDatasourcePlugin({ driver: 'memory', config: {} } as never));
await kernel.use(new DefaultDatasourcePlugin({ driver: 'sqlite-wasm', config: { filename: ':memory:' } } as never));
await kernel.use(new MetadataPlugin({ watch: false, artifactWatch: false, environmentId: 'proj_sweep' } as never));
await kernel.use(new ObjectQLPlugin({ environmentId: 'proj_sweep' } as never));
// The app's own metadata is the subject. Seed data is skipped — the three
Expand Down
12 changes: 6 additions & 6 deletions test/event-attendee-cascade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { SysUser } from '@objectstack/platform-objects';
import stack from './helpers/composed-stack';

Expand Down Expand Up @@ -171,10 +171,9 @@ describe('crm_event_attendee declares cascade on all three party lookups', () =>
* metadata-only assertion can show that a delete now succeeds, and no hook
* harness runs the referential pass at all.
*
* `InMemoryDriver` is used for the same reason `object-validation-predicates`
* uses it: it stores only the columns a row was written with, so the merged
* record handed to the validator has ABSENT keys — the shape that made the rule
* fire in the first place.
* On a real SQLite database, column-complete: the cleared column is `null`.
* The rule first fired on `driver-memory`, whose merged record had the key
* ABSENT; that shape is swept per predicate in `object-validation-predicates`.
*
* `sys_user` is the PLATFORM object, imported rather than stubbed: the whole
* user-side decision rests on a real account erasure reaching this same delete
Expand All @@ -189,7 +188,7 @@ describe('deleting a person who attended a meeting', () => {

beforeEach(async () => {
ql = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: {
crm_event_attendee: EVENT_ATTENDEE,
crm_event: byName('crm_event'),
Expand All @@ -199,6 +198,7 @@ describe('deleting a person who attended a meeting', () => {
sys_user: SysUser,
} as never,
})) as never;
await ql.syncSchemas();
api = ql.createContext({ isSystem: true });
});
afterEach(async () => {
Expand Down
56 changes: 23 additions & 33 deletions test/flow-condition-totality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { describe, it, expect } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AutomationEngine, installBuiltinNodes } from '@objectstack/service-automation';
import { RecordChangeTrigger } from '@objectstack/trigger-record-change';
import stack from './helpers/composed-stack';
Expand Down Expand Up @@ -453,17 +453,32 @@ describe('flow conditions are TOTAL on the real engine', () => {
});

/**
* End-to-end, on the driver that actually produces the sparse record.
* End-to-end, on a real engine over a real SQLite database.
*
* Everything above evaluates conditions in isolation. This boots a real
* ObjectQL over `InMemoryDriver`, a real `AutomationEngine`, and the real
* `RecordChangeTrigger` that wires them together, then performs the ordinary
* writes that reproduced each of the three defects. Before the guards landed
* ObjectQL, a real `AutomationEngine`, and the real `RecordChangeTrigger` that
* wires them together, then performs the ordinary writes that reproduced each
* of the three defects. They were reproduced on `driver-memory`, whose
* post-write row omitted every unwritten column; before the guards landed
* every one of these logged
* `Trigger-fired run of flow '…' failed: condition failed to evaluate as CEL`
* and the automation silently did not happen.
*
* Since the maintainer's ruling on #2092 every test runs on SQL. Measured on
* 18.0.0-next.0 for #2092, with `case_escalation_on_create`'s start condition
* put back to its unguarded form: a born-critical case written through the
* trigger no longer hands the condition an absent key on EITHER driver (the
* run evaluates, no abort), while the same condition handed a record without
* `escalated_date` directly still aborts with `No such key`. That matches the
* 18.0.0-next.0 upgrade's own note (#2091): the trigger's `record` is
* materialised over every declared field. So these writes are not expected to
* reach the absent-key shape; what they pin is that each flow runs end to end
* without a condition abort on the driver a deployed install uses. The
* absent-key shape (a sparse datasource such as `driver-mongodb`) is held by
* the `answers on a record with no keys at all` and `any single referenced key
* is the one that is missing` sweeps above.
*/
describe('conditions answer on a driver whose stored record omits the key', () => {
describe('conditions answer end-to-end on the real engine', () => {
const objMap = Object.fromEntries(objects.map((o) => [o.name as string, o]));

interface Booted {
Expand All @@ -478,14 +493,15 @@ describe('conditions answer on a driver whose stored record omits the key', () =
silent.child = () => silent;

const ql: AnyRec = (await ObjectQL.create({
datasources: { default: new InMemoryDriver({ persistence: false }) },
datasources: { default: new SqliteWasmDriver({ filename: ':memory:' }) },
objects: objMap as never,
// `logger` is honoured at runtime but is not on the published options
// type. Without it this suite prints every engine INFO line plus the
// downstream `notify` / `runAs` errors these flows legitimately hit in a
// user-less harness, which reads as a broken build when it is not.
...({ logger: silent } as object),
})) as never;
await ql.syncSchemas();

const engine = new AutomationEngine(silent);
installBuiltinNodes(engine, {
Expand Down Expand Up @@ -531,32 +547,6 @@ describe('conditions answer on a driver whose stored record omits the key', () =
.filter((r) => /failed to evaluate as CEL|No such key|no such overload/.test(r.error ?? ''))
.map((r) => `${r.flow}: ${String(r.error).split('\n')[0]}`);

it('stores no key for a column it was never given — the precondition', async () => {
const b = await boot([]);
try {
const api = b.ql.createContext({ isSystem: true });
const row = await api.object('crm_case').insert({
subject: 'Prod outage', description: 'Everything is down',
priority: 'critical', status: 'new',
});
const stored = await api.object('crm_case').findOne({ where: { id: row.id } });
// Not `toBeNull()`: the key is ABSENT, which is the whole point. If a
// platform upgrade makes this driver column-complete, this assertion
// fails and everything below stops proving anything — intended signal.
expect('escalated_date' in (stored ?? {})).toBe(false);
// …and `owner_id` has no schema-level filler at all — the acting user is
// stamped onto it by the security middleware (#548), which this harness
// does not install and which a system write short-circuits anyway. So the
// column is missing rather than null, which is precisely the shape
// `contact_welcome`'s `has(record.owner_id)` guard has to survive. The
// positive direction — a USER insert really does come back owned — is
// pinned in `test/ownership-model.test.ts`.
expect('owner_id' in (stored ?? {})).toBe(false);
} finally {
await b.close();
}
}, 60_000);

it('case_escalation_on_create: a case BORN critical still evaluates', async () => {
const b = await boot(['case_escalation_on_create']);
try {
Expand Down
Loading
Loading