@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.
Files changed (26) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js +146 -0
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js.map +7 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js +119 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js.map +7 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js +174 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js +132 -0
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js.map +7 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js +68 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js.map +7 -0
  12. package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js +74 -0
  13. package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js.map +7 -0
  14. package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js +57 -0
  15. package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js.map +7 -0
  16. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +127 -0
  17. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +7 -0
  18. package/package.json +6 -6
  19. package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.ts +209 -0
  20. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.ts +176 -0
  21. package/src/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.ts +222 -0
  22. package/src/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.ts +184 -0
  23. package/src/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.ts +95 -0
  24. package/src/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.ts +115 -0
  25. package/src/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.ts +97 -0
  26. 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
+ }