@bevel-software/platform-core-backend 0.36.1 → 0.37.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/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +5 -2
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +11 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/kb-fs/file-change-notifier.d.ts +51 -26
- package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
- package/dist/modules/kb-fs/file-change-notifier.js +6 -8
- package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +0 -7
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +4 -12
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/onboarding/starter-pack.service.d.ts +1 -3
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -1
- package/dist/modules/onboarding/starter-pack.service.js +1 -1
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -1
- package/dist/modules/version-control/git/git.service.d.ts +18 -0
- package/dist/modules/version-control/git/git.service.d.ts.map +1 -1
- package/dist/modules/version-control/git/git.service.js +53 -2
- package/dist/modules/version-control/git/git.service.js.map +1 -1
- package/dist/modules/version-control/version-control-hooks.d.ts +29 -2
- package/dist/modules/version-control/version-control-hooks.d.ts.map +1 -1
- package/dist/modules/version-control/version-control-hooks.js +17 -0
- package/dist/modules/version-control/version-control-hooks.js.map +1 -1
- package/dist/modules/version-control/version-control.service.d.ts +0 -15
- package/dist/modules/version-control/version-control.service.d.ts.map +1 -1
- package/dist/modules/version-control/version-control.service.js +4 -13
- package/dist/modules/version-control/version-control.service.js.map +1 -1
- package/dist/shared/fire-and-forget.d.ts +14 -0
- package/dist/shared/fire-and-forget.d.ts.map +1 -0
- package/dist/shared/fire-and-forget.js +23 -0
- package/dist/shared/fire-and-forget.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/capturing-logger.ts +36 -0
- package/src/__tests__/real-access.ts +16 -0
- package/src/__tests__/real-auth.ts +13 -0
- package/src/__tests__/real-readers.ts +27 -0
- package/src/__tests__/real-version-control.ts +22 -0
- package/src/core/__tests__/commit-announcements-wired-once.test.ts +53 -0
- package/src/core/create-core-services.ts +4 -2
- package/src/modules/access/__tests__/access-requests.routes.test.ts +3 -3
- package/src/modules/access/__tests__/groups-admin.service.test.ts +20 -20
- package/src/modules/access/__tests__/roles-admin.service.test.ts +12 -12
- package/src/modules/access/__tests__/roles-capabilities.test.ts +11 -11
- package/src/modules/access/__tests__/roles.routes.test.ts +8 -8
- package/src/modules/access/__tests__/synced-groups-committer.test.ts +2 -2
- package/src/modules/access/__tests__/user-access-removal.service.test.ts +6 -6
- package/src/modules/database/__tests__/connection.pg.test.ts +55 -0
- package/src/modules/database/__tests__/connection.test.ts +30 -0
- package/src/modules/database/connection.ts +12 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +6 -20
- package/src/modules/kb-fs/__tests__/file-change-notifier.test.ts +23 -4
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +8 -49
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +73 -10
- package/src/modules/kb-fs/file-change-notifier.ts +55 -32
- package/src/modules/kb-fs/locking-filesystem.ts +4 -18
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +4 -4
- package/src/modules/onboarding/starter-pack.service.ts +2 -4
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +46 -21
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +6 -6
- package/src/modules/version-control/__tests__/version-control.service.facade.test.ts +4 -4
- package/src/modules/version-control/__tests__/version-control.service.releaseLock.test.ts +43 -33
- package/src/modules/version-control/git/__tests__/git.service.commitLanded.test.ts +199 -0
- package/src/modules/version-control/git/__tests__/git.service.mergeChangeRequest.test.ts +79 -2
- package/src/modules/version-control/git/git.service.ts +55 -2
- package/src/modules/version-control/version-control-hooks.ts +41 -2
- package/src/modules/version-control/version-control.service.ts +4 -12
- package/src/shared/__tests__/fire-and-forget.test.ts +39 -0
- package/src/shared/fire-and-forget.ts +26 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fire-and-forget.js","sourceRoot":"","sources":["../../src/shared/fire-and-forget.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAI,QAAkC,EAAE,KAAQ,EAAE,GAAY,EAAE,MAAc;IAC7G,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,QAAQ,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,aAAa,CAAC,MAAM,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,GAAY,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAClG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAQ,KAA8B,CAAC,IAAI,KAAK,UAAU,CAAC;AACnH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, version control (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
56
56
|
"yaml": "^2.9.0",
|
|
57
57
|
"zod": "^3.24.0",
|
|
58
|
-
"@bevel-software/platform-mcp-core": "0.
|
|
59
|
-
"@bevel-software/platform-shared": "0.
|
|
58
|
+
"@bevel-software/platform-mcp-core": "0.37.0",
|
|
59
|
+
"@bevel-software/platform-shared": "0.37.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ILogger, LogFields } from '../shared/logger.contract.js';
|
|
2
|
+
|
|
3
|
+
/** One line a capturing logger kept: its level, message, and fields with the child bindings folded in. */
|
|
4
|
+
export interface CapturedLine {
|
|
5
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
6
|
+
message: string;
|
|
7
|
+
fields: LogFields;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A logger that keeps every line instead of printing it — the one capturing
|
|
12
|
+
* implementation a suite installs through `setLogger` (restoring the previous
|
|
13
|
+
* one in `finally`) or hands to code that takes an `ILogger`, when what is
|
|
14
|
+
* logged is the behaviour under test. Children fold their bindings into each
|
|
15
|
+
* line's fields, so `module: 'database'` is asserted like any other field.
|
|
16
|
+
*/
|
|
17
|
+
export function capturingLogger(): { logger: ILogger; lines: CapturedLine[] } {
|
|
18
|
+
const lines: CapturedLine[] = [];
|
|
19
|
+
const make = (bindings: LogFields): ILogger => {
|
|
20
|
+
const line = (level: CapturedLine['level']) => (message: string, fields: LogFields = {}) => {
|
|
21
|
+
// Like the production sink, the child's `module` is the logger's own and
|
|
22
|
+
// a call cannot rename it; every other field a call passes wins.
|
|
23
|
+
const folded: LogFields = { ...bindings, ...fields };
|
|
24
|
+
if (bindings.module !== undefined) folded.module = bindings.module;
|
|
25
|
+
lines.push({ level, message, fields: folded });
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
debug: line('debug'),
|
|
29
|
+
info: line('info'),
|
|
30
|
+
warn: line('warn'),
|
|
31
|
+
error: line('error'),
|
|
32
|
+
child: (more) => make({ ...bindings, ...more }),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
return { logger: make({}), lines };
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ITreeWalker } from '../shared/fs.contract.js';
|
|
2
|
+
import type { WorkspaceService } from '../modules/workspace/index.js';
|
|
3
|
+
import { AccessControlService } from '../modules/access/access-control.service.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The access resolver over a real tree, wired the way the composition root
|
|
7
|
+
* wires it (`core/create-core-services.ts`). A module's suite whose
|
|
8
|
+
* assertions are what the real rules answer after its service ran — who may
|
|
9
|
+
* read, who may write — takes it from HERE rather than constructing another
|
|
10
|
+
* module's implementation itself: the service is on no module entry, by
|
|
11
|
+
* design (a module receives `IAccessControl` by injection), and this fixture
|
|
12
|
+
* stands in for the root, as `real-disk.ts` does for the disk.
|
|
13
|
+
*/
|
|
14
|
+
export function realAccessControl(workspace: WorkspaceService, kbDirName: string, disk: ITreeWalker): AccessControlService {
|
|
15
|
+
return new AccessControlService(workspace, kbDirName, disk);
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AuthService } from '../modules/auth/auth.service.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The auth service, constructed the way the composition root constructs it
|
|
5
|
+
* (`core/create-core-services.ts`). A module's suite that drives routes which
|
|
6
|
+
* take the real service — the access routes, which it guards — takes it from
|
|
7
|
+
* HERE rather than constructing another module's implementation itself: the
|
|
8
|
+
* auth entry holds the service's contract and the class behind it is the
|
|
9
|
+
* root's to wire, as `real-disk.ts` stands in for the root for the disk.
|
|
10
|
+
*/
|
|
11
|
+
export function realAuthService(...args: ConstructorParameters<typeof AuthService>): AuthService {
|
|
12
|
+
return new AuthService(...args);
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FileReaderRegistry } from '../modules/workspace/index.js';
|
|
2
|
+
import { createFileReaderRegistry } from '../modules/workspace/file-readers/file-reader.registry.js';
|
|
3
|
+
import type { DocExtractOutcome, DocExtractService } from '../modules/workspace/file-readers/doc-extract.service.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The file readers, composed the way the composition root composes them
|
|
7
|
+
* (`core/create-core-services.ts`) over a document extractor. A module's suite
|
|
8
|
+
* that reads real files through them takes them from HERE rather than
|
|
9
|
+
* composing another module's internals itself: the registry factory is on no
|
|
10
|
+
* module entry, by design (a module receives the registry by injection), and
|
|
11
|
+
* this fixture stands in for the root, as `real-disk.ts` does for the disk.
|
|
12
|
+
*/
|
|
13
|
+
export function realFileReaders(docExtract: DocExtractService = noDocExtract()): FileReaderRegistry {
|
|
14
|
+
return createFileReaderRegistry(docExtract);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A document extractor that extracts nothing: a suite that reads no
|
|
19
|
+
* document-format file gets this typed refusal, not a silent empty text.
|
|
20
|
+
*/
|
|
21
|
+
export function noDocExtract(): DocExtractService {
|
|
22
|
+
const extract = async (): Promise<DocExtractOutcome> => ({
|
|
23
|
+
ok: false,
|
|
24
|
+
message: 'this suite reads no document-format file',
|
|
25
|
+
});
|
|
26
|
+
return { extract } satisfies Pick<DocExtractService, 'extract'> as DocExtractService;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
2
|
+
import type { WorkspaceService } from '../modules/workspace/index.js';
|
|
3
|
+
import { GitService } from '../modules/version-control/git/git.service.js';
|
|
4
|
+
import { VersionControlHooks } from '../modules/version-control/version-control-hooks.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The version-control collaborators, wired the way the composition root wires
|
|
8
|
+
* them (`core/create-core-services.ts`): the git service over a workspace and
|
|
9
|
+
* the lifecycle hooks it consults — where every commit it lands is reported
|
|
10
|
+
* (`onCommitLanded`). A module's suite that commits to a real repository
|
|
11
|
+
* takes them from HERE rather than constructing another module's
|
|
12
|
+
* implementation itself: none of them is on a module entry, by design — they
|
|
13
|
+
* arrive by injection — and this fixture stands in for the root, as
|
|
14
|
+
* `real-disk.ts` does for the disk.
|
|
15
|
+
*/
|
|
16
|
+
export function realVersionControlHooks(): VersionControlHooks {
|
|
17
|
+
return new VersionControlHooks();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function realGitService(workspace: WorkspaceService, hooks: VersionControlHooks, kb: KbContext): GitService {
|
|
21
|
+
return new GitService(workspace, hooks, kb);
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A landed commit is announced from ONE place. The git service reports every
|
|
8
|
+
* commit it lands through `VersionControlHooks.commitLanded`; the composition
|
|
9
|
+
* root hands that to the file-change notifier with one registration; nothing
|
|
10
|
+
* else emits on the notifier or calls the hook. A second announcer is how a
|
|
11
|
+
* batch gets announced twice, and a committer that announces on its own is how
|
|
12
|
+
* the next one forgets to — both were real findings on the release that made
|
|
13
|
+
* this rule, so the rule is pinned at the source level.
|
|
14
|
+
*/
|
|
15
|
+
const SRC = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
|
+
const ROOT = path.join(SRC, 'core', 'create-core-services.ts');
|
|
17
|
+
|
|
18
|
+
async function shippedSources(dir: string, out: string[] = []): Promise<string[]> {
|
|
19
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
20
|
+
const full = path.join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
if (entry.name !== '__tests__') await shippedSources(full, out);
|
|
23
|
+
} else if (/\.ts$/.test(entry.name) && !/\.test\.ts$/.test(entry.name)) out.push(full);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function filesContaining(needle: RegExp): Promise<string[]> {
|
|
29
|
+
const hits: string[] = [];
|
|
30
|
+
for (const file of await shippedSources(SRC)) {
|
|
31
|
+
if (needle.test(await fs.readFile(file, 'utf8'))) hits.push(path.relative(SRC, file).replace(/\\/g, '/'));
|
|
32
|
+
}
|
|
33
|
+
return hits.sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('commit announcements are wired once', () => {
|
|
37
|
+
it('the composition root hands the git layer\'s landed commits to the notifier with one registration', async () => {
|
|
38
|
+
const source = await fs.readFile(ROOT, 'utf8');
|
|
39
|
+
const registrations = source.match(/versionControlHooks\.onCommitLanded\(/g) ?? [];
|
|
40
|
+
expect(registrations).toHaveLength(1);
|
|
41
|
+
expect(source).toContain('versionControlHooks.onCommitLanded((change) => fileChangeNotifier.emit(change));');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('only the git service reports a landed commit, and only the root registers for it', async () => {
|
|
45
|
+
expect(await filesContaining(/\.commitLanded\(/)).toEqual(['modules/version-control/git/git.service.ts']);
|
|
46
|
+
expect(await filesContaining(/\.onCommitLanded\(/)).toEqual(['core/create-core-services.ts']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('nothing but the root emits on the notifier, and no committer announces on its own', async () => {
|
|
50
|
+
expect(await filesContaining(/fileChangeNotifier\.emit\(|fileChanges\??\.emit\(/)).toEqual(['core/create-core-services.ts']);
|
|
51
|
+
expect(await filesContaining(/announceCommitted|announceReleased/)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1024,6 +1024,10 @@ export async function createCoreServices(
|
|
|
1024
1024
|
// id-repair keeps the id namespace collision-free. Subscribers are registered
|
|
1025
1025
|
// below, once the reacting services exist.
|
|
1026
1026
|
const fileChangeNotifier = new FileChangeNotifier();
|
|
1027
|
+
// Every commit the git service lands — a save, an autosave checkpoint, a
|
|
1028
|
+
// batch, a merge, a `commit_change` — reaches the notifier through this one
|
|
1029
|
+
// registration, and nothing else emits on it (a test pins both).
|
|
1030
|
+
versionControlHooks.onCommitLanded((change) => fileChangeNotifier.emit(change));
|
|
1027
1031
|
// Who tells the reviewers of an opened change request: the composition root
|
|
1028
1032
|
// of a distribution registers its listener here, after this returns (see
|
|
1029
1033
|
// `ChangeRequestHooks`).
|
|
@@ -1040,7 +1044,6 @@ export async function createCoreServices(
|
|
|
1040
1044
|
kb,
|
|
1041
1045
|
changeGate,
|
|
1042
1046
|
eventBus,
|
|
1043
|
-
fileChangeNotifier,
|
|
1044
1047
|
// Exposed as `versionControlService.hooks` — the SAME instance GitService and
|
|
1045
1048
|
// the agent-access gate consult, so a deployment's registrations against
|
|
1046
1049
|
// it reach every hook point.
|
|
@@ -1254,7 +1257,6 @@ export async function createCoreServices(
|
|
|
1254
1257
|
pluginSource,
|
|
1255
1258
|
pluginLocks: pluginProvisionService,
|
|
1256
1259
|
events: eventBus,
|
|
1257
|
-
fileChanges: fileChangeNotifier,
|
|
1258
1260
|
// Its own lock: one starter-pack choice at a time, a concern no other
|
|
1259
1261
|
// service's mutex serialises.
|
|
1260
1262
|
choosing: new WorkspaceMutex(),
|
|
@@ -6,13 +6,13 @@ import { joinBranchFor, type IVersionControlService } from '@bevel-software/plat
|
|
|
6
6
|
|
|
7
7
|
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
8
8
|
import { testKbContext, TEST_BRANCH_MODEL } from '../../../__tests__/kb-context.js';
|
|
9
|
-
import type { AuthService } from '../../auth/
|
|
10
|
-
import type { WorkspaceService } from '../../workspace/
|
|
9
|
+
import type { AuthService } from '../../auth/index.js';
|
|
10
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
11
11
|
import type { IAccessControl } from '../access-control.interface.js';
|
|
12
12
|
import type { IAccessRequestLifecycle } from '../access-requests.contract.js';
|
|
13
13
|
import { createAccessRequestRoutes } from '../access-requests.routes.js';
|
|
14
14
|
import { ChangeRequestConflictsError } from '../../../shared/domain-errors.js';
|
|
15
|
-
import { pendingProposals } from '../../plugins/
|
|
15
|
+
import { pendingProposals } from '../../plugins/index.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Asking for Can edit or Owner on an item, and what the editors then see.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
|
|
7
|
-
import type { WorkspaceService } from '../../workspace/
|
|
8
|
-
import type { VersionControlService } from '../../version-control/
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
8
|
+
import type { VersionControlService } from '../../version-control/index.js';
|
|
9
9
|
import type { AuthUser, FileTreeEntry } from '@bevel-software/platform-shared';
|
|
10
10
|
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
11
11
|
import { AccessControlService } from '../access-control.service.js';
|
|
@@ -110,8 +110,8 @@ describe('GroupsAdminService', () => {
|
|
|
110
110
|
service = new GroupsAdminService(
|
|
111
111
|
workspace,
|
|
112
112
|
versionControl.svc,
|
|
113
|
-
new AccessControlService(workspace as never, KB,
|
|
114
|
-
|
|
113
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
114
|
+
realDisk(),
|
|
115
115
|
KB,
|
|
116
116
|
() => DEFAULT_BRANCH,
|
|
117
117
|
);
|
|
@@ -272,7 +272,7 @@ describe('GroupsAdminService', () => {
|
|
|
272
272
|
'---\nread:\n - deny everyone\n - Sara <sara@x.io>\nwrite:\n - Sara <sara@x.io>\n---\n',
|
|
273
273
|
);
|
|
274
274
|
await write(repo, 'Plugins/Sara/Notes.md', "---\nread: ['group/GTM Team']\n---\n# Notes\n");
|
|
275
|
-
const access = new AccessControlService(stubWorkspace(root) as never, KB,
|
|
275
|
+
const access = new AccessControlService(stubWorkspace(root) as never, KB, realDisk());
|
|
276
276
|
expect(await access.canRead(workspaceIdForBranch(DEFAULT_BRANCH), ADMIN.email, 'Plugins/Sara/Notes.md')).toBe(false);
|
|
277
277
|
|
|
278
278
|
await service.renameGroup(ADMIN, 'gtm team', 'Go To Market');
|
|
@@ -289,7 +289,7 @@ describe('GroupsAdminService', () => {
|
|
|
289
289
|
// `assertCanWriteAtRef`); this stub does the same through the live model.
|
|
290
290
|
function gatedService(): { gated: GroupsAdminService; gatedCommits: { summary: string }[] } {
|
|
291
291
|
const workspace = stubWorkspace(root);
|
|
292
|
-
const access = new AccessControlService(workspace as never, KB,
|
|
292
|
+
const access = new AccessControlService(workspace as never, KB, realDisk());
|
|
293
293
|
const versionControl = stubVersionControl();
|
|
294
294
|
const svc = versionControl.svc as unknown as Record<string, unknown>;
|
|
295
295
|
svc.commitChanges = async (ws: string, user: AuthUser, summary: string, onlyPaths: string[] = []) => {
|
|
@@ -300,7 +300,7 @@ describe('GroupsAdminService', () => {
|
|
|
300
300
|
versionControl.commits.push({ summary });
|
|
301
301
|
return {} as unknown;
|
|
302
302
|
};
|
|
303
|
-
const gated = new GroupsAdminService(workspace, versionControl.svc, access,
|
|
303
|
+
const gated = new GroupsAdminService(workspace, versionControl.svc, access, realDisk(), KB, () => DEFAULT_BRANCH);
|
|
304
304
|
return { gated, gatedCommits: versionControl.commits };
|
|
305
305
|
}
|
|
306
306
|
|
|
@@ -395,8 +395,8 @@ describe('GroupsAdminService', () => {
|
|
|
395
395
|
const svc = new GroupsAdminService(
|
|
396
396
|
workspace,
|
|
397
397
|
versionControl.svc,
|
|
398
|
-
new AccessControlService(workspace as never, KB,
|
|
399
|
-
|
|
398
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
399
|
+
realDisk(),
|
|
400
400
|
KB,
|
|
401
401
|
() => DEFAULT_BRANCH,
|
|
402
402
|
);
|
|
@@ -442,8 +442,8 @@ describe('GroupsAdminService', () => {
|
|
|
442
442
|
const svc = new GroupsAdminService(
|
|
443
443
|
workspace,
|
|
444
444
|
versionControl.svc,
|
|
445
|
-
new AccessControlService(workspace as never, KB,
|
|
446
|
-
|
|
445
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
446
|
+
realDisk(),
|
|
447
447
|
KB,
|
|
448
448
|
() => DEFAULT_BRANCH,
|
|
449
449
|
);
|
|
@@ -482,8 +482,8 @@ describe('GroupsAdminService', () => {
|
|
|
482
482
|
const svc = new GroupsAdminService(
|
|
483
483
|
workspace,
|
|
484
484
|
versionControl.svc,
|
|
485
|
-
new AccessControlService(workspace as never, KB,
|
|
486
|
-
|
|
485
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
486
|
+
realDisk(),
|
|
487
487
|
KB,
|
|
488
488
|
() => DEFAULT_BRANCH,
|
|
489
489
|
);
|
|
@@ -538,8 +538,8 @@ describe('groups roster referencedBy — the mirror of the roles roster attribut
|
|
|
538
538
|
const svc = new GroupsAdminService(
|
|
539
539
|
workspace,
|
|
540
540
|
stubVersionControl().svc,
|
|
541
|
-
new AccessControlService(workspace as never, KB,
|
|
542
|
-
|
|
541
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
542
|
+
realDisk(),
|
|
543
543
|
KB,
|
|
544
544
|
() => DEFAULT_BRANCH,
|
|
545
545
|
);
|
|
@@ -581,8 +581,8 @@ describe('groups roster referencedBy — the mirror of the roles roster attribut
|
|
|
581
581
|
await write(repo2, 'team/access.md', '---\nwrite:\n - Reviewer\n---\n');
|
|
582
582
|
await write(repo2, 'secret/Plan.md', '---\nread:\n - deny everyone\n - Reviewer\n---\n# Plan\n');
|
|
583
583
|
const workspace = stubWorkspace(root2);
|
|
584
|
-
const access = new AccessControlService(workspace as never, KB,
|
|
585
|
-
const svc = new GroupsAdminService(workspace, stubVersionControl().svc, access,
|
|
584
|
+
const access = new AccessControlService(workspace as never, KB, realDisk());
|
|
585
|
+
const svc = new GroupsAdminService(workspace, stubVersionControl().svc, access, realDisk(), KB, () => DEFAULT_BRANCH);
|
|
586
586
|
const rowsFor = async (email: string) =>
|
|
587
587
|
(await svc.getRoster(email)).groups.find((g) => g.canonical === 'reviewer')!.bareNameResolvesToRole;
|
|
588
588
|
|
|
@@ -620,8 +620,8 @@ describe('groups roster referencedBy — the mirror of the roles roster attribut
|
|
|
620
620
|
const svc = new GroupsAdminService(
|
|
621
621
|
workspace,
|
|
622
622
|
stubVersionControl().svc,
|
|
623
|
-
new AccessControlService(workspace as never, KB,
|
|
624
|
-
|
|
623
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
624
|
+
realDisk(),
|
|
625
625
|
KB,
|
|
626
626
|
() => DEFAULT_BRANCH,
|
|
627
627
|
);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
|
|
7
|
-
import type { WorkspaceService } from '../../workspace/
|
|
8
|
-
import type { VersionControlService } from '../../version-control/
|
|
9
|
-
import type { VersionControlEventBus } from '../../version-control/
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
8
|
+
import type { VersionControlService } from '../../version-control/index.js';
|
|
9
|
+
import type { VersionControlEventBus } from '../../version-control/index.js';
|
|
10
10
|
import type { FileTreeEntry, VersionControlEventPayload } from '@bevel-software/platform-shared';
|
|
11
11
|
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
12
12
|
import { AccessControlService } from '../access-control.service.js';
|
|
@@ -168,10 +168,10 @@ describe('RolesAdminService', () => {
|
|
|
168
168
|
repo = path.join(workspaceDir, KB);
|
|
169
169
|
await write(repo, 'roles.yaml', ROLES);
|
|
170
170
|
ws = stubWorkspace(workspaceDir);
|
|
171
|
-
access = new AccessControlService(ws, KB,
|
|
171
|
+
access = new AccessControlService(ws, KB, realDisk());
|
|
172
172
|
versionControl = stubVersionControl();
|
|
173
173
|
bus = stubEventBus();
|
|
174
|
-
svc = new RolesAdminService(ws, versionControl.svc, access,
|
|
174
|
+
svc = new RolesAdminService(ws, versionControl.svc, access, realDisk(), KB, () => DEFAULT_BRANCH, bus.bus, [
|
|
175
175
|
'recovery-admin@example.com',
|
|
176
176
|
]);
|
|
177
177
|
});
|
|
@@ -193,7 +193,7 @@ describe('RolesAdminService', () => {
|
|
|
193
193
|
// `DEFAULT_BRANCH` and froze it — every roles route then failed branch
|
|
194
194
|
// validation with `Invalid branch name "": empty name` until a restart.
|
|
195
195
|
let branch = '';
|
|
196
|
-
const late = new RolesAdminService(ws, versionControl.svc, access,
|
|
196
|
+
const late = new RolesAdminService(ws, versionControl.svc, access, realDisk(), KB, () => branch, bus.bus);
|
|
197
197
|
branch = DEFAULT_BRANCH;
|
|
198
198
|
const roster = await late.getRoster(ADMIN.email);
|
|
199
199
|
expect(roster.map((r) => r.canonical)).toEqual(['admin', 'sales']);
|
|
@@ -257,7 +257,7 @@ describe('RolesAdminService', () => {
|
|
|
257
257
|
const corrupt = 'roles:\n Admin:\n - a@x.eu\n Admin:\n - b@x.eu\n';
|
|
258
258
|
await write(repo, 'roles.yaml', corrupt);
|
|
259
259
|
access.invalidate(WS);
|
|
260
|
-
const adminless = new RolesAdminService(ws, versionControl.svc, access,
|
|
260
|
+
const adminless = new RolesAdminService(ws, versionControl.svc, access, realDisk(), KB, () => DEFAULT_BRANCH, bus.bus);
|
|
261
261
|
// 409, not 500: the route helper hides >=500 bodies behind a generic
|
|
262
262
|
// message, and this break-glass refusal must reach the operator verbatim.
|
|
263
263
|
await expect(adminless.recover(ADMIN)).rejects.toMatchObject({
|
|
@@ -572,8 +572,8 @@ describe('roster referencedBy under group shadowing', () => {
|
|
|
572
572
|
svc = new RolesAdminService(
|
|
573
573
|
ws,
|
|
574
574
|
stubVersionControl().svc,
|
|
575
|
-
new AccessControlService(ws, KB,
|
|
576
|
-
|
|
575
|
+
new AccessControlService(ws, KB, realDisk()),
|
|
576
|
+
realDisk(),
|
|
577
577
|
KB,
|
|
578
578
|
() => DEFAULT_BRANCH,
|
|
579
579
|
stubEventBus().bus,
|
|
@@ -630,8 +630,8 @@ describe('Admin fixed members — the deployment admin', () => {
|
|
|
630
630
|
return new RolesAdminService(
|
|
631
631
|
ws,
|
|
632
632
|
versionControl.svc,
|
|
633
|
-
new AccessControlService(ws, KB,
|
|
634
|
-
|
|
633
|
+
new AccessControlService(ws, KB, realDisk()),
|
|
634
|
+
realDisk(),
|
|
635
635
|
KB,
|
|
636
636
|
() => DEFAULT_BRANCH,
|
|
637
637
|
stubEventBus().bus,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
|
|
7
|
-
import type { WorkspaceService } from '../../workspace/
|
|
8
|
-
import type { VersionControlService } from '../../version-control/
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
8
|
+
import type { VersionControlService } from '../../version-control/index.js';
|
|
9
9
|
import type { AuthUser, FileTreeEntry } from '@bevel-software/platform-shared';
|
|
10
10
|
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
11
11
|
import { AccessControlService } from '../access-control.service.js';
|
|
@@ -131,8 +131,8 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
|
|
|
131
131
|
service = new RolesAdminService(
|
|
132
132
|
workspace,
|
|
133
133
|
versionControl.svc,
|
|
134
|
-
new AccessControlService(workspace as never, KB,
|
|
135
|
-
|
|
134
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
135
|
+
realDisk(),
|
|
136
136
|
KB,
|
|
137
137
|
() => DEFAULT_BRANCH,
|
|
138
138
|
);
|
|
@@ -216,8 +216,8 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
|
|
|
216
216
|
const svc = new RolesAdminService(
|
|
217
217
|
workspace,
|
|
218
218
|
versionControl.svc,
|
|
219
|
-
new AccessControlService(workspace as never, KB,
|
|
220
|
-
|
|
219
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
220
|
+
realDisk(),
|
|
221
221
|
KB,
|
|
222
222
|
() => DEFAULT_BRANCH,
|
|
223
223
|
);
|
|
@@ -280,8 +280,8 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
|
|
|
280
280
|
const svc = new RolesAdminService(
|
|
281
281
|
workspace,
|
|
282
282
|
versionControl.svc,
|
|
283
|
-
new AccessControlService(workspace as never, KB,
|
|
284
|
-
|
|
283
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
284
|
+
realDisk(),
|
|
285
285
|
KB,
|
|
286
286
|
() => DEFAULT_BRANCH,
|
|
287
287
|
);
|
|
@@ -305,8 +305,8 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
|
|
|
305
305
|
const svc = new RolesAdminService(
|
|
306
306
|
workspace,
|
|
307
307
|
versionControl.svc,
|
|
308
|
-
new AccessControlService(workspace as never, KB,
|
|
309
|
-
|
|
308
|
+
new AccessControlService(workspace as never, KB, realDisk()),
|
|
309
|
+
realDisk(),
|
|
310
310
|
KB,
|
|
311
311
|
() => DEFAULT_BRANCH,
|
|
312
312
|
);
|
|
@@ -7,13 +7,13 @@ import express from 'express';
|
|
|
7
7
|
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
8
8
|
import type { SQL } from 'drizzle-orm';
|
|
9
9
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
10
|
-
import {
|
|
10
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
11
11
|
|
|
12
12
|
import type { IAccessControl } from '../access-control.interface.js';
|
|
13
|
-
import type { WorkspaceService } from '../../workspace/
|
|
14
|
-
import {
|
|
15
|
-
import type { VersionControlService } from '../../version-control/
|
|
16
|
-
import type { VersionControlEventBus } from '../../version-control/
|
|
13
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
14
|
+
import { realAuthService } from '../../../__tests__/real-auth.js';
|
|
15
|
+
import type { VersionControlService } from '../../version-control/index.js';
|
|
16
|
+
import type { VersionControlEventBus } from '../../version-control/index.js';
|
|
17
17
|
import type { FileTreeEntry } from '@bevel-software/platform-shared';
|
|
18
18
|
import { createAccessRoutes } from '../access.routes.js';
|
|
19
19
|
import { usersDbDouble } from './users-db-double.js';
|
|
@@ -64,7 +64,7 @@ async function makeHarness(
|
|
|
64
64
|
const realAccess = new AccessControlService(
|
|
65
65
|
{ getWorkspacePath: async () => workspaceDir, readFile: async () => ROLES } as unknown as WorkspaceService,
|
|
66
66
|
KB,
|
|
67
|
-
|
|
67
|
+
realDisk(),
|
|
68
68
|
);
|
|
69
69
|
const accessControl = {
|
|
70
70
|
// Admin gate: the route's assertCanMutate calls canWrite('roles.yaml').
|
|
@@ -173,7 +173,7 @@ async function makeHarness(
|
|
|
173
173
|
|
|
174
174
|
// The real auth service over the same double, so the promotion's switch
|
|
175
175
|
// lock and read run as they do in production (`whileSwitchedOn`).
|
|
176
|
-
const authService =
|
|
176
|
+
const authService = realAuthService(db, { jwtSecret: 'test', adminEmail: '', adminPassword: '', allowedEmailDomains: [] });
|
|
177
177
|
vi.spyOn(authService, 'getUserById').mockImplementation(async () => ADMIN);
|
|
178
178
|
|
|
179
179
|
const app = express();
|
|
@@ -182,7 +182,7 @@ async function makeHarness(
|
|
|
182
182
|
(req as unknown as { userId: string }).userId = ADMIN.id;
|
|
183
183
|
next();
|
|
184
184
|
});
|
|
185
|
-
app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, versionControlService, eventBus, db, testKbContext({ kbDirName: KB }),
|
|
185
|
+
app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, versionControlService, eventBus, db, testKbContext({ kbDirName: KB }), realDisk()));
|
|
186
186
|
|
|
187
187
|
const server = await new Promise<Server>((resolve) => {
|
|
188
188
|
const s = app.listen(0, () => resolve(s));
|
|
@@ -4,9 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
|
|
6
6
|
import type { AuthUser, IVersionControlService } from '@bevel-software/platform-shared';
|
|
7
|
-
import type { WorkspaceService } from '../../workspace/
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
8
8
|
import type { AccessControlService } from '../access-control.service.js';
|
|
9
|
-
import type { VersionControlEventBus } from '../../version-control/
|
|
9
|
+
import type { VersionControlEventBus } from '../../version-control/index.js';
|
|
10
10
|
import { createSyncedGroupsCommitter } from '../synced-groups-committer.js';
|
|
11
11
|
import { PushNeedsAgentResolutionError, VersionControlValidationError } from '../../../shared/domain-errors.js';
|
|
12
12
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
|
-
import type { WorkspaceService } from '../../workspace/
|
|
9
|
-
import type { VersionControlService } from '../../version-control/
|
|
8
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
9
|
+
import type { VersionControlService } from '../../version-control/index.js';
|
|
10
10
|
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
11
11
|
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
12
12
|
import { PushNeedsAgentResolutionError } from '../../../shared/domain-errors.js';
|
|
@@ -167,8 +167,8 @@ describe('UserAccessRemovalService', () => {
|
|
|
167
167
|
|
|
168
168
|
const read = (rel: string) => fs.readFile(path.join(repo, rel), 'utf-8');
|
|
169
169
|
|
|
170
|
-
function build(versionControl = stubVersionControl(), owners: string[] = ['owner@x.io'], disk: ITreeWalker =
|
|
171
|
-
const access = new AccessControlService(workspace as never, KB,
|
|
170
|
+
function build(versionControl = stubVersionControl(), owners: string[] = ['owner@x.io'], disk: ITreeWalker = realDisk()) {
|
|
171
|
+
const access = new AccessControlService(workspace as never, KB, realDisk());
|
|
172
172
|
return {
|
|
173
173
|
versionControl,
|
|
174
174
|
service: new UserAccessRemovalService(workspace, versionControl.svc, access, disk, KB, () => DEFAULT_BRANCH, undefined, owners),
|
|
@@ -356,7 +356,7 @@ describe('UserAccessRemovalService', () => {
|
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
it('refuses to commit a partial cleanup when a folder cannot be listed', async () => {
|
|
359
|
-
const real =
|
|
359
|
+
const real = realDisk();
|
|
360
360
|
const holey = {
|
|
361
361
|
walkKb: async (...args: Parameters<ITreeWalker['walkKb']>) => {
|
|
362
362
|
const result = await real.walkKb(...args);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
import { closeDb, createDb } from '../connection.js';
|
|
4
|
+
import { setLogger } from '../../../shared/logging.js';
|
|
5
|
+
import { capturingLogger } from '../../../__tests__/capturing-logger.js';
|
|
6
|
+
import { PG_ADMIN_URL, PG_TEST_TIMEOUT, withPgAdmin } from '../../../__tests__/pg-scratch.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The recovery the pool's error listener promises, on a real Postgres: a
|
|
10
|
+
* pooled client the server terminates while it idles — a restart, a forced
|
|
11
|
+
* database drop, an administrator's `pg_terminate_backend` — is logged and
|
|
12
|
+
* let go, and the next query opens a fresh connection and succeeds. The unit
|
|
13
|
+
* case in `connection.test.ts` pins the listener on a pool that never
|
|
14
|
+
* connected; this one pins what the listener is for.
|
|
15
|
+
*
|
|
16
|
+
* Runs where `TEST_DATABASE_URL` names a server; only connects to the
|
|
17
|
+
* database it names and terminates this test's own backend. Writes nothing.
|
|
18
|
+
*/
|
|
19
|
+
describe.skipIf(!PG_ADMIN_URL)('a pooled connection the server terminates', () => {
|
|
20
|
+
it(
|
|
21
|
+
'is logged with the error, and the next query succeeds on a new connection',
|
|
22
|
+
async () => {
|
|
23
|
+
const { logger: capturing, lines } = capturingLogger();
|
|
24
|
+
const previous = setLogger(capturing);
|
|
25
|
+
const db = createDb(PG_ADMIN_URL!);
|
|
26
|
+
try {
|
|
27
|
+
const backendPid = async () => Number((await db.execute(sql`select pg_backend_pid() as pid`)).rows[0]!.pid);
|
|
28
|
+
// The query's client now idles in the pool; the server ends it.
|
|
29
|
+
const first = await backendPid();
|
|
30
|
+
const noticed = new Promise<void>((resolve) => db.$client.once('error', () => resolve()));
|
|
31
|
+
// The signal is checked before the wait: a backend that was not
|
|
32
|
+
// terminated would otherwise surface as a timeout, not as the cause.
|
|
33
|
+
const { rows } = await withPgAdmin((admin) =>
|
|
34
|
+
admin.query<{ pg_terminate_backend: boolean }>('select pg_terminate_backend($1)', [first]),
|
|
35
|
+
);
|
|
36
|
+
expect(rows[0]?.pg_terminate_backend).toBe(true);
|
|
37
|
+
await noticed;
|
|
38
|
+
|
|
39
|
+
const second = await backendPid();
|
|
40
|
+
expect(second).not.toBe(first);
|
|
41
|
+
expect(lines).toEqual([
|
|
42
|
+
{
|
|
43
|
+
level: 'warn',
|
|
44
|
+
message: 'an idle database connection was closed by the server; the next query opens a new one:',
|
|
45
|
+
fields: { module: 'database', err: expect.objectContaining({ code: '57P01' }) },
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
} finally {
|
|
49
|
+
await closeDb(db);
|
|
50
|
+
setLogger(previous);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
PG_TEST_TIMEOUT,
|
|
54
|
+
);
|
|
55
|
+
});
|