Skip to content

Commit 69bde34

Browse files
committed
test(firestore): expand pipeline TS runtime and expression coverage
1 parent 8e39993 commit 69bde34

4 files changed

Lines changed: 185 additions & 10 deletions

File tree

okf-bundle/packages/firestore/pipeline-coverage-work-queue.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ timestamp: 2026-06-25T12:00:00Z
88

99
# Pipeline coverage and parity — work queue
1010

11-
> **IN PROGRESS:** **K** queued — TS `pipeline_runtime` + `expressions`. **J** complete (J0–J6).
11+
> **IN PROGRESS:** **L** queued — Android parsed-aggregate tail. **K** complete.
1212
> **Goal/order:** platform parity first; then TS/native coverage toward intractable limits. Links: [parity](pipeline-platform-parity.md), [SDK audit](pipeline-sdk-support-audit.md), [coverage](../../testing/coverage-design.md), [e2e](../../testing/running-e2e.md), [architecture](pipelines.md).
1313
1414
---
@@ -60,7 +60,7 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/change-au
6060
| **I** | **Platform parity audit** || 31 e2e branches; registry P-001–P-031 |
6161
| **Ib** | **SDK support reconciliation** || Guard list vs iOS 12.15 / Android 34.15 CHANGELOG; [audit method](pipeline-sdk-support-audit.md) |
6262
| **J** | **Parity remediation** | **✅ complete** | **J0** probes → **J0b** consolidation → **J0 remainder****J1–J6** |
63-
| **K** | TS `pipeline_runtime` + `expressions` | queued | guards, timestamp/FieldPath normalization gaps |
63+
| **K** | TS `pipeline_runtime` + `expressions` | **** | Jest alias/normalization batch; expressions e2e receiver probe |
6464
| **L** | Android parsed-aggregate tail | queued | ~143 missed *(was old J)* |
6565
| **M** | Android exit frames + receiver chains | queued | ~77 loop remainder + exit/receiver *(was old K)* |
6666
| **N** | iOS stage coercion | queued | ~293 missed; operand tail *(was old L)* |
@@ -76,16 +76,16 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/change-au
7676

7777
## Current snapshot
7878

79-
**Label:** `j-complete`; **harness:** full test app (committed)
79+
**Label:** `k-complete`; **harness:** full test app (committed)
8080

81-
**Next item:** **K**TS `pipeline_runtime` + `expressions` normalization gaps.
81+
**Next item:** **L**Android parsed-aggregate expression args (~143 missed).
8282

8383
| **J2** P-005 `integerLiteral` | `fix(firestore, android): align pipeline integerLiteral constant lowering with iOS` | **closed** | **closed** | **closed** |||| P-005 → Resolved; CFBoolean deferral accepted |
8484
| **J3** P-010 stage option expressions | `fix(firestore, android): align pipeline stage option expression fields with iOS` | **closed** | **closed** | **closed** |||| P-010 → Resolved |
8585
| **J4** P-011 constant envelope | `fix(firestore, android): align pipeline constant envelope routing with iOS` | **closed** | **closed** | **closed** |||| P-011 → Resolved |
8686
| **J5** P-012 timestampTruncate arity | `fix(firestore, android): align pipeline timestampTruncate arity validation with iOS` | **closed** | **closed** | **closed** |||| P-012 → Resolved; iOS explicit arity guard deferred |
8787
| **J6** P-034 operand-mode audit | `docs(firestore): close P-034 operand-mode e2e audit after J1 parity` | **closed** | **closed** | **closed** |||| No code trims; P-021/P-022 confirmed |
88-
| **K** TS runtime/expressions | | open | open | open | `implementation` | `unit-focused` || queued |
88+
| **K** TS runtime/expressions | `test(firestore): expand pipeline TS runtime and expression coverage` | **closed** | **closed** | **closed** | | || Jest batch + receiver `currentTimestamp` e2e probe |
8989

9090
**Arbiter gate (2026-06-25):**
9191

@@ -114,8 +114,8 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/change-au
114114
| --------------------------- | ----------------- | ---------------------- | -------------------------------------- | ----- |
115115
| Parity drift (bridge) ||| **0 open** (P-034 closed) | **J** |
116116
| Parity drift (SDK/macOS-js) | 11 vacuous | 10 reduced + 3 vacuous | documented ||
117-
| TS `pipeline_runtime.ts` | 86% | pre-K baseline | | **K** |
118-
| TS `expressions.ts` | 89% | pre-K baseline | | **K** |
117+
| TS `pipeline_runtime.ts` | 86% | **90.62% (203/224)** | K batch (Jest; e2e lcov unchanged) | **K** |
118+
| TS `expressions.ts` | 89% | **93.61% (249/266)** | K batch (+1 e2e line) | **K** |
119119
| Android NodeBuilder | 67.5% (1167/1729) | **70.2% (1155/1645)** | F: −183 LOC dead | L, M |
120120
| Android loop L900–1299 | 106 missed | **77 missed** | F: −29 missed | M |
121121
| Android Executor | 58% | 58% || O |
@@ -303,7 +303,7 @@ Per [SDK audit §6](pipeline-sdk-support-audit.md): one function/commit; remove
303303

