@opensaas/stack-core 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +61 -0
- package/dist/access/access-filter.d.ts +76 -20
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +102 -70
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +175 -10
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/depth-limits.d.ts +26 -0
- package/dist/access/depth-limits.d.ts.map +1 -0
- package/dist/access/depth-limits.js +26 -0
- package/dist/access/depth-limits.js.map +1 -0
- package/dist/access/errors.d.ts +43 -0
- package/dist/access/errors.d.ts.map +1 -0
- package/dist/access/errors.js +55 -0
- package/dist/access/errors.js.map +1 -0
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +82 -20
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +5 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.js +1 -1
- package/dist/access/multi-column-read-write.test.js.map +1 -1
- package/dist/access/relationship-count.test.js +1 -1
- package/dist/access/relationship-count.test.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +1 -1
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/types.d.ts +15 -7
- package/dist/access/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +15 -9
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +1 -5
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +43 -6
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +5 -4
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +258 -7
- package/src/access/access-filter.ts +146 -72
- package/src/access/depth-limits.ts +26 -0
- package/src/access/errors.ts +62 -0
- package/src/access/field-visibility.ts +97 -21
- package/src/access/index.ts +6 -0
- package/src/access/multi-column-read-write.test.ts +1 -1
- package/src/access/relationship-count.test.ts +1 -1
- package/src/access/relationship-label-filter.test.ts +1 -1
- package/src/access/types.ts +12 -5
- package/src/context/index.ts +15 -6
- package/src/context/nested-operations.ts +0 -7
- package/src/context/transaction-boundary.ts +48 -7
- package/src/context/write-pipeline.ts +5 -4
- package/src/index.ts +11 -0
- package/tests/access-relationships.test.ts +78 -64
- package/tests/context.test.ts +106 -24
- package/tests/default-value-create.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +1 -1
- package/tests/nav-count.test.ts +2 -2
- package/tests/resolve-chain.test.ts +394 -0
- package/tests/transaction-boundary-hooks.test.ts +246 -1
- package/tests/write-pipeline.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -23,7 +23,7 @@ describe('Relationship Access Control', () => {
|
|
|
23
23
|
},
|
|
24
24
|
plugins: {},
|
|
25
25
|
_isSudo: false,
|
|
26
|
-
|
|
26
|
+
_resolveOutputChain: [],
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
describe('getRelatedListConfig', () => {
|
|
@@ -319,9 +319,10 @@ describe('Relationship Access Control', () => {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// Test that buildIncludeWithAccessControl creates the right where clause
|
|
322
|
-
const { buildIncludeWithAccessControl } =
|
|
322
|
+
const { buildIncludeWithAccessControl, toPrismaInclude } =
|
|
323
|
+
await import('../src/access/index.js')
|
|
323
324
|
|
|
324
|
-
const
|
|
325
|
+
const result = await buildIncludeWithAccessControl(
|
|
325
326
|
config.lists.User.fields,
|
|
326
327
|
{
|
|
327
328
|
session: null,
|
|
@@ -329,6 +330,7 @@ describe('Relationship Access Control', () => {
|
|
|
329
330
|
},
|
|
330
331
|
config,
|
|
331
332
|
)
|
|
333
|
+
const include = toPrismaInclude(result)
|
|
332
334
|
|
|
333
335
|
// Should include posts with a where filter
|
|
334
336
|
expect(include).toBeDefined()
|
|
@@ -486,9 +488,10 @@ describe('Relationship Access Control', () => {
|
|
|
486
488
|
}
|
|
487
489
|
|
|
488
490
|
// Test that buildIncludeWithAccessControl creates session-based where clause
|
|
489
|
-
const { buildIncludeWithAccessControl } =
|
|
491
|
+
const { buildIncludeWithAccessControl, toPrismaInclude } =
|
|
492
|
+
await import('../src/access/index.js')
|
|
490
493
|
|
|
491
|
-
const
|
|
494
|
+
const result = await buildIncludeWithAccessControl(
|
|
492
495
|
config.lists.User.fields,
|
|
493
496
|
{
|
|
494
497
|
session: { userId: '1' },
|
|
@@ -496,6 +499,7 @@ describe('Relationship Access Control', () => {
|
|
|
496
499
|
},
|
|
497
500
|
config,
|
|
498
501
|
)
|
|
502
|
+
const include = toPrismaInclude(result)
|
|
499
503
|
|
|
500
504
|
// Should include posts with session-based where filter
|
|
501
505
|
expect(include).toBeDefined()
|
|
@@ -505,75 +509,85 @@ describe('Relationship Access Control', () => {
|
|
|
505
509
|
})
|
|
506
510
|
})
|
|
507
511
|
|
|
508
|
-
describe('depth
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
512
|
+
describe('no depth cap on an acyclic chain (issue #830)', () => {
|
|
513
|
+
/**
|
|
514
|
+
* `filterReadableFields` used to stop recursing into relationships past
|
|
515
|
+
* `MAX_DEPTH` (5), so a nested row past that depth was returned with its
|
|
516
|
+
* field-level `read` access never evaluated and its `resolveOutput` hooks
|
|
517
|
+
* never run — even though the row-scoping half of the pipeline
|
|
518
|
+
* (`buildIncludeWithAccessControl`) may have correctly scoped it. The fix
|
|
519
|
+
* removes this cap: by the time a result reaches this function it is
|
|
520
|
+
* already a finite, ACYCLIC tree bounded by whatever the (now fail-closed)
|
|
521
|
+
* pre-query phase permitted, so there is no infinite-recursion risk left
|
|
522
|
+
* to guard against. This test builds an 8-level-deep chain — deeper than
|
|
523
|
+
* the old cap — with a read-denied field and a resolveOutput hook on the
|
|
524
|
+
* deepest list, and asserts both are applied at every level.
|
|
525
|
+
*/
|
|
526
|
+
it('applies field-level read access and resolveOutput at depth 6+', async () => {
|
|
527
|
+
const allowQuery = () => true
|
|
528
|
+
const chainLength = 8
|
|
529
|
+
const lists: OpenSaasConfig['lists'] = {}
|
|
530
|
+
for (let i = 0; i < chainLength; i++) {
|
|
531
|
+
const isLast = i === chainLength - 1
|
|
532
|
+
lists[`L${i}`] = {
|
|
533
|
+
fields: {
|
|
534
|
+
name: { type: 'text' },
|
|
535
|
+
...(isLast
|
|
536
|
+
? {
|
|
537
|
+
secret: {
|
|
538
|
+
type: 'text',
|
|
539
|
+
access: { read: () => false },
|
|
540
|
+
},
|
|
541
|
+
label: {
|
|
542
|
+
type: 'text',
|
|
543
|
+
hooks: {
|
|
544
|
+
resolveOutput: ({ value }: { value: unknown }) => `resolved:${value}`,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
}
|
|
548
|
+
: {}),
|
|
549
|
+
...(i < chainLength - 1
|
|
550
|
+
? { next: { type: 'relationship', ref: `L${i + 1}.prev` } }
|
|
551
|
+
: {}),
|
|
552
|
+
...(i > 0 ? { prev: { type: 'relationship', ref: `L${i - 1}.next` } } : {}),
|
|
530
553
|
},
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
title: { type: 'text' },
|
|
534
|
-
author: {
|
|
535
|
-
type: 'relationship',
|
|
536
|
-
ref: 'User.posts',
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
access: {
|
|
540
|
-
operation: {
|
|
541
|
-
query: () => true,
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
},
|
|
545
|
-
},
|
|
554
|
+
access: { operation: { query: allowQuery } },
|
|
555
|
+
}
|
|
546
556
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
id: '1',
|
|
551
|
-
name: 'John Doe',
|
|
552
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
553
|
-
posts: [] as any[],
|
|
557
|
+
const config: OpenSaasConfig = {
|
|
558
|
+
db: { provider: 'postgresql', url: 'postgresql://localhost:5432/test' },
|
|
559
|
+
lists,
|
|
554
560
|
}
|
|
555
561
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
562
|
+
// Build a genuinely acyclic, materialized nested object — exactly what
|
|
563
|
+
// Prisma would return for this include shape (a fresh object per level,
|
|
564
|
+
// no shared/back references).
|
|
565
|
+
let deepest: Record<string, unknown> = {
|
|
566
|
+
id: `${chainLength - 1}`,
|
|
567
|
+
name: `L${chainLength - 1}`,
|
|
568
|
+
secret: 'TOP-SECRET',
|
|
569
|
+
label: 'raw-value',
|
|
570
|
+
}
|
|
571
|
+
for (let i = chainLength - 2; i >= 0; i--) {
|
|
572
|
+
deepest = { id: `${i}`, name: `L${i}`, next: deepest }
|
|
560
573
|
}
|
|
561
|
-
|
|
562
|
-
user.posts = [post]
|
|
563
574
|
|
|
564
575
|
const result = await filterReadableFields(
|
|
565
|
-
|
|
566
|
-
config.lists.
|
|
567
|
-
{
|
|
568
|
-
session: null,
|
|
569
|
-
context: mockContext,
|
|
570
|
-
},
|
|
576
|
+
deepest,
|
|
577
|
+
config.lists.L0.fields,
|
|
578
|
+
{ session: null, context: mockContext },
|
|
571
579
|
config,
|
|
572
580
|
)
|
|
573
581
|
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
582
|
+
// Walk down to the deepest level in the filtered result.
|
|
583
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
584
|
+
let current: any = result
|
|
585
|
+
for (let i = 0; i < chainLength - 1; i++) {
|
|
586
|
+
current = current.next
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
expect(current.secret).toBeUndefined() // read-denied field stripped
|
|
590
|
+
expect(current.label).toBe('resolved:raw-value') // resolveOutput ran
|
|
577
591
|
})
|
|
578
592
|
})
|
|
579
593
|
|
package/tests/context.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
2
2
|
import { getContext } from '../src/context/index.js'
|
|
3
3
|
import { defineFragment } from '../src/query/index.js'
|
|
4
4
|
import { virtual } from '../src/fields/index.js'
|
|
5
|
+
import { AccessScopeDepthExceededError } from '../src/access/index.js'
|
|
6
|
+
import { READ_INCLUDE_MAX_DEPTH } from '../src/access/depth-limits.js'
|
|
5
7
|
import type { OpenSaasConfig } from '../src/config/types.js'
|
|
6
8
|
|
|
7
9
|
describe('getContext', () => {
|
|
@@ -1425,20 +1427,19 @@ describe('getContext', () => {
|
|
|
1425
1427
|
expect(call.include).toEqual({ posts: true })
|
|
1426
1428
|
})
|
|
1427
1429
|
|
|
1428
|
-
// Regression:
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1430
|
+
// Regression for issue #830: a read issued from inside a `resolveOutput`
|
|
1431
|
+
// hook used to lose relation row scoping ENTIRELY — `buildIncludeWithAccessControl`
|
|
1432
|
+
// returned a whole-object `undefined` for the inner read (any
|
|
1433
|
+
// `_resolveOutputChain.length > 0`), which `mergeIncludeWithAccessControl`
|
|
1434
|
+
// treated as "nothing to merge against" and passed the caller's include
|
|
1435
|
+
// through completely unscoped. The fix scopes each immediate relation with
|
|
1436
|
+
// its own access `where` while still not auto-EXPANDING into that
|
|
1437
|
+
// relation's own nested relations (preserving the original loop-prevention
|
|
1438
|
+
// — see the self-referential coverage in `access-filter.test.ts`).
|
|
1439
|
+
describe('scopes (without expanding) a caller include used inside a resolveOutput hook (#830)', () => {
|
|
1437
1440
|
// Build an Author config with a virtual field whose resolveOutput issues a
|
|
1438
1441
|
// read WITH an explicit include. While that hook runs,
|
|
1439
|
-
//
|
|
1440
|
-
// returns undefined for the inner read — exercising the
|
|
1441
|
-
// `accessControlledInclude === undefined` passthrough path.
|
|
1442
|
+
// _resolveOutputChain.length > 0.
|
|
1442
1443
|
function configWithResolveOutputProbe(
|
|
1443
1444
|
callerInclude: Record<string, unknown>,
|
|
1444
1445
|
capture: (include: unknown) => void,
|
|
@@ -1467,7 +1468,7 @@ describe('getContext', () => {
|
|
|
1467
1468
|
}
|
|
1468
1469
|
}
|
|
1469
1470
|
|
|
1470
|
-
it('findUnique inside a resolveOutput hook
|
|
1471
|
+
it('findUnique inside a resolveOutput hook row-scopes the relation instead of dropping the where', async () => {
|
|
1471
1472
|
let innerIncludeSeen: unknown
|
|
1472
1473
|
const hookConfig = configWithResolveOutputProbe({ post: true }, (include) => {
|
|
1473
1474
|
innerIncludeSeen = include
|
|
@@ -1479,12 +1480,12 @@ describe('getContext', () => {
|
|
|
1479
1480
|
const context = await getContext(hookConfig, relPrisma, null)
|
|
1480
1481
|
await context.db.author.findUnique({ where: { id: 'a1' } })
|
|
1481
1482
|
|
|
1482
|
-
//
|
|
1483
|
-
//
|
|
1484
|
-
expect(innerIncludeSeen).toEqual({ post:
|
|
1483
|
+
// `post` is still fetched (not dropped) but now carries Post's own
|
|
1484
|
+
// query-access `where` — it is no longer a bare, unscoped `true`.
|
|
1485
|
+
expect(innerIncludeSeen).toEqual({ post: { where: { status: { equals: 'published' } } } })
|
|
1485
1486
|
})
|
|
1486
1487
|
|
|
1487
|
-
it('findMany inside a resolveOutput hook
|
|
1488
|
+
it('findMany inside a resolveOutput hook scopes the relation but does not auto-expand its nested include', async () => {
|
|
1488
1489
|
let innerIncludeSeen: unknown
|
|
1489
1490
|
const hookConfig = configWithResolveOutputProbe(
|
|
1490
1491
|
{ post: { include: { author: true } } },
|
|
@@ -1499,15 +1500,96 @@ describe('getContext', () => {
|
|
|
1499
1500
|
const context = await getContext(hookConfig, relPrisma, null)
|
|
1500
1501
|
await context.db.author.findMany()
|
|
1501
1502
|
|
|
1502
|
-
// The
|
|
1503
|
-
//
|
|
1504
|
-
|
|
1503
|
+
// The caller's own nested selection (`post.include.author`) is honoured
|
|
1504
|
+
// as-is (access control does not auto-descend further here), while
|
|
1505
|
+
// `post` itself picks up its access `where`.
|
|
1506
|
+
expect(innerIncludeSeen).toEqual({
|
|
1507
|
+
post: { where: { status: { equals: 'published' } }, include: { author: true } },
|
|
1508
|
+
})
|
|
1505
1509
|
})
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
// Regression for issue #830: a caller `include` nested deeper than the
|
|
1513
|
+
// Access Filter can scope used to be returned unscoped rather than
|
|
1514
|
+
// denied. This exercises the fix end-to-end through `context.db`,
|
|
1515
|
+
// matching the reproduction in the issue: a chain of lists deep enough
|
|
1516
|
+
// to cross `READ_INCLUDE_MAX_DEPTH`, read as a non-privileged session.
|
|
1517
|
+
describe('fail-closed at the read-include depth cap through context.db (#830)', () => {
|
|
1518
|
+
function chainListConfig(count: number): OpenSaasConfig['lists'] {
|
|
1519
|
+
const lists: OpenSaasConfig['lists'] = {}
|
|
1520
|
+
for (let i = 0; i < count; i++) {
|
|
1521
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
1522
|
+
const fields: Record<string, any> = { name: { type: 'text' } }
|
|
1523
|
+
if (i < count - 1) fields.next = { type: 'relationship', ref: `C${i + 1}.prev` }
|
|
1524
|
+
if (i > 0) fields.prev = { type: 'relationship', ref: `C${i - 1}.next` }
|
|
1525
|
+
lists[`C${i}`] = {
|
|
1526
|
+
fields,
|
|
1527
|
+
access: {
|
|
1528
|
+
operation: { query: () => (i === 0 ? true : { ownerId: { equals: `C${i}` } }) },
|
|
1529
|
+
},
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return lists
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// A caller include selecting `next` `hops` more times, ending bare.
|
|
1536
|
+
function nestedCallerInclude(hops: number): Record<string, unknown> {
|
|
1537
|
+
if (hops <= 0) return true as unknown as Record<string, unknown>
|
|
1538
|
+
return { include: { next: nestedCallerInclude(hops - 1) } }
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
it('throws AccessScopeDepthExceededError for a caller include one hop past the cap', async () => {
|
|
1542
|
+
const chainLength = READ_INCLUDE_MAX_DEPTH + 2
|
|
1543
|
+
const chainConfig: OpenSaasConfig = {
|
|
1544
|
+
db: { provider: 'postgresql', url: 'postgresql://localhost:5432/test' },
|
|
1545
|
+
lists: chainListConfig(chainLength),
|
|
1546
|
+
}
|
|
1547
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1548
|
+
const chainPrisma: any = {}
|
|
1549
|
+
for (let i = 0; i < chainLength; i++) {
|
|
1550
|
+
chainPrisma[`c${i}`] = { findMany: vi.fn(), findFirst: vi.fn(), findUnique: vi.fn() }
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const context = await getContext(chainConfig, chainPrisma, null)
|
|
1506
1554
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1555
|
+
await expect(
|
|
1556
|
+
context.db.c0.findMany({
|
|
1557
|
+
include: { next: nestedCallerInclude(READ_INCLUDE_MAX_DEPTH) },
|
|
1558
|
+
}),
|
|
1559
|
+
).rejects.toThrow(AccessScopeDepthExceededError)
|
|
1560
|
+
|
|
1561
|
+
// The database is never even queried — the denial happens before the
|
|
1562
|
+
// Prisma call, not as a post-hoc filter on returned data.
|
|
1563
|
+
expect(chainPrisma.c0.findMany).not.toHaveBeenCalled()
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
it('still returns correctly row-scoped data for the same include one hop shallower', async () => {
|
|
1567
|
+
const chainLength = READ_INCLUDE_MAX_DEPTH + 1
|
|
1568
|
+
const chainConfig: OpenSaasConfig = {
|
|
1569
|
+
db: { provider: 'postgresql', url: 'postgresql://localhost:5432/test' },
|
|
1570
|
+
lists: chainListConfig(chainLength),
|
|
1571
|
+
}
|
|
1572
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1573
|
+
const chainPrisma: any = {}
|
|
1574
|
+
for (let i = 0; i < chainLength; i++) {
|
|
1575
|
+
chainPrisma[`c${i}`] = { findMany: vi.fn(), findFirst: vi.fn(), findUnique: vi.fn() }
|
|
1576
|
+
}
|
|
1577
|
+
chainPrisma.c0.findMany.mockResolvedValue([])
|
|
1578
|
+
|
|
1579
|
+
const context = await getContext(chainConfig, chainPrisma, null)
|
|
1580
|
+
|
|
1581
|
+
await context.db.c0.findMany({
|
|
1582
|
+
include: { next: nestedCallerInclude(READ_INCLUDE_MAX_DEPTH - 1) },
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
// Walk the built include down to the last list — it must carry that
|
|
1586
|
+
// list's own access `where`, proving row scoping, not just "no throw".
|
|
1587
|
+
let entry = chainPrisma.c0.findMany.mock.calls[0][0].include.next
|
|
1588
|
+
for (let i = 1; i < READ_INCLUDE_MAX_DEPTH; i++) {
|
|
1589
|
+
entry = entry.include.next
|
|
1590
|
+
}
|
|
1591
|
+
expect(entry.where).toEqual({ ownerId: { equals: `C${READ_INCLUDE_MAX_DEPTH}` } })
|
|
1592
|
+
})
|
|
1511
1593
|
})
|
|
1512
1594
|
|
|
1513
1595
|
// Regression for #628: a virtual field named in `include` used to be
|
package/tests/nav-count.test.ts
CHANGED
|
@@ -43,7 +43,7 @@ function makeContext(counts: Record<string, number>): {
|
|
|
43
43
|
storage: {},
|
|
44
44
|
plugins: {},
|
|
45
45
|
_isSudo: false,
|
|
46
|
-
|
|
46
|
+
_resolveOutputChain: [],
|
|
47
47
|
} as unknown as AccessContext
|
|
48
48
|
return { context, spies }
|
|
49
49
|
}
|
|
@@ -168,7 +168,7 @@ describe('resolveNavCounts', () => {
|
|
|
168
168
|
storage: {},
|
|
169
169
|
plugins: {},
|
|
170
170
|
_isSudo: false,
|
|
171
|
-
|
|
171
|
+
_resolveOutputChain: [],
|
|
172
172
|
} as unknown as AccessContext
|
|
173
173
|
|
|
174
174
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|