group-chat-mcp 0.1.5 → 0.2.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/CHANGELOG.md +28 -0
- package/README.md +10 -0
- package/STEPS.md +39 -0
- package/dist/__tests__/install-metadata-service.test.d.ts +2 -0
- package/dist/__tests__/install-metadata-service.test.d.ts.map +1 -0
- package/dist/__tests__/install-metadata-service.test.js +80 -0
- package/dist/__tests__/install-metadata-service.test.js.map +1 -0
- package/dist/__tests__/send-lock.test.d.ts +2 -0
- package/dist/__tests__/send-lock.test.d.ts.map +1 -0
- package/dist/__tests__/send-lock.test.js +239 -0
- package/dist/__tests__/send-lock.test.js.map +1 -0
- package/dist/__tests__/tool-handlers.test.d.ts +2 -0
- package/dist/__tests__/tool-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tool-handlers.test.js +102 -0
- package/dist/__tests__/tool-handlers.test.js.map +1 -0
- package/dist/__tests__/update-notice.test.d.ts +2 -0
- package/dist/__tests__/update-notice.test.d.ts.map +1 -0
- package/dist/__tests__/update-notice.test.js +122 -0
- package/dist/__tests__/update-notice.test.js.map +1 -0
- package/dist/__tests__/update-service.test.d.ts +2 -0
- package/dist/__tests__/update-service.test.d.ts.map +1 -0
- package/dist/__tests__/update-service.test.js +93 -0
- package/dist/__tests__/update-service.test.js.map +1 -0
- package/dist/__tests__/version-check-service.test.d.ts +2 -0
- package/dist/__tests__/version-check-service.test.d.ts.map +1 -0
- package/dist/__tests__/version-check-service.test.js +103 -0
- package/dist/__tests__/version-check-service.test.js.map +1 -0
- package/dist/constants/storage.d.ts +4 -0
- package/dist/constants/storage.d.ts.map +1 -1
- package/dist/constants/storage.js +4 -0
- package/dist/constants/storage.js.map +1 -1
- package/dist/gchat.d.ts +1 -0
- package/dist/gchat.d.ts.map +1 -1
- package/dist/gchat.js +34 -2
- package/dist/gchat.js.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/services/install-metadata-service.d.ts +15 -0
- package/dist/services/install-metadata-service.d.ts.map +1 -0
- package/dist/services/install-metadata-service.js +59 -0
- package/dist/services/install-metadata-service.js.map +1 -0
- package/dist/services/installer-service.d.ts +3 -0
- package/dist/services/installer-service.d.ts.map +1 -1
- package/dist/services/installer-service.js +55 -46
- package/dist/services/installer-service.js.map +1 -1
- package/dist/services/tool-handlers.d.ts.map +1 -1
- package/dist/services/tool-handlers.js +60 -7
- package/dist/services/tool-handlers.js.map +1 -1
- package/dist/services/update-service.d.ts +13 -0
- package/dist/services/update-service.d.ts.map +1 -0
- package/dist/services/update-service.js +76 -0
- package/dist/services/update-service.js.map +1 -0
- package/dist/services/version-check-service.d.ts +12 -0
- package/dist/services/version-check-service.d.ts.map +1 -0
- package/dist/services/version-check-service.js +109 -0
- package/dist/services/version-check-service.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/parsed-command.d.ts +4 -0
- package/dist/types/parsed-command.d.ts.map +1 -1
- package/dist/types/version-check-result.d.ts +6 -0
- package/dist/types/version-check-result.d.ts.map +1 -0
- package/dist/types/version-check-result.js +2 -0
- package/dist/types/version-check-result.js.map +1 -0
- package/dist/utils/file-lock.d.ts +2 -0
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +2 -2
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/send-lock.d.ts +24 -0
- package/dist/utils/send-lock.d.ts.map +1 -0
- package/dist/utils/send-lock.js +139 -0
- package/dist/utils/send-lock.js.map +1 -0
- package/dist/utils/update-utils.d.ts +2 -0
- package/dist/utils/update-utils.d.ts.map +1 -0
- package/dist/utils/update-utils.js +4 -0
- package/dist/utils/update-utils.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `gchat update` command: checks npm registry for the latest version, runs `npm install -g group-chat-mcp@latest`, and re-execs the new binary to refresh all previously installed IDE configurations
|
|
13
|
+
- Passive update notice on interactive commands (`install`, `uninstall`) when a newer npm version exists
|
|
14
|
+
- `InstallMetadataService` persists IDE+scope combos during install/uninstall so `gchat update` knows what to refresh
|
|
15
|
+
- `VersionCheckService` with 24h file-cached npm registry queries and semver comparison
|
|
16
|
+
- `formatUpdateNotice` utility for the update available message
|
|
17
|
+
- `update` and `update-post-install` command variants in `ParsedCommand`
|
|
18
|
+
- Unit and integration tests for all new services and update notice behavior
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `InstallerService` accepts `InstallMetadataService` via constructor parameter (dependency injection)
|
|
23
|
+
- `main()` exported from `gchat.ts` for testability
|
|
24
|
+
- Help text updated with `update` command
|
|
25
|
+
|
|
26
|
+
## [0.1.6] - 2026-03-23
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Per-conversation send lock that serializes concurrent `send_message` calls within a conversation, returning competing messages with reconsideration instructions to blocked agents
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Send lock robustness: stale lock detection with process liveness checks, lock release on agent disconnect, and jittered poll intervals to prevent filesystem contention
|
|
35
|
+
|
|
8
36
|
## [0.1.5] - 2026-03-22
|
|
9
37
|
|
|
10
38
|
### Added
|
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Multi-agent communication server using the Model Context Protocol. Enables AI ag
|
|
|
15
15
|
- Pull-based inbox for clients without push notification support (`read_notifications`)
|
|
16
16
|
- File-based shared state with atomic locking
|
|
17
17
|
- Zero-config setup via `gchat install`
|
|
18
|
+
- Built-in update command (`gchat update`) with automatic IDE config refresh
|
|
19
|
+
- Passive update notices on interactive commands when a newer version is available
|
|
18
20
|
|
|
19
21
|
## Installation
|
|
20
22
|
|
|
@@ -48,6 +50,14 @@ The installer prompts for:
|
|
|
48
50
|
|
|
49
51
|
For Claude Code, the installer registers the MCP server via `claude mcp add` (requires the Claude Code CLI on PATH). For Cursor, it writes `mcp.json` with the server entry and `hooks.json` with session lifecycle hooks that register and unregister agents per chat session.
|
|
50
52
|
|
|
53
|
+
To update to the latest version:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gchat update
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This checks the npm registry, installs the latest version, and refreshes all previously configured IDE setups. Interactive commands (`install`, `uninstall`) also display an update notice when a newer version is available.
|
|
60
|
+
|
|
51
61
|
To remove the configuration:
|
|
52
62
|
|
|
53
63
|
```bash
|
package/STEPS.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# End Goal
|
|
2
|
+
|
|
3
|
+
Add manual and automatic update capabilities to the `gchat` CLI, allowing users to update the globally installed `group-chat-mcp` npm package on demand and be notified (or auto-updated) when a newer version is available.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
- [ ] Persist install metadata during `gchat install` so `gchat update` knows which IDE+scope combos to refresh
|
|
8
|
+
- [ ] Add a version check mechanism that queries the npm registry for the latest published version, compares it to the locally installed version, and caches the result for 24 hours
|
|
9
|
+
- [ ] Integrate the version check into interactive CLI commands (install, uninstall, update) to display an update notice when a newer version is available
|
|
10
|
+
- [ ] Add a `gchat update` command that updates the globally installed package, then re-execs the new binary to refresh configs for all persisted IDE+scope combos
|
|
11
|
+
|
|
12
|
+
## Questions Answered
|
|
13
|
+
|
|
14
|
+
1. Q: What should "automatic update" mean for gchat?
|
|
15
|
+
A: Notify only. On CLI invocation, check the npm registry in the background. If a newer version exists, print an update notice. No mutation without explicit user action via `gchat update`.
|
|
16
|
+
|
|
17
|
+
2. Q: Should the update notice appear on all commands or only interactive ones?
|
|
18
|
+
A: Interactive commands only (install, uninstall, update). Hook commands (cursor-join, cursor-leave) output JSON and must stay clean.
|
|
19
|
+
|
|
20
|
+
3. Q: Should the version check result be cached?
|
|
21
|
+
A: Yes. Cache for 24 hours in a local file. Skip the registry call if checked within that window.
|
|
22
|
+
|
|
23
|
+
4. Q: Should `gchat update` support targeting a specific version?
|
|
24
|
+
A: No. Always install the latest published version.
|
|
25
|
+
|
|
26
|
+
5. Q: Should `gchat update` re-run the installer after updating?
|
|
27
|
+
A: No full reinstall. After updating the npm package, detect which IDE configurations exist and apply incremental config patches (new files, updated files) without requiring the user to re-run `gchat install`.
|
|
28
|
+
|
|
29
|
+
6. Q: How should config patching work?
|
|
30
|
+
A: Re-apply the current version's installer logic for all managed IDE+scope combos. The installer's merge logic is already idempotent — it updates existing entries without clobbering unrelated config.
|
|
31
|
+
|
|
32
|
+
7. Q: How should `gchat update` know which IDE+scope combos to refresh?
|
|
33
|
+
A: Persist install choices in a metadata file (e.g. `~/.group-chat-mcp/install-meta.json`) during `gchat install`. `gchat update` reads this file to determine what to refresh.
|
|
34
|
+
|
|
35
|
+
8. Q: Should the version check run in parallel with the main command or sequentially before it?
|
|
36
|
+
A: Parallel. Fire the registry check at command start, run the main command, print the update notice (if any) after the command completes. No added latency.
|
|
37
|
+
|
|
38
|
+
9. Q: After npm install replaces the binary, should the running (old) process refresh configs or re-exec the new binary?
|
|
39
|
+
A: Re-exec the new binary. After `npm install -g` completes, spawn `gchat update --post-install` using the newly installed binary. The new process runs the new version's installer logic, guaranteeing configs match the new version's expectations.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-metadata-service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/install-metadata-service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { InstallMetadataService } from '../services/install-metadata-service.js';
|
|
6
|
+
import { IDE } from '../enums/ide.js';
|
|
7
|
+
import { Scope } from '../enums/scope.js';
|
|
8
|
+
let tmpDir;
|
|
9
|
+
let service;
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-meta-test-'));
|
|
12
|
+
service = new InstallMetadataService(tmpDir);
|
|
13
|
+
});
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
describe('InstallMetadataService', () => {
|
|
18
|
+
describe('addInstall', () => {
|
|
19
|
+
it('Given no metadata file exists When addInstall is called with (ClaudeCode, Global) Then the file is created with one entry', async () => {
|
|
20
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
21
|
+
const raw = await fs.readFile(path.join(tmpDir, 'install-meta.json'), 'utf-8');
|
|
22
|
+
const entries = JSON.parse(raw);
|
|
23
|
+
expect(entries).toEqual([{ ide: 'claudeCode', scope: 'global' }]);
|
|
24
|
+
});
|
|
25
|
+
it('Given a metadata file with (ClaudeCode, Global) When addInstall is called with (Cursor, Local) Then the file contains both entries', async () => {
|
|
26
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
27
|
+
await service.addInstall(IDE.Cursor, Scope.Local);
|
|
28
|
+
const raw = await fs.readFile(path.join(tmpDir, 'install-meta.json'), 'utf-8');
|
|
29
|
+
const entries = JSON.parse(raw);
|
|
30
|
+
expect(entries).toEqual([
|
|
31
|
+
{ ide: 'claudeCode', scope: 'global' },
|
|
32
|
+
{ ide: 'cursor', scope: 'local' },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
it('Given a metadata file with (ClaudeCode, Global) When addInstall is called with (ClaudeCode, Global) again Then the file still contains one entry', async () => {
|
|
36
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
37
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
38
|
+
const raw = await fs.readFile(path.join(tmpDir, 'install-meta.json'), 'utf-8');
|
|
39
|
+
const entries = JSON.parse(raw);
|
|
40
|
+
expect(entries).toEqual([{ ide: 'claudeCode', scope: 'global' }]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('removeInstall', () => {
|
|
44
|
+
it('Given a metadata file with one entry When removeInstall is called for that entry Then the file is deleted', async () => {
|
|
45
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
46
|
+
await service.removeInstall(IDE.ClaudeCode, Scope.Global);
|
|
47
|
+
const exists = await fs.access(path.join(tmpDir, 'install-meta.json')).then(() => true).catch(() => false);
|
|
48
|
+
expect(exists).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('Given a metadata file with two entries When removeInstall is called for one Then the file contains the other entry', async () => {
|
|
51
|
+
await service.addInstall(IDE.ClaudeCode, Scope.Global);
|
|
52
|
+
await service.addInstall(IDE.Cursor, Scope.Local);
|
|
53
|
+
await service.removeInstall(IDE.ClaudeCode, Scope.Global);
|
|
54
|
+
const raw = await fs.readFile(path.join(tmpDir, 'install-meta.json'), 'utf-8');
|
|
55
|
+
const entries = JSON.parse(raw);
|
|
56
|
+
expect(entries).toEqual([{ ide: 'cursor', scope: 'local' }]);
|
|
57
|
+
});
|
|
58
|
+
it('Given no metadata file exists When removeInstall is called Then no error is thrown', async () => {
|
|
59
|
+
await expect(service.removeInstall(IDE.ClaudeCode, Scope.Global)).resolves.toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('getInstalls', () => {
|
|
63
|
+
it('Given no metadata file exists When getInstalls is called Then an empty array is returned', async () => {
|
|
64
|
+
const installs = await service.getInstalls();
|
|
65
|
+
expect(installs).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
it('Given a metadata file with malformed entries When getInstalls is called Then only valid entries are returned', async () => {
|
|
68
|
+
const metadataPath = path.join(tmpDir, 'install-meta.json');
|
|
69
|
+
await fs.writeFile(metadataPath, JSON.stringify([42, null, { ide: 123 }, { ide: 'claudeCode', scope: 'global' }]), 'utf-8');
|
|
70
|
+
const result = await service.getInstalls();
|
|
71
|
+
expect(result).toEqual([{ ide: 'claudeCode', scope: 'global' }]);
|
|
72
|
+
});
|
|
73
|
+
it('Given a corrupt metadata file When getInstalls is called Then an empty array is returned', async () => {
|
|
74
|
+
await fs.writeFile(path.join(tmpDir, 'install-meta.json'), '{not valid json!!!', 'utf-8');
|
|
75
|
+
const installs = await service.getInstalls();
|
|
76
|
+
expect(installs).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=install-metadata-service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-metadata-service.test.js","sourceRoot":"","sources":["../../src/__tests__/install-metadata-service.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-lock.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/send-lock.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { MessageType } from '../enums/message-type.js';
|
|
6
|
+
import { sendLockDir, tryAcquireSendLock, waitForSendLockRelease, releaseSendLock, getMessagesSince, getMessageCount, readSendLockInfo, } from '../utils/send-lock.js';
|
|
7
|
+
import { LOCK_INFO_FILENAME } from '../utils/file-lock.js';
|
|
8
|
+
import { writeJsonFile } from '../utils/file-utils.js';
|
|
9
|
+
import { MESSAGES_DIR } from '../constants/storage.js';
|
|
10
|
+
let tmpDir;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-sendlock-test-'));
|
|
13
|
+
await fs.mkdir(path.join(tmpDir, MESSAGES_DIR), { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
describe('sendLockDir', () => {
|
|
19
|
+
describe('Given agents A and B targeting same and different conversations', () => {
|
|
20
|
+
it('When sendLockDir is called for the same conversation from different callers Then the same path is returned', () => {
|
|
21
|
+
const dirA = sendLockDir(tmpDir, 'conv-1');
|
|
22
|
+
const dirB = sendLockDir(tmpDir, 'conv-1');
|
|
23
|
+
expect(dirA).toBe(dirB);
|
|
24
|
+
expect(dirA).toBe(path.join(tmpDir, MESSAGES_DIR, 'conv-1.send-lock'));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('tryAcquireSendLock', () => {
|
|
29
|
+
describe('Given no contention', () => {
|
|
30
|
+
it('When agent acquires send lock Then lock acquired and released cleanly', async () => {
|
|
31
|
+
const lockDir = sendLockDir(tmpDir, 'conv-1');
|
|
32
|
+
const result = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
33
|
+
expect(result).toEqual({ acquired: true });
|
|
34
|
+
const info = await readSendLockInfo(lockDir);
|
|
35
|
+
expect(info).not.toBeNull();
|
|
36
|
+
expect(info.agentId).toBe('agent-A');
|
|
37
|
+
expect(info.pid).toBe(process.pid);
|
|
38
|
+
await releaseSendLock(lockDir);
|
|
39
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
40
|
+
expect(exists).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('Given agent A holds send lock on conv X', () => {
|
|
44
|
+
it('When agent B tries to acquire on conv X Then B gets acquired: false with A agentId', async () => {
|
|
45
|
+
const lockDir = sendLockDir(tmpDir, 'conv-X');
|
|
46
|
+
await tryAcquireSendLock(lockDir, 'agent-A');
|
|
47
|
+
const result = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
48
|
+
expect(result).toEqual({ acquired: false, holderAgentId: 'agent-A' });
|
|
49
|
+
await releaseSendLock(lockDir);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('Given agent A holds send lock on conv X', () => {
|
|
53
|
+
it('When agent B acquires on conv Y Then B succeeds immediately', async () => {
|
|
54
|
+
const lockDirX = sendLockDir(tmpDir, 'conv-X');
|
|
55
|
+
const lockDirY = sendLockDir(tmpDir, 'conv-Y');
|
|
56
|
+
await tryAcquireSendLock(lockDirX, 'agent-A');
|
|
57
|
+
const result = await tryAcquireSendLock(lockDirY, 'agent-B');
|
|
58
|
+
expect(result).toEqual({ acquired: true });
|
|
59
|
+
await releaseSendLock(lockDirX);
|
|
60
|
+
await releaseSendLock(lockDirY);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('waitForSendLockRelease', () => {
|
|
65
|
+
describe('Given lock held for >10s by dead process', () => {
|
|
66
|
+
it('When agent calls waitForSendLockRelease Then stale lock broken and released: true returned', async () => {
|
|
67
|
+
const lockDir = sendLockDir(tmpDir, 'conv-stale');
|
|
68
|
+
await fs.mkdir(lockDir);
|
|
69
|
+
const staleLockInfo = {
|
|
70
|
+
pid: 999999,
|
|
71
|
+
agentId: 'dead-agent',
|
|
72
|
+
timestamp: Date.now() - 15_000,
|
|
73
|
+
};
|
|
74
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(staleLockInfo), 'utf-8');
|
|
75
|
+
const result = await waitForSendLockRelease(lockDir, 200);
|
|
76
|
+
expect(result).toEqual({ released: true });
|
|
77
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
78
|
+
expect(exists).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('Given agent blocked on send lock', () => {
|
|
82
|
+
it('When lock directory is manually deleted Then waitForSendLockRelease returns released', async () => {
|
|
83
|
+
const lockDir = sendLockDir(tmpDir, 'conv-deleted');
|
|
84
|
+
await fs.mkdir(lockDir);
|
|
85
|
+
const lockInfo = {
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
agentId: 'agent-A',
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(lockInfo), 'utf-8');
|
|
91
|
+
const waitPromise = waitForSendLockRelease(lockDir, 2000);
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
93
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
94
|
+
const result = await waitPromise;
|
|
95
|
+
expect(result).toEqual({ released: true });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Given agent A releases lock and agent C immediately acquires', () => {
|
|
99
|
+
it('When B timeout fires Then B detects C lock and does NOT break it', async () => {
|
|
100
|
+
const lockDir = sendLockDir(tmpDir, 'conv-reacquire');
|
|
101
|
+
await fs.mkdir(lockDir);
|
|
102
|
+
const originalInfo = {
|
|
103
|
+
pid: process.pid,
|
|
104
|
+
agentId: 'agent-A',
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
};
|
|
107
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(originalInfo), 'utf-8');
|
|
108
|
+
const waitPromise = waitForSendLockRelease(lockDir, 200);
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
110
|
+
const newInfo = {
|
|
111
|
+
pid: process.pid,
|
|
112
|
+
agentId: 'agent-C',
|
|
113
|
+
timestamp: Date.now() + 1000,
|
|
114
|
+
};
|
|
115
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(newInfo), 'utf-8');
|
|
116
|
+
const result = await waitPromise;
|
|
117
|
+
expect(result).toEqual({ timedOut: true });
|
|
118
|
+
const lockStillExists = await fs.stat(lockDir).catch(() => null);
|
|
119
|
+
expect(lockStillExists).not.toBeNull();
|
|
120
|
+
const currentInfo = await readSendLockInfo(lockDir);
|
|
121
|
+
expect(currentInfo.agentId).toBe('agent-C');
|
|
122
|
+
await releaseSendLock(lockDir);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('releaseSendLock', () => {
|
|
127
|
+
describe('Given send lock held', () => {
|
|
128
|
+
it('When releaseSendLock called Then lock directory and lock.info removed', async () => {
|
|
129
|
+
const lockDir = sendLockDir(tmpDir, 'conv-release');
|
|
130
|
+
await tryAcquireSendLock(lockDir, 'agent-A');
|
|
131
|
+
const existsBefore = await fs.stat(lockDir).catch(() => null);
|
|
132
|
+
expect(existsBefore).not.toBeNull();
|
|
133
|
+
await releaseSendLock(lockDir);
|
|
134
|
+
const existsAfter = await fs.stat(lockDir).catch(() => null);
|
|
135
|
+
expect(existsAfter).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Given send lock acquired', () => {
|
|
139
|
+
it('When operation fails Then lock released via finally pattern', async () => {
|
|
140
|
+
const lockDir = sendLockDir(tmpDir, 'conv-finally');
|
|
141
|
+
const acquireResult = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
142
|
+
expect(acquireResult.acquired).toBe(true);
|
|
143
|
+
let errorCaught = false;
|
|
144
|
+
try {
|
|
145
|
+
throw new Error('simulated failure');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
errorCaught = true;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await releaseSendLock(lockDir);
|
|
152
|
+
}
|
|
153
|
+
expect(errorCaught).toBe(true);
|
|
154
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
155
|
+
expect(exists).toBeNull();
|
|
156
|
+
const reacquireResult = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
157
|
+
expect(reacquireResult).toEqual({ acquired: true });
|
|
158
|
+
await releaseSendLock(lockDir);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('contention and waiting', () => {
|
|
163
|
+
describe('Given 2 agents try to acquire same conv lock', () => {
|
|
164
|
+
it('When first releases Then exactly one of the waiters acquires it', async () => {
|
|
165
|
+
const lockDir = sendLockDir(tmpDir, 'conv-contention');
|
|
166
|
+
const firstResult = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
167
|
+
expect(firstResult.acquired).toBe(true);
|
|
168
|
+
const secondResult = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
169
|
+
expect(secondResult.acquired).toBe(false);
|
|
170
|
+
const waitPromise = waitForSendLockRelease(lockDir, 2000);
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
172
|
+
await releaseSendLock(lockDir);
|
|
173
|
+
const waitResult = await waitPromise;
|
|
174
|
+
expect(waitResult).toEqual({ released: true });
|
|
175
|
+
const acquireAfterWait = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
176
|
+
expect(acquireAfterWait.acquired).toBe(true);
|
|
177
|
+
await releaseSendLock(lockDir);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('getMessagesSince', () => {
|
|
182
|
+
describe('Given agent A sent messages while B waited', () => {
|
|
183
|
+
it('When B calls getMessagesSince with snapshot index Then only messages since snapshot returned', async () => {
|
|
184
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'conv-messages.json');
|
|
185
|
+
const messages = [
|
|
186
|
+
{ id: 'msg-1', conversationId: 'conv-messages', senderId: 'agent-A', content: 'Hello', type: MessageType.Message, timestamp: 1000 },
|
|
187
|
+
{ id: 'msg-2', conversationId: 'conv-messages', senderId: 'agent-A', content: 'World', type: MessageType.Message, timestamp: 2000 },
|
|
188
|
+
{ id: 'msg-3', conversationId: 'conv-messages', senderId: 'agent-A', content: 'New msg', type: MessageType.Message, timestamp: 3000 },
|
|
189
|
+
];
|
|
190
|
+
await writeJsonFile(messagesPath, messages);
|
|
191
|
+
const snapshotIndex = 1;
|
|
192
|
+
const result = await getMessagesSince(messagesPath, snapshotIndex);
|
|
193
|
+
expect(result).toHaveLength(2);
|
|
194
|
+
expect(result[0].id).toBe('msg-2');
|
|
195
|
+
expect(result[1].id).toBe('msg-3');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe('getMessageCount', () => {
|
|
200
|
+
describe('Given messages file has messages', () => {
|
|
201
|
+
it('When getMessageCount called Then correct count returned', async () => {
|
|
202
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'conv-count.json');
|
|
203
|
+
const messages = [
|
|
204
|
+
{ id: 'msg-1', conversationId: 'conv-count', senderId: 'agent-A', content: 'Hello', type: MessageType.Message, timestamp: 1000 },
|
|
205
|
+
{ id: 'msg-2', conversationId: 'conv-count', senderId: 'agent-B', content: 'World', type: MessageType.Message, timestamp: 2000 },
|
|
206
|
+
{ id: 'msg-3', conversationId: 'conv-count', senderId: 'agent-A', content: 'Goodbye', type: MessageType.Message, timestamp: 3000 },
|
|
207
|
+
];
|
|
208
|
+
await writeJsonFile(messagesPath, messages);
|
|
209
|
+
const count = await getMessageCount(messagesPath);
|
|
210
|
+
expect(count).toBe(3);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('Given no messages file exists', () => {
|
|
214
|
+
it('When getMessageCount called Then 0 returned', async () => {
|
|
215
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'nonexistent.json');
|
|
216
|
+
const count = await getMessageCount(messagesPath);
|
|
217
|
+
expect(count).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('integration: participation check before lock', () => {
|
|
222
|
+
describe('Given agent not in conversation', () => {
|
|
223
|
+
it('When participation check runs before lock Then check fails before lock acquisition', async () => {
|
|
224
|
+
const conversationId = 'conv-participation';
|
|
225
|
+
const agentId = 'agent-outsider';
|
|
226
|
+
const participants = ['agent-A', 'agent-B'];
|
|
227
|
+
const isParticipant = participants.includes(agentId);
|
|
228
|
+
expect(isParticipant).toBe(false);
|
|
229
|
+
const lockDir = sendLockDir(tmpDir, conversationId);
|
|
230
|
+
if (!isParticipant) {
|
|
231
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
232
|
+
expect(exists).toBeNull();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await tryAcquireSendLock(lockDir, agentId);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=send-lock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-lock.test.js","sourceRoot":"","sources":["../../src/__tests__/send-lock.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-handlers.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tool-handlers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { StateService } from '../services/state-service.js';
|
|
6
|
+
import { handleToolCall } from '../services/tool-handlers.js';
|
|
7
|
+
import { tryAcquireSendLock, sendLockDir, releaseSendLock, } from '../utils/send-lock.js';
|
|
8
|
+
import { ConversationType } from '../enums/conversation-type.js';
|
|
9
|
+
let tmpDir;
|
|
10
|
+
let stateService;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-toolhandlers-test-'));
|
|
13
|
+
stateService = new StateService(tmpDir);
|
|
14
|
+
await stateService.init();
|
|
15
|
+
});
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
describe('handleToolCall send_message', () => {
|
|
21
|
+
describe('Given a single agent in a conversation with no contention', () => {
|
|
22
|
+
it('When send_message is called Then the message is sent and no lock directory remains', async () => {
|
|
23
|
+
// Given
|
|
24
|
+
const agent = await stateService.registerAgent('/test/project');
|
|
25
|
+
const conversation = await stateService.createConversation({
|
|
26
|
+
type: ConversationType.Group,
|
|
27
|
+
name: 'test-conv',
|
|
28
|
+
participants: [agent.id],
|
|
29
|
+
});
|
|
30
|
+
await stateService.setHasAnnounced(agent.id, conversation.id);
|
|
31
|
+
// When
|
|
32
|
+
const result = await handleToolCall(stateService, 'send_message', agent.id, {
|
|
33
|
+
content: 'Hello world',
|
|
34
|
+
conversationId: conversation.id,
|
|
35
|
+
});
|
|
36
|
+
// Then
|
|
37
|
+
expect('isError' in result).toBe(false);
|
|
38
|
+
expect(result.content[0].text).toContain('Message sent');
|
|
39
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
40
|
+
const lockExists = await fs.stat(lockDir).catch(() => null);
|
|
41
|
+
expect(lockExists).toBeNull();
|
|
42
|
+
const messages = await stateService.getMessages(conversation.id);
|
|
43
|
+
expect(messages).toHaveLength(1);
|
|
44
|
+
expect(messages[0].content).toBe('Hello world');
|
|
45
|
+
expect(messages[0].senderId).toBe(agent.id);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Given two agents in a conversation and agent A holds the send lock', () => {
|
|
49
|
+
it('When agent B calls send_message Then B receives a contention error containing the competing message', async () => {
|
|
50
|
+
// Given
|
|
51
|
+
const agentA = await stateService.registerAgent('/test/project');
|
|
52
|
+
const agentB = await stateService.registerAgent('/test/project');
|
|
53
|
+
const conversation = await stateService.createConversation({
|
|
54
|
+
type: ConversationType.Group,
|
|
55
|
+
name: 'contention-conv',
|
|
56
|
+
participants: [agentA.id, agentB.id],
|
|
57
|
+
});
|
|
58
|
+
await stateService.setHasAnnounced(agentA.id, conversation.id);
|
|
59
|
+
await stateService.setHasAnnounced(agentB.id, conversation.id);
|
|
60
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
61
|
+
await tryAcquireSendLock(lockDir, agentA.id);
|
|
62
|
+
// When
|
|
63
|
+
const agentBPromise = handleToolCall(stateService, 'send_message', agentB.id, {
|
|
64
|
+
content: 'Message from B',
|
|
65
|
+
conversationId: conversation.id,
|
|
66
|
+
});
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
await stateService.addMessage(conversation.id, agentA.id, 'Message from A', 'message');
|
|
69
|
+
await releaseSendLock(lockDir);
|
|
70
|
+
const result = await agentBPromise;
|
|
71
|
+
// Then
|
|
72
|
+
expect(result.isError).toBe(true);
|
|
73
|
+
expect(result.content[0].text).toContain('Message from A');
|
|
74
|
+
expect(result.content[0].text).toContain('reconsider');
|
|
75
|
+
const messages = await stateService.getMessages(conversation.id);
|
|
76
|
+
const bMessages = messages.filter((m) => m.senderId === agentB.id);
|
|
77
|
+
expect(bMessages).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('Given an agent in a conversation and addMessage throws', () => {
|
|
81
|
+
it('When send_message is called Then the lock is released in finally and the error propagates', async () => {
|
|
82
|
+
// Given
|
|
83
|
+
const agent = await stateService.registerAgent('/test/project');
|
|
84
|
+
const conversation = await stateService.createConversation({
|
|
85
|
+
type: ConversationType.Group,
|
|
86
|
+
name: 'fail-conv',
|
|
87
|
+
participants: [agent.id],
|
|
88
|
+
});
|
|
89
|
+
await stateService.setHasAnnounced(agent.id, conversation.id);
|
|
90
|
+
vi.spyOn(stateService, 'addMessage').mockRejectedValueOnce(new Error('Disk write failed'));
|
|
91
|
+
// When / Then
|
|
92
|
+
await expect(handleToolCall(stateService, 'send_message', agent.id, {
|
|
93
|
+
content: 'This will fail',
|
|
94
|
+
conversationId: conversation.id,
|
|
95
|
+
})).rejects.toThrow('Disk write failed');
|
|
96
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
97
|
+
const lockExists = await fs.stat(lockDir).catch(() => null);
|
|
98
|
+
expect(lockExists).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=tool-handlers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-handlers.test.js","sourceRoot":"","sources":["../../src/__tests__/tool-handlers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-notice.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/update-notice.test.ts"],"names":[],"mappings":""}
|