304304
**Gate for Phase K+:** J0 complete + **J0b** committed + J1–J6 bridge commits + parity **Resolved** updated.
305305

306-
**Current gates:** **K** queued (`implementation_gate` open). **J** complete.
306+
**Current gates:** **L** queued. **J** and **K** complete.
307307

308308
---
309309

packages/firestore/__tests__/pipelines-fluent-serialization-cases.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as pipelines from '../lib/pipelines';
2+
import * as pipelineExpressions from '../lib/pipelines/expressions';
23

34
/** Keep in sync with EXPRESSION_METHOD_NAMES in lib/pipelines/expressions.ts */
45
export const EXPRESSION_METHOD_NAMES = [
@@ -225,6 +226,20 @@ function unaryField(method: string, target = scores): FluentParityCase {
225226
});
226227
}
227228

229+
function resolveGlobalHelper(method: string): (...args: unknown[]) => unknown {
230+
const fromPipelines = (pipelines as Record<string, unknown>)[method];
231+
if (typeof fromPipelines === 'function') {
232+
return fromPipelines as (...args: unknown[]) => unknown;
233+
}
234+
235+
const fromExpressions = (pipelineExpressions as Record<string, unknown>)[method];
236+
if (typeof fromExpressions === 'function') {
237+
return fromExpressions as (...args: unknown[]) => unknown;
238+
}
239+
240+
throw new Error(`Missing global helper for ${method}`);
241+
}
242+
228243
function binaryCompare(
229244
category: 'binary-compare' | 'binary-math',
230245
method: string,
@@ -235,7 +250,7 @@ function binaryCompare(
235250
category,
236251
method,
237252
expectedName: canonicalName(method),
238-
global: () => (pipelines as any)[canonicalName(method)](target, arg),
253+
global: () => resolveGlobalHelper(method)(target, arg),
239254
fluent: () => fluent(target)[method](arg),
240255
});
241256
}

