@open-mercato/ai-assistant 0.6.5-develop.4516.1.88e6ab71a9 → 0.6.5-develop.4534.1.b459babe6d
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js +146 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js +119 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js +174 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js +132 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js +68 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js +74 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js +57 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +127 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +7 -0
- package/package.json +6 -6
- package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.ts +209 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.ts +176 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.ts +222 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.ts +184 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.ts +95 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.ts +115 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.ts +97 -0
- package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +198 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Client } from 'pg';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Direct-Postgres fixtures for AI Assistant integration specs.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the sanctioned pattern in
|
|
10
|
+
* `@open-mercato/core/helpers/integration/dbFixtures` (raw SQL against
|
|
11
|
+
* `DATABASE_URL`) for the surfaces that have NO public create route:
|
|
12
|
+
* - `ai_pending_actions`: a pending mutation approval is only ever born from
|
|
13
|
+
* the internal `prepareMutation` path during a real LLM agent turn. Seeding
|
|
14
|
+
* the row directly keeps confirm/cancel/GET coverage deterministic and
|
|
15
|
+
* provider-free.
|
|
16
|
+
* - prompt overrides: the `prompt-override` route exposes no DELETE, so
|
|
17
|
+
* versioned rows must be swept via SQL after a test.
|
|
18
|
+
*
|
|
19
|
+
* The app server and this helper MUST share one `DATABASE_URL`, so specs using
|
|
20
|
+
* these helpers only run under the coherent app+DB harness
|
|
21
|
+
* (`yarn test:integration` / `:ephemeral`), never against an arbitrary dev
|
|
22
|
+
* server whose `DATABASE_URL` differs from `apps/mercato/.env`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function resolveAppRoot(): string {
|
|
26
|
+
const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();
|
|
27
|
+
return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readEnvValue(key: string): string | undefined {
|
|
31
|
+
if (process.env[key]) return process.env[key];
|
|
32
|
+
const candidatePaths = [
|
|
33
|
+
path.resolve(resolveAppRoot(), '.env'),
|
|
34
|
+
path.resolve(process.cwd(), 'apps/mercato/.env'),
|
|
35
|
+
path.resolve(process.cwd(), '.env'),
|
|
36
|
+
];
|
|
37
|
+
for (const envPath of candidatePaths) {
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
40
|
+
const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));
|
|
41
|
+
if (match?.[1]) return match[1].trim();
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveDatabaseUrl(): string {
|
|
50
|
+
const url = readEnvValue('DATABASE_URL');
|
|
51
|
+
if (!url) throw new Error('[internal] DATABASE_URL is not configured for AI Assistant integration DB fixtures');
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {
|
|
56
|
+
const client = new Client({ connectionString: resolveDatabaseUrl() });
|
|
57
|
+
await client.connect();
|
|
58
|
+
try {
|
|
59
|
+
return await run(client);
|
|
60
|
+
} finally {
|
|
61
|
+
await client.end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type SeededPendingActionStatus =
|
|
66
|
+
| 'pending'
|
|
67
|
+
| 'confirmed'
|
|
68
|
+
| 'cancelled'
|
|
69
|
+
| 'expired'
|
|
70
|
+
| 'executing'
|
|
71
|
+
| 'failed';
|
|
72
|
+
|
|
73
|
+
export interface SeedPendingActionInput {
|
|
74
|
+
tenantId: string;
|
|
75
|
+
organizationId?: string | null;
|
|
76
|
+
createdByUserId: string;
|
|
77
|
+
status?: SeededPendingActionStatus;
|
|
78
|
+
agentId?: string;
|
|
79
|
+
toolName?: string;
|
|
80
|
+
/** Minutes from now until expiry. Negative => the row is already expired. */
|
|
81
|
+
expiresInMinutes?: number;
|
|
82
|
+
normalizedInput?: Record<string, unknown>;
|
|
83
|
+
executionResult?: Record<string, unknown> | null;
|
|
84
|
+
idempotencyKey?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SeededPendingAction {
|
|
88
|
+
id: string;
|
|
89
|
+
idempotencyKey: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Inserts an `ai_pending_actions` row directly. Returns the new id so the test
|
|
94
|
+
* can act on it and clean it up. Defaults produce an actionable `pending` row
|
|
95
|
+
* with a future TTL; override `status`/`expiresInMinutes`/`executionResult` to
|
|
96
|
+
* cover the idempotency short-circuit and 409 error branches.
|
|
97
|
+
*/
|
|
98
|
+
export async function seedPendingActionInDb(input: SeedPendingActionInput): Promise<SeededPendingAction> {
|
|
99
|
+
const idempotencyKey = input.idempotencyKey ?? `it-pending-${randomUUID()}`;
|
|
100
|
+
const status = input.status ?? 'pending';
|
|
101
|
+
const expiresInMinutes = input.expiresInMinutes ?? 60;
|
|
102
|
+
const normalizedInput = JSON.stringify(input.normalizedInput ?? {});
|
|
103
|
+
const executionResult =
|
|
104
|
+
input.executionResult === undefined || input.executionResult === null
|
|
105
|
+
? null
|
|
106
|
+
: JSON.stringify(input.executionResult);
|
|
107
|
+
return withClient(async (client) => {
|
|
108
|
+
const result = await client.query<{ id: string }>(
|
|
109
|
+
`insert into ai_pending_actions
|
|
110
|
+
(id, tenant_id, organization_id, agent_id, tool_name, normalized_input,
|
|
111
|
+
field_diff, attachment_ids, idempotency_key, created_by_user_id, status,
|
|
112
|
+
queue_mode, execution_result, created_at, expires_at)
|
|
113
|
+
values
|
|
114
|
+
(gen_random_uuid(), $1, $2, $3, $4, $5::jsonb,
|
|
115
|
+
'[]'::jsonb, '[]'::jsonb, $6, $7, $8,
|
|
116
|
+
'inline', $9::jsonb, now(), now() + make_interval(mins => $10::int))
|
|
117
|
+
returning id`,
|
|
118
|
+
[
|
|
119
|
+
input.tenantId,
|
|
120
|
+
input.organizationId ?? null,
|
|
121
|
+
input.agentId ?? 'it_agent.pending_fixture',
|
|
122
|
+
input.toolName ?? 'it_tool.noop',
|
|
123
|
+
normalizedInput,
|
|
124
|
+
idempotencyKey,
|
|
125
|
+
input.createdByUserId,
|
|
126
|
+
status,
|
|
127
|
+
executionResult,
|
|
128
|
+
expiresInMinutes,
|
|
129
|
+
],
|
|
130
|
+
);
|
|
131
|
+
return { id: result.rows[0].id, idempotencyKey };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Hard-deletes a seeded pending action row (best-effort test cleanup). */
|
|
136
|
+
export async function deletePendingActionInDb(id: string | null): Promise<void> {
|
|
137
|
+
if (!id) return;
|
|
138
|
+
await withClient(async (client) => {
|
|
139
|
+
await client.query('delete from ai_pending_actions where id = $1', [id]);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sweeps every per-agent override row for a tenant across the three override
|
|
145
|
+
* tables. Required for `prompt-override` cleanup (no DELETE route) and used as a
|
|
146
|
+
* belt-and-braces teardown for mutation-policy / loop overrides.
|
|
147
|
+
*/
|
|
148
|
+
export async function deleteAgentOverridesInDb(input: { tenantId: string; agentId: string }): Promise<void> {
|
|
149
|
+
if (!input.tenantId || !input.agentId) return;
|
|
150
|
+
await withClient(async (client) => {
|
|
151
|
+
await client.query('delete from ai_agent_prompt_overrides where tenant_id = $1 and agent_id = $2', [
|
|
152
|
+
input.tenantId,
|
|
153
|
+
input.agentId,
|
|
154
|
+
]);
|
|
155
|
+
await client.query('delete from ai_agent_mutation_policy_overrides where tenant_id = $1 and agent_id = $2', [
|
|
156
|
+
input.tenantId,
|
|
157
|
+
input.agentId,
|
|
158
|
+
]);
|
|
159
|
+
await client.query('delete from ai_agent_runtime_overrides where tenant_id = $1 and agent_id = $2', [
|
|
160
|
+
input.tenantId,
|
|
161
|
+
input.agentId,
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Hard-deletes any tenant model-allowlist rows (best-effort cleanup). */
|
|
167
|
+
export async function deleteTenantAllowlistInDb(tenantId: string | null): Promise<void> {
|
|
168
|
+
if (!tenantId) return;
|
|
169
|
+
await withClient(async (client) => {
|
|
170
|
+
await client.query('delete from ai_tenant_model_allowlists where tenant_id = $1', [tenantId]);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hard-deletes a conversation and its participant + message rows by the
|
|
176
|
+
* client-facing `conversation_id`. The DELETE route only soft-deletes, so this
|
|
177
|
+
* gives specs a true teardown for the rows they created.
|
|
178
|
+
*/
|
|
179
|
+
export async function deleteConversationCascadeInDb(input: {
|
|
180
|
+
tenantId: string;
|
|
181
|
+
conversationId: string;
|
|
182
|
+
}): Promise<void> {
|
|
183
|
+
if (!input.tenantId || !input.conversationId) return;
|
|
184
|
+
await withClient(async (client) => {
|
|
185
|
+
await client.query('delete from ai_chat_messages where tenant_id = $1 and conversation_id = $2', [
|
|
186
|
+
input.tenantId,
|
|
187
|
+
input.conversationId,
|
|
188
|
+
]);
|
|
189
|
+
await client.query(
|
|
190
|
+
'delete from ai_chat_conversation_participants where tenant_id = $1 and conversation_id = $2',
|
|
191
|
+
[input.tenantId, input.conversationId],
|
|
192
|
+
);
|
|
193
|
+
await client.query('delete from ai_chat_conversations where tenant_id = $1 and conversation_id = $2', [
|
|
194
|
+
input.tenantId,
|
|
195
|
+
input.conversationId,
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
}
|