@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
|
@@ -21,6 +21,7 @@ import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
|
21
21
|
import { createMcpRoutes } from '../../mcp/mcp.routes.js';
|
|
22
22
|
import { McpService } from '../../mcp/mcp.service.js';
|
|
23
23
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
24
|
+
import { sharedFileRules, sharedFileRulesSection, sharedRulesPointer } from '../../agent-instructions/shared-file-rules.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Who decodes escape sequences in written content a second time — settled on
|
|
@@ -361,24 +362,33 @@ describe('escape sequences survive a write — call_tool_chain', () => {
|
|
|
361
362
|
});
|
|
362
363
|
|
|
363
364
|
describe('what the write tools tell an agent about escape sequences', () => {
|
|
364
|
-
it('
|
|
365
|
+
it('states the warning once, in the shared rules, and in no description', async () => {
|
|
365
366
|
const { descriptions } = await startPlatform();
|
|
366
367
|
const served = await descriptions();
|
|
368
|
+
// It applies to the three tools that take content as a JSON string, so it is
|
|
369
|
+
// a SHARED rule: stated in the handshake instructions and in the managed
|
|
370
|
+
// agent guide, and repeated in no tool description — a description that
|
|
371
|
+
// carried it was long enough for a client to cut the end off.
|
|
372
|
+
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'escape-sequences')!;
|
|
373
|
+
expect(rule.body).toContain('some clients decode escape sequences in arguments before sending');
|
|
374
|
+
// The way out is the upload route, which the rule after it spells out by
|
|
375
|
+
// the names of the tools this deployment serves.
|
|
376
|
+
expect(rule.body).toContain('by upload (see below), which lands it unchanged');
|
|
377
|
+
const upload = sharedFileRules(testKbContext().layout).find((r) => r.id === 'upload-route')!;
|
|
378
|
+
expect(upload.body).toContain('`request_file_upload`');
|
|
379
|
+
expect(upload.body).toContain('`apply_file_upload`');
|
|
380
|
+
// It names the three tools it is about, so an agent reading the section
|
|
381
|
+
// knows where it applies.
|
|
382
|
+
for (const name of ['write_file', 'write_files', 'edit_file']) expect(rule.body, name).toContain(name);
|
|
383
|
+
expect(sharedFileRulesSection(testKbContext().layout)).toContain(rule.body);
|
|
367
384
|
|
|
385
|
+
const carrying = Object.entries(served)
|
|
386
|
+
.filter(([, d]) => d.includes('decode escape sequences in arguments before sending') || d.includes('some clients decode them in arguments'))
|
|
387
|
+
.map(([n]) => n);
|
|
388
|
+
expect(carrying).toEqual([]);
|
|
389
|
+
// Each of the three still points at where the warning is.
|
|
368
390
|
for (const name of ['write_file', 'write_files', 'edit_file']) {
|
|
369
|
-
|
|
370
|
-
expect(d, name).toBeDefined();
|
|
371
|
-
// Some clients decode before sending …
|
|
372
|
-
expect(d, name).toContain('some clients decode them in arguments before sending');
|
|
373
|
-
// … and the route that lands such content unchanged.
|
|
374
|
-
expect(d, name).toContain('request_upload_token');
|
|
375
|
-
expect(d, name).toContain('lands it unchanged');
|
|
391
|
+
expect(served[name], name).toContain(sharedRulesPointer(testKbContext().layout).trim());
|
|
376
392
|
}
|
|
377
|
-
// Said on the three tools that take content as a JSON string, and nowhere else.
|
|
378
|
-
const carrying = Object.entries(served)
|
|
379
|
-
.filter(([, d]) => d.includes('some clients decode them in arguments before sending'))
|
|
380
|
-
.map(([n]) => n)
|
|
381
|
-
.sort();
|
|
382
|
-
expect(carrying).toEqual(['edit_file', 'write_file', 'write_files']);
|
|
383
393
|
});
|
|
384
394
|
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
8
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
9
|
+
import { WorkspaceService } from '../workspace.service.js';
|
|
10
|
+
import { NodeGitRunner } from '../../workflow/git/node-git-runner.js';
|
|
11
|
+
import { gitCredentials } from '../../../shared/git.contract.js';
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The working copy a repo-global operation is handed.
|
|
17
|
+
*
|
|
18
|
+
* Listing change requests, reading one by number, listing branches: none of
|
|
19
|
+
* them is about a particular draft, so they run in whichever clone is already
|
|
20
|
+
* on disk (`findAnyWorkspaceId`). That clone is one this process may never have
|
|
21
|
+
* opened — and a clone gets its credential helper when its branch is opened.
|
|
22
|
+
*
|
|
23
|
+
* A deployment's oldest clone had none: it predated the helper being persisted.
|
|
24
|
+
* It sorted first on disk, so every repo-global fetch ran in it, failed with
|
|
25
|
+
* "could not read Username", was swallowed, and came back as `unknown branch`
|
|
26
|
+
* for each open change request — thirty-four of them, on every list, while all
|
|
27
|
+
* thirty-four branches were on the remote. Opening that one branch in the app
|
|
28
|
+
* stamped the helper and the errors stopped.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const BRANCH = 'main';
|
|
32
|
+
const OLD_DRAFT = 'someone/an-old-draft';
|
|
33
|
+
const KB_DIR = 'knowledge-base';
|
|
34
|
+
|
|
35
|
+
async function git(cwd: string, args: string[]): Promise<string> {
|
|
36
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
37
|
+
cwd,
|
|
38
|
+
env: {
|
|
39
|
+
...process.env,
|
|
40
|
+
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
|
|
41
|
+
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return stdout.toString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The credential helpers a clone's own config carries; none is the empty list. */
|
|
48
|
+
async function helpersOf(repoDir: string): Promise<string[]> {
|
|
49
|
+
return git(repoDir, ['config', '--local', '--get-all', 'credential.helper']).then(
|
|
50
|
+
(out) => out.split('\n').filter((line) => line.trim() !== ''),
|
|
51
|
+
() => [],
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let root: string;
|
|
56
|
+
let workspacesRoot: string;
|
|
57
|
+
let upstream: string;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-any-creds-'));
|
|
61
|
+
workspacesRoot = path.join(root, 'workspaces');
|
|
62
|
+
await fs.mkdir(workspacesRoot, { recursive: true });
|
|
63
|
+
upstream = path.join(root, 'upstream.git');
|
|
64
|
+
await git(root, ['init', '--bare', '-b', BRANCH, upstream]);
|
|
65
|
+
const seed = path.join(root, '.seed');
|
|
66
|
+
await fs.mkdir(seed);
|
|
67
|
+
await git(seed, ['init', '-b', BRANCH]);
|
|
68
|
+
await fs.writeFile(path.join(seed, 'README.md'), 'seed\n', 'utf8');
|
|
69
|
+
await git(seed, ['add', '-A']);
|
|
70
|
+
await git(seed, ['commit', '-m', 'init']);
|
|
71
|
+
await git(seed, ['remote', 'add', 'origin', upstream]);
|
|
72
|
+
await git(seed, ['push', 'origin', BRANCH]);
|
|
73
|
+
await git(seed, ['push', 'origin', `${BRANCH}:refs/heads/${OLD_DRAFT}`]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** A clone as an older build left it: on disk, complete, and with no helper of ours. */
|
|
81
|
+
async function anOldCloneWithNoHelper(branch: string): Promise<string> {
|
|
82
|
+
const repoDir = path.join(workspacesRoot, encodeURIComponent(branch), KB_DIR);
|
|
83
|
+
await fs.mkdir(path.dirname(repoDir), { recursive: true });
|
|
84
|
+
await git(root, ['clone', '-b', branch, upstream, repoDir]);
|
|
85
|
+
expect(await helpersOf(repoDir)).toEqual([]);
|
|
86
|
+
return repoDir;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function service(token: string | null) {
|
|
90
|
+
return new WorkspaceService(
|
|
91
|
+
workspacesRoot,
|
|
92
|
+
() => upstream,
|
|
93
|
+
testKbContext({ kbDirName: KB_DIR, branchModel: { defaultBranch: BRANCH, protectedBranches: [BRANCH] } }),
|
|
94
|
+
new NodeFs(),
|
|
95
|
+
new NodeGitRunner(undefined, gitCredentials('x-access-token', token)),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('the working copy a repo-global operation runs in', () => {
|
|
100
|
+
it('is given the deployment’s credential helper before it is handed out', async () => {
|
|
101
|
+
const repoDir = await anOldCloneWithNoHelper(OLD_DRAFT);
|
|
102
|
+
|
|
103
|
+
const id = await service('a-token').findAnyWorkspaceId();
|
|
104
|
+
|
|
105
|
+
expect(id).toBe(encodeURIComponent(OLD_DRAFT));
|
|
106
|
+
// One helper, ours: the one that reads the token from the environment the
|
|
107
|
+
// runner hands each git call. The token itself is never written down.
|
|
108
|
+
const helpers = await helpersOf(repoDir);
|
|
109
|
+
expect(helpers).toHaveLength(1);
|
|
110
|
+
expect(helpers[0]).toContain('GITHUB_TOKEN');
|
|
111
|
+
expect(helpers[0]).not.toContain('a-token');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('is the same clone it always picked: the first on disk', async () => {
|
|
115
|
+
await anOldCloneWithNoHelper(OLD_DRAFT);
|
|
116
|
+
await anOldCloneWithNoHelper(BRANCH);
|
|
117
|
+
const svc = service('a-token');
|
|
118
|
+
const entries = (await fs.readdir(workspacesRoot)).filter((name) => !name.startsWith('.'));
|
|
119
|
+
expect(await svc.findAnyWorkspaceId()).toBe(entries[0]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('keeps a helper an operator wrote themselves, beside ours', async () => {
|
|
123
|
+
const repoDir = await anOldCloneWithNoHelper(OLD_DRAFT);
|
|
124
|
+
await git(repoDir, ['config', '--add', 'credential.helper', 'store']);
|
|
125
|
+
|
|
126
|
+
await service('a-token').findAnyWorkspaceId();
|
|
127
|
+
|
|
128
|
+
const helpers = await helpersOf(repoDir);
|
|
129
|
+
expect(helpers).toContain('store');
|
|
130
|
+
expect(helpers.some((h) => h.includes('GITHUB_TOKEN'))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('writes the helper once, not on every call', async () => {
|
|
134
|
+
const repoDir = await anOldCloneWithNoHelper(OLD_DRAFT);
|
|
135
|
+
const svc = service('a-token');
|
|
136
|
+
await svc.findAnyWorkspaceId();
|
|
137
|
+
const config = path.join(repoDir, '.git', 'config');
|
|
138
|
+
const stampedAt = (await fs.stat(config)).mtimeMs;
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
140
|
+
|
|
141
|
+
await svc.findAnyWorkspaceId();
|
|
142
|
+
await svc.findAnyWorkspaceId();
|
|
143
|
+
|
|
144
|
+
expect((await fs.stat(config)).mtimeMs).toBe(stampedAt);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('stamps nothing on a deployment that has no token', async () => {
|
|
148
|
+
const repoDir = await anOldCloneWithNoHelper(OLD_DRAFT);
|
|
149
|
+
expect(await service(null).findAnyWorkspaceId()).toBe(encodeURIComponent(OLD_DRAFT));
|
|
150
|
+
expect(await helpersOf(repoDir)).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('still answers null when nothing is cloned', async () => {
|
|
154
|
+
expect(await service('a-token').findAnyWorkspaceId()).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -75,18 +75,58 @@ function service(
|
|
|
75
75
|
kbRepoUrl: () => string,
|
|
76
76
|
setAsideRoot?: string,
|
|
77
77
|
aroundSetAside?: (workspaceId: string, move: () => Promise<void>) => Promise<void>,
|
|
78
|
+
gitRunner: NodeGitRunner = new NodeGitRunner(),
|
|
78
79
|
) {
|
|
79
80
|
return new WorkspaceService(
|
|
80
81
|
workspacesRoot,
|
|
81
82
|
kbRepoUrl,
|
|
82
83
|
testKbContext({ branchModel: { defaultBranch: BRANCH, protectedBranches: [BRANCH] } }),
|
|
83
84
|
new NodeFs(),
|
|
84
|
-
|
|
85
|
+
gitRunner,
|
|
85
86
|
setAsideRoot,
|
|
86
87
|
aroundSetAside,
|
|
87
88
|
);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A git runner that puts two callers' reads of a working copy's address in ONE
|
|
93
|
+
* order, the one that matters: both read the copy that is there, and the
|
|
94
|
+
* second is handed its answer only once `afterTheFirstMove` has come.
|
|
95
|
+
*
|
|
96
|
+
* Two holds, because holding the second answer back is only half of it. The
|
|
97
|
+
* FIRST answer is held until the second read has happened, so the first caller
|
|
98
|
+
* cannot move the copy before the second has looked at it — otherwise the
|
|
99
|
+
* second would read whatever replaced it, and the stale answer this is about
|
|
100
|
+
* would never exist. Then the second answer is held until the first move has
|
|
101
|
+
* ended. Neither can wait on the other forever: the first caller is held in its
|
|
102
|
+
* read, so no move is in flight for the second to wait on, and it always reads.
|
|
103
|
+
* Everything else runs as it is.
|
|
104
|
+
*/
|
|
105
|
+
function withTheSecondAddressReadHeldPastTheMove(afterTheFirstMove: Promise<void>): NodeGitRunner {
|
|
106
|
+
let reads = 0;
|
|
107
|
+
let secondHasRead!: () => void;
|
|
108
|
+
const onceTheSecondHasRead = new Promise<void>((resolve) => {
|
|
109
|
+
secondHasRead = resolve;
|
|
110
|
+
});
|
|
111
|
+
return new Proxy(new NodeGitRunner(), {
|
|
112
|
+
get(target, prop, receiver) {
|
|
113
|
+
if (prop !== 'run') return Reflect.get(target, prop, receiver);
|
|
114
|
+
return async (cwd: string, args: string[], opts?: never) => {
|
|
115
|
+
const answer = await target.run(cwd, args, opts);
|
|
116
|
+
const readsTheAddress = args[0] === 'config' && args.includes('--get') && args.includes('remote.origin.url');
|
|
117
|
+
if (!readsTheAddress) return answer;
|
|
118
|
+
const mine = ++reads;
|
|
119
|
+
if (mine === 1) await onceTheSecondHasRead;
|
|
120
|
+
if (mine === 2) {
|
|
121
|
+
secondHasRead();
|
|
122
|
+
await afterTheFirstMove;
|
|
123
|
+
}
|
|
124
|
+
return answer;
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
const cloneDir = () => path.join(workspacesRoot, encodeURIComponent(BRANCH), 'knowledge-base');
|
|
91
131
|
|
|
92
132
|
/** Every working copy under a set-aside root, as repo directories. */
|
|
@@ -307,6 +347,42 @@ describe('what surrounds the setting aside of one working copy', () => {
|
|
|
307
347
|
expect(await setAside()).toHaveLength(1);
|
|
308
348
|
expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
|
|
309
349
|
});
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The same two callers, in the order the test above only sometimes got. The
|
|
353
|
+
* second reads the old copy's address, and is handed the answer after the
|
|
354
|
+
* first has finished moving that copy — when nothing says a move is in
|
|
355
|
+
* flight any more, and what sits at the path is the fresh clone the first is
|
|
356
|
+
* making. It moved that too: a second set-aside, of a working copy of the
|
|
357
|
+
* RIGHT repository, taken from under the clone that was writing it.
|
|
358
|
+
*/
|
|
359
|
+
it('moves it once when the second caller comes back with the old address after the move has ended', async () => {
|
|
360
|
+
const { replacement } = await onTheOldRepository();
|
|
361
|
+
let moves = 0;
|
|
362
|
+
let firstMoveEnded!: () => void;
|
|
363
|
+
const afterTheFirstMove = new Promise<void>((resolve) => {
|
|
364
|
+
firstMoveEnded = resolve;
|
|
365
|
+
});
|
|
366
|
+
const svc = service(
|
|
367
|
+
() => replacement,
|
|
368
|
+
undefined,
|
|
369
|
+
async (_workspaceId, move) => {
|
|
370
|
+
moves += 1;
|
|
371
|
+
await move();
|
|
372
|
+
firstMoveEnded();
|
|
373
|
+
},
|
|
374
|
+
withTheSecondAddressReadHeldPastTheMove(afterTheFirstMove),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const [first, second] = await Promise.all([svc.getOrCreateForBranch(BRANCH), svc.getOrCreateForBranch(BRANCH)]);
|
|
378
|
+
|
|
379
|
+
expect(moves).toBe(1);
|
|
380
|
+
expect(first.repoDir).toBe(second.repoDir);
|
|
381
|
+
expect(await setAside()).toHaveLength(1);
|
|
382
|
+
expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
|
|
383
|
+
// And the clone both were served is whole: its address is the new one.
|
|
384
|
+
expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(replacement);
|
|
385
|
+
});
|
|
310
386
|
});
|
|
311
387
|
|
|
312
388
|
describe('forgetClone', () => {
|
|
@@ -1181,6 +1181,63 @@ describe('WorkspaceService.unzipFile — per-entry write guard', () => {
|
|
|
1181
1181
|
await expect(fs.stat(path.join(repoDir, 'out', 'blocked-dir'))).rejects.toThrow();
|
|
1182
1182
|
});
|
|
1183
1183
|
|
|
1184
|
+
/**
|
|
1185
|
+
* An archive reader caps the inflation at the size the header DECLARES, but
|
|
1186
|
+
* only when that size is above zero: an entry declaring zero is inflated with
|
|
1187
|
+
* no cap. So "check the declared size, then read" let a few kilobytes expand
|
|
1188
|
+
* to whatever memory there was. Both headers are patched to say "empty" here,
|
|
1189
|
+
* over four megabytes that deflate to four kilobytes.
|
|
1190
|
+
*/
|
|
1191
|
+
it('refuses an entry whose header says it is empty and whose stream is not, and extracts the rest', async () => {
|
|
1192
|
+
const { default: AdmZip } = await import('adm-zip');
|
|
1193
|
+
const zip = new AdmZip();
|
|
1194
|
+
zip.addFile('bomb.md', Buffer.alloc(4 * 1024 * 1024, 0x61));
|
|
1195
|
+
const bomb = zip.toBuffer();
|
|
1196
|
+
// The uncompressed-size field: 22 bytes into the local header, 24 into the
|
|
1197
|
+
// central-directory one.
|
|
1198
|
+
bomb.writeUInt32LE(0, bomb.indexOf(Buffer.from([0x50, 0x4b, 0x03, 0x04])) + 22);
|
|
1199
|
+
bomb.writeUInt32LE(0, bomb.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])) + 24);
|
|
1200
|
+
await fs.writeFile(path.join(repoDir, 'bomb.zip'), bomb);
|
|
1201
|
+
|
|
1202
|
+
const res = await svc.unzipFile(workspaceId, 'bomb.zip', 'out');
|
|
1203
|
+
|
|
1204
|
+
expect(res.extracted).toEqual([]);
|
|
1205
|
+
expect(res.skipped).toHaveLength(1);
|
|
1206
|
+
expect(res.skipped[0]).toMatchObject({ path: 'bomb.md' });
|
|
1207
|
+
expect(res.skipped[0].reason).toContain('declares an empty file');
|
|
1208
|
+
await expect(fs.stat(path.join(repoDir, 'out', 'bomb.md'))).rejects.toThrow();
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it('extracts the other entries of an archive in which one fails its checksum', async () => {
|
|
1212
|
+
const { default: AdmZip } = await import('adm-zip');
|
|
1213
|
+
const zip = new AdmZip();
|
|
1214
|
+
zip.addFile('bad.md', Buffer.from('this entry will not match its checksum'));
|
|
1215
|
+
zip.addFile('fine.md', Buffer.from('fine'));
|
|
1216
|
+
const archive = zip.toBuffer();
|
|
1217
|
+
// The CRC field of the FIRST entry, in both of its headers: 14 bytes into
|
|
1218
|
+
// the local one, 16 into the central-directory one. A reader checks one or
|
|
1219
|
+
// the other depending on the entry's flags.
|
|
1220
|
+
const firstLocal = archive.indexOf(Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
|
1221
|
+
const firstCentral = archive.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
|
|
1222
|
+
archive.writeUInt32LE((archive.readUInt32LE(firstLocal + 14) ^ 0xffffffff) >>> 0, firstLocal + 14);
|
|
1223
|
+
archive.writeUInt32LE((archive.readUInt32LE(firstCentral + 16) ^ 0xffffffff) >>> 0, firstCentral + 16);
|
|
1224
|
+
await fs.writeFile(path.join(repoDir, 'crc.zip'), archive);
|
|
1225
|
+
|
|
1226
|
+
const res = await svc.unzipFile(workspaceId, 'crc.zip', 'out');
|
|
1227
|
+
|
|
1228
|
+
expect(res.extracted).toEqual(['knowledge-base/out/fine.md']);
|
|
1229
|
+
expect(res.skipped).toHaveLength(1);
|
|
1230
|
+
expect(res.skipped[0].path).toBe('bad.md');
|
|
1231
|
+
expect(res.skipped[0].reason).toContain('could not be read');
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('still extracts a file that really is empty', async () => {
|
|
1235
|
+
await writeZip('empty.zip', { 'empty.md': '', 'full.md': 'x' });
|
|
1236
|
+
const res = await svc.unzipFile(workspaceId, 'empty.zip', 'out');
|
|
1237
|
+
expect(res.extracted.sort()).toEqual(['knowledge-base/out/empty.md', 'knowledge-base/out/full.md']);
|
|
1238
|
+
expect((await fs.readFile(path.join(repoDir, 'out', 'empty.md'))).byteLength).toBe(0);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1184
1241
|
it('does not create the destination directory when every entry is blocked', async () => {
|
|
1185
1242
|
await writeZip('d.zip', { 'a.md': '1', 'b.md': '2' });
|
|
1186
1243
|
const res = await svc.unzipFile(workspaceId, 'd.zip', 'out', async () => {
|
|
@@ -14,16 +14,19 @@ import { SpillStore } from '../spill-store.js';
|
|
|
14
14
|
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
15
15
|
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
16
16
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
17
|
+
import { sharedFileRulesSection, sharedRulesPointer } from '../../agent-instructions/shared-file-rules.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* What the platform TELLS AN AGENT about the conventions file, under the
|
|
20
21
|
* default name and under a deployment's own.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The conventions reminder and the platform-file list are SHARED RULES now —
|
|
24
|
+
* stated in the handshake instructions and in the managed agent guide, once
|
|
25
|
+
* each — so their wording is asserted against that one text. What each tool
|
|
26
|
+
* description still has to get right is the POINTER at the end, which names the
|
|
27
|
+
* guide by the name this deployment gave it; that is the only place a remote
|
|
28
|
+
* agent learns which file to open, and it has no checkout, so no harness reads
|
|
29
|
+
* that file for it.
|
|
27
30
|
*/
|
|
28
31
|
const KB_DIR = 'knowledge-base';
|
|
29
32
|
/** The context the tools read; a case applies a deployment's own names to it. */
|
|
@@ -61,21 +64,25 @@ async function listed(registry: ToolRegistry): Promise<Map<string, string>> {
|
|
|
61
64
|
|
|
62
65
|
afterEach(() => kb.applyLayout({ ...DEFAULT_KB_LAYOUT }));
|
|
63
66
|
|
|
64
|
-
describe(
|
|
65
|
-
it('names AGENTS.md, and CLAUDE.md beside it, under the default name',
|
|
67
|
+
describe("what names the guide, under the default name and under a deployment's own", () => {
|
|
68
|
+
it('names AGENTS.md, and CLAUDE.md beside it, under the default name', () => {
|
|
69
|
+
const rules = sharedFileRulesSection(kb.layout);
|
|
70
|
+
expect(rules).toContain(
|
|
71
|
+
'read `AGENTS.md` at the KB root — or `CLAUDE.md` on a knowledge base seeded before it was renamed',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("points every entrypoint at the section, by the guide's own name", async () => {
|
|
66
76
|
const byName = await descriptions();
|
|
67
77
|
// On EVERY entrypoint, reads included: any of them can be a session's first.
|
|
68
78
|
for (const name of ['grep', 'list_files', 'file_stat', 'write_file', 'move_file']) {
|
|
69
|
-
expect(byName.get(name), name).toContain(
|
|
70
|
-
'read `AGENTS.md` at the KB root — or `CLAUDE.md` on a knowledge base seeded before it was renamed',
|
|
71
|
-
);
|
|
79
|
+
expect(byName.get(name), name).toContain('see "Working with files" in AGENTS.md.');
|
|
72
80
|
}
|
|
73
81
|
});
|
|
74
82
|
|
|
75
|
-
it(
|
|
83
|
+
it("names the configured file first and the organisation's own AGENTS.md second", () => {
|
|
76
84
|
kb.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
|
|
77
|
-
const
|
|
78
|
-
const note = byName.get('grep') ?? '';
|
|
85
|
+
const note = sharedFileRulesSection(kb.layout);
|
|
79
86
|
expect(note).toContain('read `HEXIS.md` at the KB root, then `AGENTS.md` if it also exists');
|
|
80
87
|
expect(note).toContain("the organisation's own conventions");
|
|
81
88
|
// Ours first: an agent that reads only one must read the platform's.
|
|
@@ -84,27 +91,23 @@ describe('the conventions note every workspace tool carries', () => {
|
|
|
84
91
|
expect(note).toContain('`CLAUDE.md`');
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
it('
|
|
94
|
+
it('lists the platform files under the configured name, and no longer under AGENTS.md', () => {
|
|
88
95
|
kb.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expect(text.replace(/`AGENTS\.md` if it also exists/g, ''), name).not.toContain(
|
|
99
|
-
'or `AGENTS.md` at the repository root',
|
|
100
|
-
);
|
|
101
|
-
}
|
|
96
|
+
const rules = sharedFileRulesSection(kb.layout);
|
|
97
|
+
// Listed with the depth each one counts at — `access.md` and
|
|
98
|
+
// `.bevelignore` govern the folder they sit in, the other two are read from
|
|
99
|
+
// the root — because the names alone would have an agent refuse a nested
|
|
100
|
+
// `HEXIS.md` it may rename.
|
|
101
|
+
expect(rules).toContain('`access.md` or `.bevelignore` in any folder, `roles.yaml` or `HEXIS.md` at the repository root');
|
|
102
|
+
// The customer's file is content on such a deployment, so the rule that
|
|
103
|
+
// refuses a move must not claim it.
|
|
104
|
+
expect(rules.replace(/`AGENTS\.md` if it also exists/g, '')).not.toContain('`AGENTS.md`');
|
|
102
105
|
});
|
|
103
106
|
|
|
104
|
-
it('keeps naming AGENTS.md as a platform file under the default name',
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
it('keeps naming AGENTS.md as a platform file under the default name', () => {
|
|
108
|
+
expect(sharedFileRulesSection(kb.layout)).toContain(
|
|
109
|
+
'`access.md` or `.bevelignore` in any folder, `roles.yaml` or `AGENTS.md` at the repository root',
|
|
110
|
+
);
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
/**
|
|
@@ -117,14 +120,14 @@ describe('the conventions note every workspace tool carries', () => {
|
|
|
117
120
|
it('follows a layout applied after the tools were mounted', async () => {
|
|
118
121
|
const registry = new ToolRegistry();
|
|
119
122
|
const atMount = await descriptions(registry);
|
|
120
|
-
expect(atMount.get('grep')).toContain('
|
|
123
|
+
expect(atMount.get('grep')).toContain('in AGENTS.md.');
|
|
121
124
|
|
|
122
125
|
kb.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
|
|
123
126
|
|
|
124
127
|
const now = await listed(registry);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
for (const name of ['grep', 'file_stat', 'delete_file', 'move_file']) {
|
|
129
|
+
expect(now.get(name), name).toContain(sharedRulesPointer(kb.layout));
|
|
130
|
+
expect(now.get(name), name).not.toContain('in AGENTS.md.');
|
|
131
|
+
}
|
|
129
132
|
});
|
|
130
133
|
});
|