packages/firestore/__tests__/pipelines.test.ts

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expect, it, jest } from '@jest/globals';
22
import { getFirestore } from '../lib';
33
import { getApp } from '@react-native-firebase/app';
4+
import { FieldPath } from '../lib/FieldPath';
5+
import { Timestamp } from '../lib/FirestoreTimestamp';
46
import {
57
arrayFilter,
68
arrayFirst,
@@ -27,9 +29,19 @@ import {
2729
countAll,
2830
average,
2931
subcollection,
32+
array,
33+
map,
3034
} from '../lib/pipelines';
3135
import '../lib/pipelines';
32-
import { ConstantExpression, FunctionExpression } from '../lib/pipelines/expressions';
36+
import {
37+
avg,
38+
ConstantExpression,
39+
eq,
40+
FunctionExpression,
41+
gt,
42+
gte,
43+
lt,
44+
} from '../lib/pipelines/expressions';
3345

3446
describe('Firestore pipelines runtime', function () {
3547
it('installs pipeline() and serializes source builders', function () {
@@ -646,6 +658,151 @@ describe('Firestore pipelines runtime', function () {
646658
});
647659
});
648660

661+
it('serializes comparison alias exports gt, eq, gte, and lt', function () {
662+
expect(gt(field('rating'), constant(4))).toMatchObject({
663+
exprType: 'Function',
664+
name: 'greaterThan',
665+
});
666+
expect(eq(field('sku'), constant('SKU001'))).toMatchObject({
667+
exprType: 'Function',
668+
name: 'equal',
669+
});
670+
expect(gte(field('stock'), constant(50))).toMatchObject({
671+
exprType: 'Function',
672+
name: 'greaterThanOrEqual',
673+
});
674+
expect(lt(field('price'), constant(100))).toMatchObject({
675+
exprType: 'Function',
676+
name: 'lessThan',
677+
});
678+
});
679+
680+
it('serializes avg aggregate alias to average', function () {
681+
expect(avg(field('score'))).toMatchObject({
682+
exprType: 'AggregateFunction',
683+
kind: 'average',
684+
});
685+
});
686+
687+
it('normalizes array and map helpers that embed runtime expression nodes', function () {
688+
const arrayExpr: any = array([field('score'), constant('tail')]);
689+
expect(arrayExpr).toMatchObject({
690+
exprType: 'Function',
691+
name: 'array',
692+
args: [
693+
{ exprType: 'Field', path: 'score' },
694+
{ exprType: 'Constant', value: 'tail' },
695+
],
696+
});
697+
698+
const mapExpr: any = map({ label: field('name'), version: constant(1) });
699+
expect(mapExpr).toMatchObject({
700+
exprType: 'Function',
701+
name: 'map',
702+
});
703+
expect(mapExpr.args[0].exprType).toBe('Constant');
704+
expect(mapExpr.args[0].value.label.exprType).toBe('Field');
705+
expect(mapExpr.args[0].value.version.value).toBe(1);
706+
});
707+
708+
it('preserves non-plain constant values without walking prototype objects', function () {
709+
class Marker {
710+
readonly tag = 'marker';
711+
}
712+
const marker = new Marker();
713+
const expr: any = constant(marker);
714+
expect(expr).toMatchObject({
715+
exprType: 'Constant',
716+
value: marker,
717+
});
718+
});
719+
720+
it('normalizes pipeline execute results across timestamp and field path shapes', async function () {
721+
const db: any = getFirestore();
722+
const existingExecutionTime = new Timestamp(1735689600, 123000000);
723+
const nativeExecute = jest.fn(async () => ({
724+
executionTime: existingExecutionTime,
725+
results: [
726+
{
727+
path: 'books/alpha',
728+
id: 'alpha',
729+
data: { title: 'Alpha', nested: { score: 9 } },
730+
createTime: 1700000000123,
731+
updateTime: [1700000001, 456000000],
732+
},
733+
{
734+
path: 'books/beta',
735+
data: { plain: true },
736+
createTime: { seconds: 2, nanoseconds: 3 },
737+
updateTime: { seconds: 4, nanoseconds: 5 },
738+
},
739+
],
740+
}));
741+
742+
const originalNativeModule = db._nativeModule;
743+
db._nativeModule = { pipelineExecute: nativeExecute };
744+
745+
try {
746+
const snapshot = await execute(db.pipeline().documents(['books/alpha', 'books/beta']));
747+
748+
expect(snapshot.executionTime).toBe(existingExecutionTime);
749+
expect(snapshot.results[0]?.createTime?.toMillis()).toBe(1700000000123);
750+
expect(snapshot.results[0]?.updateTime?.seconds).toBe(1700000001);
751+
expect(snapshot.results[0]?.updateTime?.nanoseconds).toBe(456000000);
752+
expect(snapshot.results[0]?.get('nested.score')).toBe(9);
753+
expect(snapshot.results[0]?.get(new FieldPath('nested', 'score'))).toBe(9);
754+
expect(snapshot.results[0]?.get(field('nested.score'))).toBe(9);
755+
expect(snapshot.results[1]?.data()).toEqual({ plain: true });
756+
expect(snapshot.results[1]?.createTime?.seconds).toBe(2);
757+
expect(snapshot.results[1]?.updateTime?.nanoseconds).toBe(5);
758+
} finally {
759+
db._nativeModule = originalNativeModule;
760+
}
761+
});
762+
763+
it('serializes FieldPath and Timestamp arguments in pipeline stages', function () {
764+
const db: any = getFirestore();
765+
const timestamp = new Timestamp(12, 34);
766+
const fieldPath = new FieldPath('meta', 'createdAt');
767+
768+
const serialized = db
769+
.pipeline()
770+
.collection('books')
771+
.where(greaterThan(field('rating'), timestamp as any))
772+
.sort(Ordering.of(fieldPath as any).descending())
773+
.distinct({ groups: [fieldPath] } as any)
774+
.select(
775+
field('title')
776+
.add(timestamp as any)
777+
.as('createdAt'),
778+
)
779+
.serialize();
780+
781+
expect(serialized.stages[0]?.options?.condition?.args?.[1]).toMatchObject({
782+
exprType: 'Constant',
783+
value: {
784+
seconds: 12,
785+
nanoseconds: 34,
786+
},
787+
});
788+
expect(serialized.stages[1]?.options?.orderings?.[0]?.expr).toMatchObject({
789+
exprType: 'Constant',
790+
value: {
791+
segments: ['meta', 'createdAt'],
792+
},
793+
});
794+
expect(serialized.stages[2]?.options?.groups?.[0]).toEqual({
795+
segments: ['meta', 'createdAt'],
796+
});
797+
expect(serialized.stages[3]?.options?.selections?.[0]?.expr?.args?.[1]).toMatchObject({
798+
exprType: 'Constant',
799+
value: {
800+
seconds: 12,
801+
nanoseconds: 34,
802+
},
803+
});
804+
});
805+
649806
it('supports method-style expression chaining and ordering helper serialization', function () {
650807
const db: any = getFirestore();
651808

packages/firestore/e2e/Pipeline.e2e.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,6 +2682,7 @@ describe('FirestorePipeline', function () {
26822682
.documents([docPath])
26832683
.select(
26842684
currentTimestamp().as('now'),
2685+
field('eventTime').currentTimestamp().as('receiverNow'),
26852686
timestampToUnixMicros(field('eventTime')).as('eventTimeMicros'),
26862687
unixSecondsToTimestamp(field('epochSec')).as('fromSec'),
26872688
unixMicrosToTimestamp(field('epochMicros')).as('fromMicros'),
@@ -2692,6 +2693,8 @@ describe('FirestorePipeline', function () {
26922693
const data = snapshot.results[0].data();
26932694
data.now.constructor.name.should.equal('Timestamp');
26942695
should(data.now.toMillis()).be.greaterThan(0);
2696+
data.receiverNow.constructor.name.should.equal('Timestamp');
2697+
should(data.receiverNow.toMillis()).be.greaterThan(0);
26952698
data.eventTimeMicros.should.equal(1700000000000000);
26962699
data.fromSec.constructor.name.should.equal('Timestamp');
26972700
data.fromSec.seconds.should.equal(1700000000);

0 commit comments

Comments
 (0)