@bevel-software/platform-core-backend 0.23.0 → 0.25.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-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +17 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +25 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +12 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +10 -0
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +16 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +17 -0
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +4 -1
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +7 -3
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +38 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +39 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +2 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +2 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
- package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +4 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +57 -16
- package/dist/modules/auth/account-erasure.service.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +25 -15
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +66 -35
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/connection.d.ts +16 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +117 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +296 -96
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +81 -31
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts +95 -1
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +390 -2
- package/dist/modules/database/migrate.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +17 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +29 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +8 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +38 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-records.store.js +7 -4
- package/dist/modules/plugins/join-request-records.store.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +8 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +30 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +80 -0
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
- package/dist/modules/tool-registry/description-length.js +108 -0
- package/dist/modules/tool-registry/description-length.js.map +1 -0
- package/dist/modules/workflow/git/git.service.d.ts +25 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +40 -1
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +5 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +7 -3
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +4 -1
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.js +210 -0
- package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.store.js +553 -0
- package/dist/modules/workspace/agent-upload.store.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +46 -4
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/upload-limits.d.ts +13 -0
- package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
- package/dist/modules/workspace/upload-limits.js +13 -0
- package/dist/modules/workspace/upload-limits.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +1 -1
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +13 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +61 -33
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +11 -9
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +570 -134
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +0 -6
- package/dist/modules/workspace/write-denial.d.ts.map +1 -1
- package/dist/modules/workspace/write-denial.js +0 -6
- package/dist/modules/workspace/write-denial.js.map +1 -1
- package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
- package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
- package/dist/modules/workspace/zip-entry-rules.js +154 -0
- package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
- package/dist/shared/column-crypto.d.ts +194 -0
- package/dist/shared/column-crypto.d.ts.map +1 -0
- package/dist/shared/column-crypto.js +144 -0
- package/dist/shared/column-crypto.js.map +1 -0
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -1
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +1 -0
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/dist/tenancy/tenant-secrets.d.ts +5 -1
- package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
- package/dist/tenancy/tenant-secrets.js +4 -0
- package/dist/tenancy/tenant-secrets.js.map +1 -1
- package/kb-template/AGENTS.md +42 -0
- package/migrations/0016_pii_encryption.sql +20 -0
- package/migrations/meta/0016_snapshot.json +2327 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/core/__tests__/lifecycle.test.ts +71 -4
- package/src/core/create-core-server.ts +21 -3
- package/src/core/create-core-services.ts +27 -1
- package/src/core/lifecycle.ts +19 -0
- package/src/core-config.ts +18 -0
- package/src/index.ts +25 -0
- package/src/modules/access/__tests__/users-db-double.ts +21 -12
- package/src/modules/access/access.routes.ts +4 -1
- package/src/modules/access/directory-sync-bot.ts +7 -3
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +8 -4
- package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
- package/src/modules/agent-instructions/compose.ts +50 -7
- package/src/modules/agent-instructions/index.ts +12 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
- package/src/modules/audit/agent-audit.service.ts +4 -2
- package/src/modules/auth/__tests__/account-deactivation.test.ts +2 -1
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
- package/src/modules/auth/__tests__/account.routes.test.ts +6 -3
- package/src/modules/auth/__tests__/auth.service.test.ts +3 -1
- package/src/modules/auth/account-erasure.service.ts +69 -18
- package/src/modules/auth/auth.service.ts +33 -23
- package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
- package/src/modules/code-mode/code-mode.tool.ts +80 -34
- package/src/modules/database/__tests__/connection.test.ts +12 -0
- package/src/modules/database/__tests__/pii-backfill.pg.test.ts +780 -0
- package/src/modules/database/connection.ts +117 -0
- package/src/modules/database/core-schema.ts +81 -31
- package/src/modules/database/migrate.ts +540 -2
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +93 -0
- package/src/modules/kb-fs/locking-filesystem.ts +37 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +47 -7
- package/src/modules/mcp/mcp.service.ts +46 -7
- package/src/modules/plugins/join-request-records.store.ts +7 -4
- package/src/modules/tool-auth/external-api-key.service.ts +8 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +151 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +37 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
- package/src/modules/tool-registry/description-length.ts +111 -0
- package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
- package/src/modules/workflow/git/git.service.ts +47 -1
- package/src/modules/workflow/pending-commits.service.ts +5 -1
- package/src/modules/workflow/recovery-bot.ts +7 -3
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow.service.ts +4 -1
- package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +24 -14
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +77 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +57 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +39 -36
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +196 -46
- package/src/modules/workspace/agent-upload.routes.ts +214 -0
- package/src/modules/workspace/agent-upload.store.ts +668 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
- package/src/modules/workspace/startup/steps/template-source.ts +53 -5
- package/src/modules/workspace/upload-limits.ts +12 -0
- package/src/modules/workspace/workspace.routes.ts +1 -2
- package/src/modules/workspace/workspace.service.ts +63 -37
- package/src/modules/workspace/workspace.tools.ts +647 -148
- package/src/modules/workspace/write-denial.ts +0 -8
- package/src/modules/workspace/zip-entry-rules.ts +173 -0
- package/src/shared/__tests__/column-crypto.test.ts +217 -0
- package/src/shared/column-crypto.ts +218 -0
- package/src/shared/token-crypto.ts +28 -1
- package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -0
- package/src/tenancy/static-tenant-source.ts +1 -0
- package/src/tenancy/tenant-secrets.ts +5 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { testKbContext } from '../../../../__tests__/kb-context.js';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import type { WorkspaceService } from '../../../workspace/workspace.service.js';
|
|
9
|
+
import { WorkflowHooks } from '../../workflow-hooks.js';
|
|
10
|
+
import { GitService } from '../git.service.js';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the log says when a change request's branches cannot be fetched.
|
|
16
|
+
*
|
|
17
|
+
* The fetch is best-effort: when it fails, the refs are resolved from what the
|
|
18
|
+
* working copy already has. A working copy that could not reach the remote at
|
|
19
|
+
* all then reported `unknown branch` for every branch it had never seen, and
|
|
20
|
+
* nothing said the fetch had been refused — the log named a missing branch
|
|
21
|
+
* that was on the remote the whole time.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const TRUNK = 'current-company-state';
|
|
25
|
+
const WORKSPACE = 'ws-pr-fetch';
|
|
26
|
+
|
|
27
|
+
async function runGit(cwd: string, args: string[]): Promise<void> {
|
|
28
|
+
await execFileAsync('git', args, {
|
|
29
|
+
cwd,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
|
|
33
|
+
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let root: string;
|
|
39
|
+
let repo: string;
|
|
40
|
+
let upstream: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'git-pr-fetch-'));
|
|
44
|
+
upstream = path.join(root, 'upstream.git');
|
|
45
|
+
await runGit(root, ['init', '--bare', '-b', TRUNK, upstream]);
|
|
46
|
+
const seed = path.join(root, '.seed');
|
|
47
|
+
await fs.mkdir(seed);
|
|
48
|
+
await runGit(seed, ['init', '-b', TRUNK]);
|
|
49
|
+
await runGit(seed, ['remote', 'add', 'origin', upstream]);
|
|
50
|
+
await fs.writeFile(path.join(seed, 'base.md'), 'base\n');
|
|
51
|
+
await runGit(seed, ['add', '-A']);
|
|
52
|
+
await runGit(seed, ['commit', '-m', 'init']);
|
|
53
|
+
await runGit(seed, ['push', 'origin', TRUNK]);
|
|
54
|
+
// A draft the working copy below has never seen: pushed after it was cloned.
|
|
55
|
+
repo = path.join(root, WORKSPACE, 'knowledge-base');
|
|
56
|
+
await fs.mkdir(path.dirname(repo), { recursive: true });
|
|
57
|
+
await runGit(root, ['clone', upstream, repo]);
|
|
58
|
+
await runGit(seed, ['checkout', '-b', 'alice/feature']);
|
|
59
|
+
await fs.writeFile(path.join(seed, 'new.md'), 'new\n');
|
|
60
|
+
await runGit(seed, ['add', '-A']);
|
|
61
|
+
await runGit(seed, ['commit', '-m', 'feature']);
|
|
62
|
+
await runGit(seed, ['push', 'origin', 'alice/feature']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
vi.restoreAllMocks();
|
|
67
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function service(): GitService {
|
|
71
|
+
const workspaces = {
|
|
72
|
+
getWorkspacePath: async () => path.dirname(repo),
|
|
73
|
+
} as unknown as WorkspaceService;
|
|
74
|
+
return new GitService(workspaces, new WorkflowHooks(), testKbContext());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Everything the service logged at warn level, as one string per line. */
|
|
78
|
+
function warnings(spy: ReturnType<typeof vi.spyOn>): string[] {
|
|
79
|
+
return spy.mock.calls.map((call) => call.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join(' '));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('a change request whose branches cannot be fetched', () => {
|
|
83
|
+
it('says the fetch failed, and where, when the working copy cannot reach the remote', async () => {
|
|
84
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
85
|
+
// The remote this working copy fetches from is gone from under it.
|
|
86
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
87
|
+
|
|
88
|
+
await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).rejects.toThrow(
|
|
89
|
+
'unknown branch: alice/feature',
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const said = warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
93
|
+
expect(said).toHaveLength(1);
|
|
94
|
+
expect(said[0]).toContain(`"${WORKSPACE}"`);
|
|
95
|
+
expect(said[0]).toContain('though it may be on the remote');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('says it once for a list that asks about many requests in one working copy', async () => {
|
|
99
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
100
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
101
|
+
const git = service();
|
|
102
|
+
|
|
103
|
+
for (const branch of ['alice/feature', 'bob/other', 'carol/third']) {
|
|
104
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, branch).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toHaveLength(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('says it again once a minute has passed, and not before', async () => {
|
|
111
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
113
|
+
const git = service();
|
|
114
|
+
const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
115
|
+
// Only the clock the service reads is moved: git itself runs for real.
|
|
116
|
+
const realNow = Date.now.bind(Date);
|
|
117
|
+
let ahead = 0;
|
|
118
|
+
vi.spyOn(Date, 'now').mockImplementation(() => realNow() + ahead);
|
|
119
|
+
|
|
120
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
121
|
+
expect(said()).toHaveLength(1);
|
|
122
|
+
|
|
123
|
+
ahead = 30_000;
|
|
124
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
125
|
+
expect(said()).toHaveLength(1);
|
|
126
|
+
|
|
127
|
+
ahead = 61_000;
|
|
128
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
129
|
+
expect(said()).toHaveLength(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('says it again at once for a working copy that reached the remote in between', async () => {
|
|
133
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
134
|
+
const git = service();
|
|
135
|
+
const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
136
|
+
const broken = path.join(root, 'no-such-remote.git');
|
|
137
|
+
|
|
138
|
+
await runGit(repo, ['remote', 'set-url', 'origin', broken]);
|
|
139
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
140
|
+
expect(said()).toHaveLength(1);
|
|
141
|
+
|
|
142
|
+
// It reaches the remote again: what was said about it is over, and nothing
|
|
143
|
+
// of it is kept.
|
|
144
|
+
await runGit(repo, ['remote', 'set-url', 'origin', upstream]);
|
|
145
|
+
expect(await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
|
|
146
|
+
|
|
147
|
+
// So a failure right after is a new one, said without waiting out a minute.
|
|
148
|
+
await runGit(repo, ['remote', 'set-url', 'origin', broken]);
|
|
149
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'bob/other').catch(() => undefined);
|
|
150
|
+
expect(said()).toHaveLength(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('says nothing when the branch is simply gone from the remote: that is what a retired branch looks like', async () => {
|
|
154
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
155
|
+
|
|
156
|
+
await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'dave/retired')).rejects.toThrow(
|
|
157
|
+
'unknown branch: dave/retired',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('says nothing when the fetch works, and finds the branch the working copy had never seen', async () => {
|
|
164
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
|
|
166
|
+
expect(await service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
|
|
167
|
+
|
|
168
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -2959,7 +2959,53 @@ export class GitService implements IGitService {
|
|
|
2959
2959
|
'fetch', '--no-write-fetch-head', 'origin',
|
|
2960
2960
|
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`,
|
|
2961
2961
|
`+refs/heads/${headBranch}:refs/remotes/origin/${headBranch}`,
|
|
2962
|
-
]).
|
|
2962
|
+
]).then(
|
|
2963
|
+
() => void this.prFetchFailureLoggedAt.delete(cwd),
|
|
2964
|
+
(err: unknown) => this.notePrFetchFailure(cwd, err),
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/**
|
|
2969
|
+
* When each working copy's change-request fetch failure was last logged.
|
|
2970
|
+
* Holds only the working copies that are failing NOW: an entry goes the
|
|
2971
|
+
* moment that copy reaches the remote again, so the table never outgrows the
|
|
2972
|
+
* clones that cannot fetch, however many come and go over a process's life.
|
|
2973
|
+
*/
|
|
2974
|
+
private readonly prFetchFailureLoggedAt = new Map<string, number>();
|
|
2975
|
+
|
|
2976
|
+
/**
|
|
2977
|
+
* Say WHY a change request's branches could not be fetched, when the reason
|
|
2978
|
+
* is not the one that is expected.
|
|
2979
|
+
*
|
|
2980
|
+
* The fetch stays best-effort, so the caller goes on to resolve the refs
|
|
2981
|
+
* from whatever the clone already has. One failure is ordinary and says
|
|
2982
|
+
* nothing: a branch that is gone from the remote, which is what a retired
|
|
2983
|
+
* source branch looks like. Every OTHER failure is the working copy being
|
|
2984
|
+
* unable to reach the remote at all — a credential it does not have, a host
|
|
2985
|
+
* that does not answer — and swallowed, it resurfaced one step later as
|
|
2986
|
+
* `unknown branch` for a branch that was sitting on the remote the whole
|
|
2987
|
+
* time. A deployment logged that for every open change request on every
|
|
2988
|
+
* list, and nothing in the log said the fetch had been refused.
|
|
2989
|
+
*
|
|
2990
|
+
* Logged once a minute per working copy at most: the list asks once per open
|
|
2991
|
+
* request, and the reason is the same for all of them.
|
|
2992
|
+
*/
|
|
2993
|
+
private notePrFetchFailure(cwd: string, err: unknown): void {
|
|
2994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2995
|
+
if (isMissingRemoteBranchFailure(message)) {
|
|
2996
|
+
// The remote answered, and said the branch is not there: this working
|
|
2997
|
+
// copy reaches it, so whatever was logged about it before is over.
|
|
2998
|
+
this.prFetchFailureLoggedAt.delete(cwd);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
const now = Date.now();
|
|
3002
|
+
if (now - (this.prFetchFailureLoggedAt.get(cwd) ?? 0) < 60_000) return;
|
|
3003
|
+
this.prFetchFailureLoggedAt.set(cwd, now);
|
|
3004
|
+
log.warn(
|
|
3005
|
+
`could not fetch change-request branches in the working copy "${path.basename(path.dirname(cwd))}": ` +
|
|
3006
|
+
'a branch it has not seen will be reported as unknown, though it may be on the remote.',
|
|
3007
|
+
{ detail: redactGitToken(message, this.credentials.token()) },
|
|
3008
|
+
);
|
|
2963
3009
|
}
|
|
2964
3010
|
|
|
2965
3011
|
/** Unified-diff patch for one file across the CR range (rename-aware). */
|
|
@@ -195,6 +195,7 @@ export class PendingCommitsService {
|
|
|
195
195
|
.set({
|
|
196
196
|
queuedAt: new Date(),
|
|
197
197
|
authorEmail: email,
|
|
198
|
+
authorEmailBidx: email,
|
|
198
199
|
authorName: input.authorName,
|
|
199
200
|
// Reset transient counters — this is effectively a fresh enqueue.
|
|
200
201
|
attempts: 0,
|
|
@@ -216,6 +217,7 @@ export class PendingCommitsService {
|
|
|
216
217
|
branch: input.branch,
|
|
217
218
|
path: input.path,
|
|
218
219
|
authorEmail: email,
|
|
220
|
+
authorEmailBidx: email,
|
|
219
221
|
authorName: input.authorName,
|
|
220
222
|
});
|
|
221
223
|
}
|
|
@@ -249,11 +251,13 @@ export class PendingCommitsService {
|
|
|
249
251
|
),
|
|
250
252
|
);
|
|
251
253
|
if ((rows[0]?.count ?? 0) > 0) return false;
|
|
254
|
+
const email = canonicalEmail(input.authorEmail);
|
|
252
255
|
await this.db.insert(pendingCommits).values({
|
|
253
256
|
workspaceId,
|
|
254
257
|
branch: input.branch,
|
|
255
258
|
path: input.path,
|
|
256
|
-
authorEmail:
|
|
259
|
+
authorEmail: email,
|
|
260
|
+
authorEmailBidx: email,
|
|
257
261
|
authorName: input.authorName,
|
|
258
262
|
});
|
|
259
263
|
return true;
|
|
@@ -41,8 +41,12 @@ export const RECOVERY_BOT_NAME = 'Bevel Recovery Bot';
|
|
|
41
41
|
export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
|
|
42
42
|
const inserted = await db
|
|
43
43
|
.insert(users)
|
|
44
|
-
.values({
|
|
45
|
-
|
|
44
|
+
.values({
|
|
45
|
+
email: RECOVERY_BOT_EMAIL,
|
|
46
|
+
emailBidx: RECOVERY_BOT_EMAIL,
|
|
47
|
+
name: RECOVERY_BOT_NAME,
|
|
48
|
+
})
|
|
49
|
+
.onConflictDoNothing({ target: users.emailBidx })
|
|
46
50
|
.returning();
|
|
47
51
|
if (inserted.length > 0) {
|
|
48
52
|
const row = inserted[0];
|
|
@@ -57,7 +61,7 @@ export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
|
|
|
57
61
|
const [row] = await db
|
|
58
62
|
.select()
|
|
59
63
|
.from(users)
|
|
60
|
-
.where(eq(users.
|
|
64
|
+
.where(eq(users.emailBidx, RECOVERY_BOT_EMAIL))
|
|
61
65
|
.limit(1);
|
|
62
66
|
if (!row) {
|
|
63
67
|
// Vanishingly unlikely: the row exists for the conflict to fire but is
|
|
@@ -247,6 +247,9 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
|
|
|
247
247
|
prNumber: PR,
|
|
248
248
|
path: 'Sales/Terms.md',
|
|
249
249
|
approverEmail: 'carol@bevel.software',
|
|
250
|
+
// The index column of a copied row is written with the address, not
|
|
251
|
+
// with the stored index read back beside it.
|
|
252
|
+
approverEmailBidx: 'carol@bevel.software',
|
|
250
253
|
approverName: 'Carol',
|
|
251
254
|
headSha: NEW_HEAD,
|
|
252
255
|
// The reviewer approved THEN, not now — the merge is not an approval.
|
|
@@ -256,6 +259,7 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
|
|
|
256
259
|
prNumber: PR,
|
|
257
260
|
path: 'Sales/Pricing.md',
|
|
258
261
|
approverEmail: 'bob@bevel.software',
|
|
262
|
+
approverEmailBidx: 'bob@bevel.software',
|
|
259
263
|
approverName: 'Bob',
|
|
260
264
|
headSha: NEW_HEAD,
|
|
261
265
|
approvedAt: APPROVED_AT,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { SQL } from 'drizzle-orm';
|
|
3
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
2
4
|
import { ReviewWorkflowService } from '../review-workflow.service.js';
|
|
3
5
|
import type { ApprovalTx } from '../review-workflow.interface.js';
|
|
4
6
|
|
|
@@ -34,6 +36,8 @@ function harness() {
|
|
|
34
36
|
const locked: unknown[][] = [];
|
|
35
37
|
const lockSql: string[] = [];
|
|
36
38
|
const sets: unknown[] = [];
|
|
39
|
+
/** The condition each rewrite was limited to, as the SQL and values it sends. */
|
|
40
|
+
const wheres: { sql: string; params: unknown[] }[] = [];
|
|
37
41
|
const tx = {
|
|
38
42
|
execute: async (statement: { queryChunks?: unknown[] }) => {
|
|
39
43
|
const text = statementText(statement);
|
|
@@ -48,8 +52,9 @@ function harness() {
|
|
|
48
52
|
},
|
|
49
53
|
update: (table: unknown) => ({
|
|
50
54
|
set: (values: unknown) => ({
|
|
51
|
-
where: async () => {
|
|
55
|
+
where: async (condition: SQL) => {
|
|
52
56
|
sets.push(values);
|
|
57
|
+
wheres.push(new PgDialect().sqlToQuery(condition));
|
|
53
58
|
order.push(`update:${tableOf(table)}`);
|
|
54
59
|
},
|
|
55
60
|
}),
|
|
@@ -58,7 +63,7 @@ function harness() {
|
|
|
58
63
|
// `eraseApprover` touches nothing but the transaction it is given, so the
|
|
59
64
|
// service's own collaborators are never reached.
|
|
60
65
|
const svc = new ReviewWorkflowService({} as never, {} as never, {} as never, {} as never);
|
|
61
|
-
return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets };
|
|
66
|
+
return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets, wheres };
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
describe('ReviewWorkflowService.eraseApprover', () => {
|
|
@@ -79,6 +84,23 @@ describe('ReviewWorkflowService.eraseApprover', () => {
|
|
|
79
84
|
expect(h.lockSql[0]).toContain('pg_advisory_xact_lock');
|
|
80
85
|
expect(h.lockSql[0]).not.toMatch(/pg_advisory_lock\b/);
|
|
81
86
|
// And the rewrite is to the placeholder identity it was given.
|
|
82
|
-
|
|
87
|
+
// The blind index is rewritten with the email: nothing keyed to the
|
|
88
|
+
// erased address may survive, and the email itself is ciphertext. The
|
|
89
|
+
// index column is written with the address; the handle indexes it.
|
|
90
|
+
expect(h.sets).toEqual([
|
|
91
|
+
{
|
|
92
|
+
approverEmail: 'deleted-1@erased.invalid',
|
|
93
|
+
approverEmailBidx: 'deleted-1@erased.invalid',
|
|
94
|
+
approverName: 'Deleted user',
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
// WHICH rows: the ones whose blind index is the erased address's. The
|
|
98
|
+
// address column holds randomized ciphertext, so a match on it finds
|
|
99
|
+
// nothing — the rewrite would report success having rewritten no row, and
|
|
100
|
+
// every approval of the erased person would keep their name.
|
|
101
|
+
expect(h.wheres).toHaveLength(1);
|
|
102
|
+
expect(h.wheres[0]!.sql).toMatch(/"approver_email_bidx" = \$1$/);
|
|
103
|
+
expect(h.wheres[0]!.sql).not.toMatch(/"approver_email" =/);
|
|
104
|
+
expect(JSON.stringify(h.wheres[0]!.params)).toContain('bob@bevel.software');
|
|
83
105
|
});
|
|
84
106
|
});
|
|
@@ -343,6 +343,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
343
343
|
.values({
|
|
344
344
|
prNumber,
|
|
345
345
|
authorEmail: canonicalEmail(user.email),
|
|
346
|
+
authorEmailBidx: user.email,
|
|
346
347
|
authorName: user.name,
|
|
347
348
|
path: input.path ?? null,
|
|
348
349
|
line: input.line ?? null,
|
|
@@ -600,6 +601,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
600
601
|
prNumber,
|
|
601
602
|
path,
|
|
602
603
|
approverEmail: callerEmail,
|
|
604
|
+
approverEmailBidx: callerEmail,
|
|
603
605
|
approverName: user.name,
|
|
604
606
|
headSha,
|
|
605
607
|
})
|
|
@@ -625,10 +627,13 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
625
627
|
erased: { email: string; name: string },
|
|
626
628
|
): Promise<void> {
|
|
627
629
|
await takeEveryApprovalLock(tx);
|
|
630
|
+
// Matched via the blind index — the email column is randomized
|
|
631
|
+
// ciphertext — and the index is rewritten to the placeholder's too, so no
|
|
632
|
+
// value keyed to the erased address survives.
|
|
628
633
|
await tx
|
|
629
634
|
.update(prFileApprovals)
|
|
630
|
-
.set({ approverEmail: erased.email, approverName: erased.name })
|
|
631
|
-
.where(eq(prFileApprovals.
|
|
635
|
+
.set({ approverEmail: erased.email, approverEmailBidx: erased.email, approverName: erased.name })
|
|
636
|
+
.where(eq(prFileApprovals.approverEmailBidx, email));
|
|
632
637
|
}
|
|
633
638
|
|
|
634
639
|
/**
|
|
@@ -714,6 +719,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
714
719
|
.values({
|
|
715
720
|
prNumber,
|
|
716
721
|
triggeredByEmail,
|
|
722
|
+
triggeredByEmailBidx: triggeredByEmail,
|
|
717
723
|
triggeredByName: user.name,
|
|
718
724
|
headShaAtMerge: headSha,
|
|
719
725
|
mergeMethod: MERGE_METHOD,
|
|
@@ -930,6 +936,9 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
930
936
|
prNumber,
|
|
931
937
|
path: r.path,
|
|
932
938
|
approverEmail: r.approverEmail,
|
|
939
|
+
// The address, not the stored index read back beside it: an index
|
|
940
|
+
// column is written with what it is the index of.
|
|
941
|
+
approverEmailBidx: r.approverEmail,
|
|
933
942
|
approverName: r.approverName,
|
|
934
943
|
headSha: toHeadSha,
|
|
935
944
|
approvedAt: r.approvedAt,
|
|
@@ -1013,7 +1022,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
1013
1022
|
and(
|
|
1014
1023
|
eq(prFileApprovals.prNumber, prNumber),
|
|
1015
1024
|
eq(prFileApprovals.path, path),
|
|
1016
|
-
eq(prFileApprovals.
|
|
1025
|
+
eq(prFileApprovals.approverEmailBidx, callerEmail),
|
|
1017
1026
|
),
|
|
1018
1027
|
);
|
|
1019
1028
|
});
|
|
@@ -1770,7 +1770,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1770
1770
|
.from(changeRequests)
|
|
1771
1771
|
.where(
|
|
1772
1772
|
and(
|
|
1773
|
-
eq(changeRequests.
|
|
1773
|
+
eq(changeRequests.authorEmailBidx, email),
|
|
1774
1774
|
eq(changeRequests.sourceBranch, sourceBranch),
|
|
1775
1775
|
ne(changeRequests.state, 'open'),
|
|
1776
1776
|
),
|
|
@@ -1910,6 +1910,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1910
1910
|
title: input.title.trim(),
|
|
1911
1911
|
body,
|
|
1912
1912
|
authorEmail: canonicalEmail(user.email),
|
|
1913
|
+
authorEmailBidx: user.email,
|
|
1913
1914
|
authorName: user.name,
|
|
1914
1915
|
})
|
|
1915
1916
|
.returning({ number: changeRequests.number });
|
|
@@ -3577,6 +3578,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
3577
3578
|
applyFailureKind: failure.kind,
|
|
3578
3579
|
applyFailedAt: at,
|
|
3579
3580
|
applyFailedByName: user.name,
|
|
3581
|
+
applyFailedByEmailBidx: user.email,
|
|
3580
3582
|
})
|
|
3581
3583
|
// The write itself refuses to go backwards: the attempt map above is an
|
|
3582
3584
|
// early exit within this process, but an older UPDATE still in flight (or
|
|
@@ -3637,6 +3639,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
3637
3639
|
applyFailureConflicts: null,
|
|
3638
3640
|
applyFailedAt: null,
|
|
3639
3641
|
applyFailedByName: null,
|
|
3642
|
+
applyFailedByEmailBidx: null,
|
|
3640
3643
|
applyFailureKind: null,
|
|
3641
3644
|
})
|
|
3642
3645
|
.where(
|