Compare commits

...

6 Commits

Author SHA1 Message Date
Omri Maya 0d841bcd05 fix(ncl): default messaging-groups create instance to channel_type
`ncl messaging-groups create` failed with a NOT NULL violation on the
`instance` column (migration 016). The generic CRUD insert builds its
column list from the resource definition, and `instance` wasn't declared
there — so the INSERT omitted the column entirely. The router path never
hit this because it goes through `createMessagingGroup`, which has its own
`instance ?? channel_type` fallback.

There is no operator-facing reason to require `--instance`: the default
instance IS the channel type (migration 016, `createMessagingGroup`, and
the default-instance resolver all encode this). So rather than force a
flag, default it.

- crud: add `defaultFrom` to ColumnDef — default a column to another
  already-resolved column's value on create. Generic, reusable.
- messaging-groups: declare `instance` with `defaultFrom: 'channel_type'`
  (placed after channel_type so it resolves first), still overridable via
  `--instance` for multi-instance setups.
- test: drive the real dispatch('messaging-groups-create') path; asserts
  omitted -> channel_type and explicit --instance preserved. Goes red if
  the column/defaultFrom wiring is deleted (insert fails NOT NULL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 08:38:22 +03:00
gavrielc 2afbd18233 Merge pull request #2859 from cben0ist/fix/migrate-v2-is-main
fix(migrate-v2): don't SELECT is_main from v1 registered_groups
2026-06-26 13:38:42 +03:00
gavrielc 953496dc37 Merge branch 'main' into fix/migrate-v2-is-main 2026-06-26 13:38:27 +03:00
Christophe Benoist 797491d8b3 fix(migrate-v2): don't SELECT is_main from v1 registered_groups
The v2 DB seed queried `is_main` from the v1 `registered_groups` table, but
that column was a later v1 addition — older v1 installs (e.g. 1.1.0) don't have
it, so the migration's `1b-db` step crashes with `no such column: is_main` and
v2.db is never created, cascading into the sessions and tasks steps failing.

`is_main` was selected into the V1Group interface but never read anywhere, so
this just drops it from the SELECT and the interface. The accompanying comment
already states the intent ("Query only the columns we know exist in all v1
installs") — the code now matches it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:43:19 -04:00
github-actions[bot] 2df754459b chore: bump version to 2.1.21 2026-06-25 18:59:54 +00:00
gavrielc 0896d4089e Merge pull request #2832 from nanocoai/feat/reject-with-reason
feat(approvals): reject with reason
2026-06-25 21:59:42 +03:00
5 changed files with 89 additions and 3 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nanoclaw",
"version": "2.1.20",
"version": "2.1.21",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
"packageManager": "pnpm@10.33.0",
+1 -2
View File
@@ -43,7 +43,6 @@ interface V1Group {
folder: string;
trigger_pattern: string | null;
requires_trigger: number | null;
is_main: number | null;
}
async function main(): Promise<void> {
@@ -65,7 +64,7 @@ async function main(): Promise<void> {
// v1 schema varies — channel_name was a late addition. Query only the
// columns we know exist in all v1 installs.
const v1Groups = v1Db
.prepare('SELECT jid, name, folder, trigger_pattern, requires_trigger, is_main FROM registered_groups')
.prepare('SELECT jid, name, folder, trigger_pattern, requires_trigger FROM registered_groups')
.all() as V1Group[];
v1Db.close();
+4
View File
@@ -30,6 +30,8 @@ export interface ColumnDef {
updatable?: boolean;
/** Default value on create when not provided. */
default?: unknown;
/** Default to another column's resolved value on create when not provided. */
defaultFrom?: string;
/** Allowed values (shown in help). */
enum?: string[];
}
@@ -150,6 +152,8 @@ function genericCreate(def: ResourceDef) {
throw new Error(`--${col.name.replace(/_/g, '-')} is required`);
} else if (col.default !== undefined) {
values[col.name] = col.default;
} else if (col.defaultFrom !== undefined && values[col.defaultFrom] !== undefined) {
values[col.name] = values[col.defaultFrom];
}
}
@@ -0,0 +1,75 @@
/**
* Regression test: `ncl messaging-groups create` must satisfy the NOT NULL
* `instance` column without an operator-supplied `--instance`. The column has
* no CLI flag at the operator's altitude (the default instance IS the channel
* type), so the generic CRUD insert defaults it to `channel_type` — matching
* `createMessagingGroup`'s `instance ?? channel_type` fallback on the router
* path. Delete the `instance` column / `defaultFrom` wiring in
* `messaging-groups.ts` and this goes red: the insert fails the NOT NULL.
*/
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-msggroups' };
});
const TEST_DIR = '/tmp/nanoclaw-test-cli-msggroups';
import { initTestDb, closeDb, runMigrations } from '../../db/index.js';
import { getMessagingGroupByPlatform } from '../../db/messaging-groups.js';
import { dispatch } from '../dispatch.js';
// Side-effect import: registers the `messaging-groups-create` command.
import './messaging-groups.js';
describe('messaging-groups CLI create defaults instance to channel_type', () => {
beforeEach(() => {
if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
fs.mkdirSync(TEST_DIR, { recursive: true });
runMigrations(initTestDb());
});
afterEach(() => {
closeDb();
if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
});
it('create without --instance sets instance = channel_type', async () => {
// caller: 'host' is the post-approval re-entry path for create (approval op).
const resp = await dispatch(
{
id: 'req-1',
command: 'messaging-groups-create',
args: { channel_type: 'telegram', platform_id: '12345' },
},
{ caller: 'host' },
);
expect(resp.ok).toBe(true);
const row = getMessagingGroupByPlatform('telegram', '12345');
expect(row).toBeDefined();
expect(row?.instance).toBe('telegram');
});
it('create with an explicit --instance keeps that value', async () => {
const resp = await dispatch(
{
id: 'req-2',
command: 'messaging-groups-create',
args: { channel_type: 'telegram', platform_id: '67890', instance: 'work' },
},
{ caller: 'host' },
);
expect(resp.ok).toBe(true);
expect(getMessagingGroupByPlatform('telegram', '67890', 'work')?.instance).toBe('work');
});
});
+8
View File
@@ -23,6 +23,14 @@ registerResource({
'Platform-specific chat ID. Format varies: Telegram chat ID, Discord channel snowflake, Slack channel ID, phone number, email address.',
required: true,
},
{
name: 'instance',
type: 'string',
description:
'Adapter instance that owns this chat, when running N adapters of one channel type. Defaults to channel_type (the default instance) when omitted.',
defaultFrom: 'channel_type',
updatable: true,
},
{
name: 'name',
type: 'string',