mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e52f4eaa54 |
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Regression test — `ncl wirings create` must delegate to
|
||||
* `createMessagingGroupAgent` so the matching `agent_destinations` ACL row is
|
||||
* auto-created. The generic single-table INSERT skipped it, leaving ncl-wired
|
||||
* agents silently without the send authorization skill-wired agents get
|
||||
* (delivery throws "unauthorized channel destination" for non-origin sends).
|
||||
*
|
||||
* The approval handler in `dispatch.ts` re-enters `dispatch()` with
|
||||
* `caller: 'host'` after admin approval, so the test invokes dispatch with the
|
||||
* host caller — same code path a real approval would take.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../container-runner.js', () => ({
|
||||
wakeContainer: vi.fn().mockResolvedValue(undefined),
|
||||
isContainerRunning: vi.fn().mockReturnValue(false),
|
||||
getActiveContainerCount: vi.fn().mockReturnValue(0),
|
||||
killContainer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../config.js', async () => {
|
||||
const actual = await vi.importActual('../../config.js');
|
||||
return { ...actual, DATA_DIR: '/tmp/nanoclaw-test-cli-wirings' };
|
||||
});
|
||||
|
||||
const TEST_DIR = '/tmp/nanoclaw-test-cli-wirings';
|
||||
|
||||
import { initTestDb, closeDb, runMigrations, createAgentGroup, getDb } from '../../db/index.js';
|
||||
import { dispatch } from '../dispatch.js';
|
||||
// Side-effect import: registers the `wirings-*` commands (including create).
|
||||
import './wirings.js';
|
||||
|
||||
function now(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
describe('wirings CLI create auto-creates the send-authorization ACL row', () => {
|
||||
const GID = 'ag-1';
|
||||
const MGID = 'mg-1';
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
|
||||
const db = initTestDb();
|
||||
runMigrations(db);
|
||||
|
||||
createAgentGroup({ id: GID, name: 'agent', folder: 'agent', agent_provider: null, created_at: now() });
|
||||
db.prepare(
|
||||
`INSERT INTO messaging_groups (id, channel_type, platform_id, instance, name, is_group, unknown_sender_policy, created_at)
|
||||
VALUES (?, 'telegram', 'tg-1', 'telegram', 'chat', 1, 'strict', ?)`,
|
||||
).run(MGID, now());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
closeDb();
|
||||
if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
it('creates the wiring and the matching agent_destinations row', async () => {
|
||||
// Precondition: no destination exists yet.
|
||||
const before = getDb()
|
||||
.prepare('SELECT COUNT(*) AS c FROM agent_destinations WHERE agent_group_id = ?')
|
||||
.get(GID) as { c: number };
|
||||
expect(before.c).toBe(0);
|
||||
|
||||
const resp = await dispatch(
|
||||
{
|
||||
id: 'req-create',
|
||||
command: 'wirings-create',
|
||||
args: { messaging_group_id: MGID, agent_group_id: GID },
|
||||
},
|
||||
{ caller: 'host' },
|
||||
);
|
||||
|
||||
expect(resp.ok).toBe(true);
|
||||
|
||||
// The wiring row exists.
|
||||
const wiring = getDb()
|
||||
.prepare('SELECT * FROM messaging_group_agents WHERE messaging_group_id = ? AND agent_group_id = ?')
|
||||
.get(MGID, GID) as Record<string, unknown> | undefined;
|
||||
expect(wiring).toBeDefined();
|
||||
|
||||
// The send-authorization ACL row was auto-created and points at the chat.
|
||||
const dest = getDb()
|
||||
.prepare('SELECT * FROM agent_destinations WHERE agent_group_id = ? AND target_type = ? AND target_id = ?')
|
||||
.get(GID, 'channel', MGID) as Record<string, unknown> | undefined;
|
||||
expect(dest).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import { createMessagingGroupAgent } from '../../db/messaging-groups.js';
|
||||
import type { EngageMode, IgnoredMessagePolicy, MessagingGroupAgent, SenderScope } from '../../types.js';
|
||||
import { registerResource } from '../crud.js';
|
||||
|
||||
registerResource({
|
||||
@@ -66,5 +70,60 @@ registerResource({
|
||||
},
|
||||
{ name: 'created_at', type: 'string', description: 'Auto-set.', generated: true },
|
||||
],
|
||||
operations: { list: 'open', get: 'open', create: 'approval', update: 'approval', delete: 'approval' },
|
||||
// `create` is intentionally not in `operations` — the generic single-table
|
||||
// INSERT skips the `agent_destinations` ACL row that the canonical helper
|
||||
// `createMessagingGroupAgent` auto-creates, so ncl-wired agents would
|
||||
// silently lack the send authorization skill-wired agents get (delivery
|
||||
// throws "unauthorized channel destination" for non-origin sends). Provided
|
||||
// as a `customOperation` that delegates to the helper instead.
|
||||
operations: { list: 'open', get: 'open', update: 'approval', delete: 'approval' },
|
||||
customOperations: {
|
||||
create: {
|
||||
access: 'approval',
|
||||
description:
|
||||
'Wire a messaging group to an agent group. Delegates to createMessagingGroupAgent so the ' +
|
||||
'matching agent_destinations ACL row is auto-created (a bare INSERT would skip it, leaving ' +
|
||||
'the agent unauthorized to send to the chat). Use --messaging-group-id and --agent-group-id, ' +
|
||||
'plus optional --engage-mode, --engage-pattern, --sender-scope, --ignored-message-policy, --session-mode.',
|
||||
handler: async (args) => {
|
||||
const messagingGroupId = args.messaging_group_id ? String(args.messaging_group_id) : '';
|
||||
const agentGroupId = args.agent_group_id ? String(args.agent_group_id) : '';
|
||||
if (!messagingGroupId) throw new Error('--messaging-group-id is required');
|
||||
if (!agentGroupId) throw new Error('--agent-group-id is required');
|
||||
|
||||
const engageMode = args.engage_mode !== undefined ? String(args.engage_mode) : 'mention';
|
||||
if (!['pattern', 'mention', 'mention-sticky'].includes(engageMode)) {
|
||||
throw new Error('engage_mode must be one of: pattern, mention, mention-sticky');
|
||||
}
|
||||
const senderScope = args.sender_scope !== undefined ? String(args.sender_scope) : 'all';
|
||||
if (!['all', 'known'].includes(senderScope)) {
|
||||
throw new Error('sender_scope must be one of: all, known');
|
||||
}
|
||||
const ignoredMessagePolicy =
|
||||
args.ignored_message_policy !== undefined ? String(args.ignored_message_policy) : 'drop';
|
||||
if (!['drop', 'accumulate'].includes(ignoredMessagePolicy)) {
|
||||
throw new Error('ignored_message_policy must be one of: drop, accumulate');
|
||||
}
|
||||
const sessionMode = args.session_mode !== undefined ? String(args.session_mode) : 'shared';
|
||||
if (!['shared', 'per-thread', 'agent-shared'].includes(sessionMode)) {
|
||||
throw new Error('session_mode must be one of: shared, per-thread, agent-shared');
|
||||
}
|
||||
|
||||
const mga: MessagingGroupAgent = {
|
||||
id: randomUUID(),
|
||||
messaging_group_id: messagingGroupId,
|
||||
agent_group_id: agentGroupId,
|
||||
engage_mode: engageMode as EngageMode,
|
||||
engage_pattern: args.engage_pattern !== undefined ? String(args.engage_pattern) : null,
|
||||
sender_scope: senderScope as SenderScope,
|
||||
ignored_message_policy: ignoredMessagePolicy as IgnoredMessagePolicy,
|
||||
session_mode: sessionMode as MessagingGroupAgent['session_mode'],
|
||||
priority: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
createMessagingGroupAgent(mga);
|
||||
return mga;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user