@open-mercato/ai-assistant 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4

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 (55) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -2
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js +146 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.js.map +7 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +4 -8
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +2 -2
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js +119 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.js.map +7 -0
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js +174 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js.map +7 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js +132 -0
  12. package/dist/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js.map +7 -0
  13. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -0
  14. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
  15. package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js +68 -0
  16. package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js.map +7 -0
  17. package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js +74 -0
  18. package/dist/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.js.map +7 -0
  19. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +6 -5
  20. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +2 -2
  21. package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js +57 -0
  22. package/dist/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.js.map +7 -0
  23. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +127 -0
  24. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +7 -0
  25. package/dist/modules/ai_assistant/lib/auth.js +2 -11
  26. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  27. package/dist/modules/ai_assistant/lib/codemode-tools.js +17 -20
  28. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  29. package/dist/modules/ai_assistant/lib/http-server.js +3 -2
  30. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  31. package/dist/modules/ai_assistant/lib/log-redaction.js +25 -0
  32. package/dist/modules/ai_assistant/lib/log-redaction.js.map +7 -0
  33. package/dist/modules/ai_assistant/lib/tool-test-runner.js +5 -3
  34. package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +2 -2
  35. package/package.json +10 -11
  36. package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-PENDING-004-confirm-cancel-mutations.spec.ts +209 -0
  37. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +6 -18
  38. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-OVERRIDES-005-prompt-mutation-loop.spec.ts +176 -0
  39. package/src/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.ts +222 -0
  40. package/src/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.ts +184 -0
  41. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -0
  42. package/src/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.ts +95 -0
  43. package/src/modules/ai_assistant/__integration__/TC-AI-SETTINGS-ALLOWLIST-003-put-delete-allowlist.spec.ts +115 -0
  44. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +7 -5
  45. package/src/modules/ai_assistant/__integration__/TC-AI-TOOLS-EXECUTE-007-direct-tool-execution.spec.ts +97 -0
  46. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +198 -0
  47. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +27 -0
  48. package/src/modules/ai_assistant/lib/__tests__/codemode-tools.test.ts +128 -0
  49. package/src/modules/ai_assistant/lib/__tests__/log-redaction.test.ts +65 -0
  50. package/src/modules/ai_assistant/lib/__tests__/tool-test-runner-pick-default-tenant.test.ts +70 -0
  51. package/src/modules/ai_assistant/lib/auth.ts +9 -15
  52. package/src/modules/ai_assistant/lib/codemode-tools.ts +21 -29
  53. package/src/modules/ai_assistant/lib/http-server.ts +3 -2
  54. package/src/modules/ai_assistant/lib/log-redaction.ts +41 -0
  55. package/src/modules/ai_assistant/lib/tool-test-runner.ts +11 -6
@@ -124,13 +124,14 @@ async function pickDefaultTenant(container) {
124
124
  );
125
125
  const tenantId = Array.isArray(tenantRows) && tenantRows[0] ? String(tenantRows[0].id) : null;
126
126
  if (!tenantId) return null;
127
- const escaped = tenantId.replace(/'/g, "''");
128
127
  const orgRows = await conn.execute(
129
- `SELECT id FROM organizations WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`
128
+ `SELECT id FROM organizations WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,
129
+ [tenantId]
130
130
  );
131
131
  const organizationId = Array.isArray(orgRows) && orgRows[0] ? String(orgRows[0].id) : null;
132
132
  const userRows = await conn.execute(
133
- `SELECT id FROM users WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`
133
+ `SELECT id FROM users WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,
134
+ [tenantId]
134
135
  );
135
136
  const userId = Array.isArray(userRows) && userRows[0] ? String(userRows[0].id) : null;
136
137
  return { tenantId, organizationId, userId };
@@ -413,6 +414,7 @@ async function runToolTests(options = {}) {
413
414
  };
414
415
  }
415
416
  export {
417
+ pickDefaultTenant,
416
418
  runToolTests
417
419
  };
418
420
  //# sourceMappingURL=tool-test-runner.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/tool-test-runner.ts"],
4
- "sourcesContent": ["/**\n * In-process AI tool test runner.\n *\n * Iterates every tool registered in `ai-tools.generated.ts`, invokes the\n * handler against a super-admin tenant context, and returns a structured\n * report. Used by the `mercato ai_assistant test-tools` CLI subcommand and\n * by `.ai/qa/tests/integration/TC-INT-AI-TOOLS.spec.ts`.\n *\n * Safety posture:\n * - No HTTP exposure \u2014 runs only inside the Node process driving the CLI.\n * - Mutation tools are exercised through `prepareMutation` and we assert a\n * pending-action envelope is returned. The pending-action row is created\n * in `ai_pending_actions` but never confirmed, so no real write happens.\n * - Tools without an explicit fixture entry are skipped with reason\n * `'no fixture'` rather than failing.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { McpToolContext, AiToolDefinition } from './types'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport { getFixture, type ToolFixture } from './tool-test-fixtures'\nimport { prepareMutation } from './prepare-mutation'\nimport { executePendingActionConfirm } from './pending-action-executor'\n\nexport type ToolTestStatus = 'pass' | 'fail' | 'skip'\n\nexport interface ToolTestRecord {\n module: string\n tool: string\n isMutation: boolean\n status: ToolTestStatus\n durationMs: number\n reason?: string\n resultPreview?: unknown\n}\n\nexport interface ToolTestReport {\n tenantId: string | null\n organizationId: string | null\n total: number\n passed: number\n failed: number\n skipped: number\n records: ToolTestRecord[]\n}\n\ninterface RawAiToolsModule {\n aiToolConfigEntriesRaw?: { moduleId: string; tools: unknown[] }[]\n aiToolConfigEntries?: { moduleId: string; tools: unknown[] }[]\n}\n\nfunction isToolDefinition(value: unknown): value is AiToolDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.name === 'string' &&\n typeof candidate.description === 'string' &&\n candidate.inputSchema !== undefined &&\n typeof candidate.handler === 'function'\n )\n}\n\n/**\n * Locate `apps/mercato/.mercato/generated/ai-tools.generated.ts` without\n * hardcoding the workspace layout. Searches upward from this file's\n * compiled location; in the monorepo dist this is\n * `packages/ai-assistant/dist/...` so we walk up until we hit a directory\n * containing `apps/mercato/.mercato/generated`.\n */\nfunction findGeneratedAiToolsPath(): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n if (!here) return null\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n // Fallback: cwd-based lookup (when CLI is invoked from apps/mercato).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import `ai-tools.generated.ts` on the fly. Mirrors the approach\n * used by `loadBootstrapData` in `@open-mercato/shared/lib/bootstrap/dynamicLoader`:\n * resolves the `@/` alias to the app root, marks every other package import as\n * external, and emits a sibling `.mjs` we can `import()` from Node. Cached on\n * mtime so repeat runs in the same process don't recompile.\n */\nasync function compileAndImportGenerated(tsPath: string): Promise<RawAiToolsModule> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n const tsExists = fs.existsSync(tsPath)\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n if (needsCompile) {\n const esbuild = await import('esbuild')\n // Transpile-only: don't bundle. Generated registry files only declare an\n // array literal whose entries are static `import(\"\u2026\")` arrow functions \u2014\n // we want those `import()` strings to stay as runtime imports so Node\n // resolves them lazily through the workspace's normal module resolution.\n // Eagerly bundling them pulls Next.js / route handler internals into the\n // .mjs and breaks at runtime (e.g. `next/server` package-exports map).\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Rewrite `@/...` aliases to absolute paths so Node can resolve them.\n const aliasRewritten = tsSource.replace(\n /from\\s+[\"']@\\/([^\"']+)[\"']/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `from ${JSON.stringify(pathToFileURL(candidate).href)}`\n },\n ).replace(\n /import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `import(${JSON.stringify(pathToFileURL(candidate).href)})`\n },\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n return (await import(pathToFileURL(jsPath).href)) as RawAiToolsModule\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many tool handlers delegate to `aiApiOperationRunner`\n * which fails closed when no manifest is registered. Idempotent: registering\n * the same array twice is safe.\n */\nasync function ensureApiRouteManifestsRegistered(generatedDir: string): Promise<void> {\n const tsPath = path.join(generatedDir, 'api-routes.generated.ts')\n if (!fs.existsSync(tsPath)) return\n try {\n const mod = (await compileAndImportGenerated(tsPath)) as Record<string, unknown>\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) {\n console.warn('[tool-test-runner] api-routes.generated.mjs returned no apiRoutes array')\n return\n }\n const registry = await import('@open-mercato/shared/modules/registry')\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.log(\n `[tool-test-runner] Registered ${apiRoutes.length} api-route manifests; getApiRouteManifests().length=${registry.getApiRouteManifests().length}`,\n )\n }\n } catch (error) {\n console.warn(\n '[tool-test-runner] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nasync function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {\n const tsPath = findGeneratedAiToolsPath()\n if (!tsPath) {\n throw new Error(\n 'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',\n )\n }\n await ensureApiRouteManifestsRegistered(path.dirname(tsPath))\n const mod = await compileAndImportGenerated(tsPath)\n const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []\n const result: { moduleId: string; tools: AiToolDefinition[] }[] = []\n for (const entry of entries) {\n if (!entry || typeof entry.moduleId !== 'string') continue\n const tools = Array.isArray(entry.tools)\n ? entry.tools.filter(isToolDefinition)\n : []\n result.push({ moduleId: entry.moduleId, tools })\n }\n return result\n}\n\nasync function pickDefaultTenant(\n container: AwilixContainer,\n): Promise<{ tenantId: string; organizationId: string | null; userId: string | null } | null> {\n try {\n const em = container.resolve<{\n getConnection: () => { execute: (sql: string) => Promise<unknown[]> }\n }>('em') as unknown as {\n getConnection: () => { execute: (sql: string) => Promise<Record<string, unknown>[]> }\n }\n const conn = em.getConnection()\n const tenantRows = await conn.execute(\n `SELECT id FROM tenants WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const tenantId =\n Array.isArray(tenantRows) && tenantRows[0]\n ? String((tenantRows[0] as Record<string, unknown>).id)\n : null\n if (!tenantId) return null\n const escaped = tenantId.replace(/'/g, \"''\")\n const orgRows = await conn.execute(\n `SELECT id FROM organizations WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const organizationId =\n Array.isArray(orgRows) && orgRows[0]\n ? String((orgRows[0] as Record<string, unknown>).id)\n : null\n const userRows = await conn.execute(\n `SELECT id FROM users WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const userId =\n Array.isArray(userRows) && userRows[0]\n ? String((userRows[0] as Record<string, unknown>).id)\n : null\n return { tenantId, organizationId, userId }\n } catch (error) {\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.warn('[tool-test-runner] pickDefaultTenant failed:', error)\n }\n return null\n }\n}\n\nfunction buildSuperAdminContext(\n container: AwilixContainer,\n tenantId: string | null,\n organizationId: string | null,\n userId: string | null,\n): McpToolContext {\n return {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures: ['*'],\n isSuperAdmin: true,\n }\n}\n\nfunction clipPreview(value: unknown): unknown {\n try {\n const json = JSON.stringify(value)\n if (json.length <= 400) return value\n return `${json.slice(0, 400)}\u2026(truncated, ${json.length} bytes)`\n } catch {\n return '[unserializable]'\n }\n}\n\nfunction dummyAgentForTool(tool: AiToolDefinition): AiAgentDefinition {\n // The runner never registers this agent; it's only used as input to\n // `prepareMutation` for shape compatibility. The id namespace is namespaced\n // under `__test__` so it cannot collide with real agents.\n const policy: AiAgentMutationPolicy = 'destructive-confirm-required'\n return {\n id: `__test__.${tool.name}`,\n moduleId: tool.name.split('.')[0] ?? '__test__',\n label: 'Tool Test Runner',\n description: 'Synthetic agent used by the tool test runner',\n systemPrompt: '',\n allowedTools: [tool.name],\n readOnly: false,\n mutationPolicy: policy,\n } as AiAgentDefinition\n}\n\nasync function executeReadTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Mirror the dispatcher: resolve a fresh container per call so EM identity\n // map state is clean between tools.\n const fresh = await createRequestContainer()\n const freshCtx: McpToolContext = { ...ctx, container: fresh, tool }\n void container // keep arg for future use\n return tool.handler(args as never, freshCtx)\n}\n\nasync function executeMutationTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Route through prepareMutation so we assert the approval contract still\n // holds \u2014 the handler must NEVER write directly. We pass a destructive\n // policy so the runtime treats the call as a confirmation candidate and\n // creates a pending-action row.\n const fresh = await createRequestContainer()\n const agent = dummyAgentForTool(tool)\n const userId = ctx.userId ?? '00000000-0000-0000-0000-000000000000'\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent,\n tool,\n toolCallArgs: args,\n conversationId: null,\n mutationPolicyOverride: null,\n },\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId,\n features: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n container: fresh,\n },\n )\n // Exercise the actual handler via the same path the production confirm\n // route uses. Without this, mutation tools whose handler crashes (e.g.\n // missing `ctx.tool` for the API operation runner) would be reported as\n // passing because `prepareMutation` never invokes the handler.\n // `prepareMutation` already enforced `tenantId` is non-null above; cast\n // here for the stricter `PendingActionExecuteContext` shape.\n const confirmTenantId = ctx.tenantId as string\n const confirmContainer = await createRequestContainer()\n const confirmation = await executePendingActionConfirm({\n action: pendingAction,\n agent,\n tool,\n ctx: {\n tenantId: confirmTenantId,\n organizationId: ctx.organizationId,\n userId,\n container: confirmContainer,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n },\n emitEvent: async () => {},\n })\n if (!confirmation.ok) {\n const error = (confirmation.executionResult as { error?: { message?: string } } | undefined)?.error\n const cause = confirmation.cause\n const causeMessage =\n cause instanceof Error\n ? cause.message\n : typeof cause === 'string'\n ? cause\n : null\n const message = error?.message ?? causeMessage ?? 'handler invocation failed'\n throw new Error(message)\n }\n return {\n status: 'pending-confirmation',\n pendingActionId: pendingAction.id,\n expiresAt: pendingAction.expiresAt.toISOString(),\n uiPartType: (uiPart as { type?: string } | undefined)?.type ?? null,\n handlerExecuted: true,\n }\n}\n\nasync function resolveFixtureInput(\n fixture: ToolFixture,\n resolved: Map<string, AiToolDefinition>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<{ ok: true; input: Record<string, unknown> } | { ok: false; reason: string }> {\n if (fixture.input) return { ok: true, input: { ...fixture.input } }\n if (!fixture.idFrom) {\n return { ok: false, reason: 'fixture has neither input nor idFrom' }\n }\n const sourceTool = resolved.get(fixture.idFrom)\n if (!sourceTool) {\n return { ok: false, reason: `idFrom tool not found: ${fixture.idFrom}` }\n }\n const sourceFixture = getFixture(fixture.idFrom)\n if (!sourceFixture?.input) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} has no static input fixture`,\n }\n }\n let listResult: unknown\n try {\n listResult = await executeReadTool(\n sourceTool,\n { ...sourceFixture.input },\n container,\n ctx,\n )\n } catch (error) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} threw: ${\n error instanceof Error ? error.message : String(error)\n }`,\n }\n }\n const records = extractRecords(listResult)\n if (!records.length) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} returned no records`,\n }\n }\n const idField = 'id'\n const id = (records[0] as Record<string, unknown>)[idField]\n if (typeof id !== 'string' || !id) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} first record has no string id`,\n }\n }\n const bindAs = fixture.bindAs ?? 'id'\n return {\n ok: true,\n input: { [bindAs]: id, ...(fixture.extra ?? {}) },\n }\n}\n\nfunction extractRecords(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n const candidate = value as Record<string, unknown>\n for (const key of ['records', 'items', 'people', 'companies', 'deals', 'results', 'data']) {\n const arr = candidate[key]\n if (Array.isArray(arr)) return arr\n }\n return []\n}\n\nexport interface RunToolTestsOptions {\n tenantId?: string | null\n organizationId?: string | null\n userId?: string | null\n moduleFilter?: string | null\n includeMutations?: boolean\n}\n\nexport async function runToolTests(\n options: RunToolTestsOptions = {},\n): Promise<ToolTestReport> {\n const includeMutations = options.includeMutations ?? true\n const container = await createRequestContainer()\n let tenantId: string | null = options.tenantId ?? null\n let organizationId: string | null = options.organizationId ?? null\n let userId: string | null = options.userId ?? null\n if (!tenantId) {\n const picked = await pickDefaultTenant(container)\n if (picked) {\n tenantId = picked.tenantId\n organizationId = picked.organizationId\n userId = userId ?? picked.userId\n }\n }\n const ctx = buildSuperAdminContext(container, tenantId, organizationId, userId)\n const grouped = await loadGeneratedTools()\n\n const flat: { moduleId: string; tool: AiToolDefinition }[] = []\n for (const group of grouped) {\n if (options.moduleFilter && group.moduleId !== options.moduleFilter) continue\n for (const tool of group.tools) flat.push({ moduleId: group.moduleId, tool })\n }\n const resolved = new Map<string, AiToolDefinition>()\n for (const { tool } of flat) resolved.set(tool.name, tool)\n\n const records: ToolTestRecord[] = []\n for (const { moduleId, tool } of flat) {\n const start = Date.now()\n const isMutation = tool.isMutation === true\n const fixture = getFixture(tool.name)\n if (!fixture) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'no fixture',\n })\n continue\n }\n if (fixture.skip) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: fixture.note ?? 'skip-by-fixture',\n })\n continue\n }\n if (isMutation && !includeMutations) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'mutations excluded',\n })\n continue\n }\n const inputResult = await resolveFixtureInput(fixture, resolved, container, ctx)\n if (!inputResult.ok) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: inputResult.reason,\n })\n continue\n }\n try {\n const result = isMutation\n ? await executeMutationTool(tool, inputResult.input, container, ctx)\n : await executeReadTool(tool, inputResult.input, container, ctx)\n // Result must be JSON-serializable.\n JSON.stringify(result)\n // Mutation tools must return a pending-confirmation envelope.\n if (\n isMutation &&\n (typeof result !== 'object' ||\n result === null ||\n (result as Record<string, unknown>).status !== 'pending-confirmation')\n ) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: 'mutation tool did not route through prepareMutation (no pending-confirmation envelope)',\n resultPreview: clipPreview(result),\n })\n continue\n }\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'pass',\n durationMs: Date.now() - start,\n resultPreview: clipPreview(result),\n })\n } catch (error) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: error instanceof Error ? error.message : String(error),\n })\n }\n }\n const passed = records.filter((r) => r.status === 'pass').length\n const failed = records.filter((r) => r.status === 'fail').length\n const skipped = records.filter((r) => r.status === 'skip').length\n return {\n tenantId,\n organizationId,\n total: records.length,\n passed,\n failed,\n skipped,\n records,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * In-process AI tool test runner.\n *\n * Iterates every tool registered in `ai-tools.generated.ts`, invokes the\n * handler against a super-admin tenant context, and returns a structured\n * report. Used by the `mercato ai_assistant test-tools` CLI subcommand and\n * by `.ai/qa/tests/integration/TC-INT-AI-TOOLS.spec.ts`.\n *\n * Safety posture:\n * - No HTTP exposure \u2014 runs only inside the Node process driving the CLI.\n * - Mutation tools are exercised through `prepareMutation` and we assert a\n * pending-action envelope is returned. The pending-action row is created\n * in `ai_pending_actions` but never confirmed, so no real write happens.\n * - Tools without an explicit fixture entry are skipped with reason\n * `'no fixture'` rather than failing.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport type { AwilixContainer } from 'awilix'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { McpToolContext, AiToolDefinition } from './types'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport { getFixture, type ToolFixture } from './tool-test-fixtures'\nimport { prepareMutation } from './prepare-mutation'\nimport { executePendingActionConfirm } from './pending-action-executor'\n\nexport type ToolTestStatus = 'pass' | 'fail' | 'skip'\n\nexport interface ToolTestRecord {\n module: string\n tool: string\n isMutation: boolean\n status: ToolTestStatus\n durationMs: number\n reason?: string\n resultPreview?: unknown\n}\n\nexport interface ToolTestReport {\n tenantId: string | null\n organizationId: string | null\n total: number\n passed: number\n failed: number\n skipped: number\n records: ToolTestRecord[]\n}\n\ninterface RawAiToolsModule {\n aiToolConfigEntriesRaw?: { moduleId: string; tools: unknown[] }[]\n aiToolConfigEntries?: { moduleId: string; tools: unknown[] }[]\n}\n\nfunction isToolDefinition(value: unknown): value is AiToolDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.name === 'string' &&\n typeof candidate.description === 'string' &&\n candidate.inputSchema !== undefined &&\n typeof candidate.handler === 'function'\n )\n}\n\n/**\n * Locate `apps/mercato/.mercato/generated/ai-tools.generated.ts` without\n * hardcoding the workspace layout. Searches upward from this file's\n * compiled location; in the monorepo dist this is\n * `packages/ai-assistant/dist/...` so we walk up until we hit a directory\n * containing `apps/mercato/.mercato/generated`.\n */\nfunction findGeneratedAiToolsPath(): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n if (!here) return null\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n // Fallback: cwd-based lookup (when CLI is invoked from apps/mercato).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', 'ai-tools.generated.ts')\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import `ai-tools.generated.ts` on the fly. Mirrors the approach\n * used by `loadBootstrapData` in `@open-mercato/shared/lib/bootstrap/dynamicLoader`:\n * resolves the `@/` alias to the app root, marks every other package import as\n * external, and emits a sibling `.mjs` we can `import()` from Node. Cached on\n * mtime so repeat runs in the same process don't recompile.\n */\nasync function compileAndImportGenerated(tsPath: string): Promise<RawAiToolsModule> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n const tsExists = fs.existsSync(tsPath)\n if (!tsExists) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n if (needsCompile) {\n const esbuild = await import('esbuild')\n // Transpile-only: don't bundle. Generated registry files only declare an\n // array literal whose entries are static `import(\"\u2026\")` arrow functions \u2014\n // we want those `import()` strings to stay as runtime imports so Node\n // resolves them lazily through the workspace's normal module resolution.\n // Eagerly bundling them pulls Next.js / route handler internals into the\n // .mjs and breaks at runtime (e.g. `next/server` package-exports map).\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Rewrite `@/...` aliases to absolute paths so Node can resolve them.\n const aliasRewritten = tsSource.replace(\n /from\\s+[\"']@\\/([^\"']+)[\"']/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `from ${JSON.stringify(pathToFileURL(candidate).href)}`\n },\n ).replace(\n /import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g,\n (_match, p1: string) => {\n const target = path.join(appRoot, p1)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return `import(${JSON.stringify(pathToFileURL(candidate).href)})`\n },\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n return (await import(pathToFileURL(jsPath).href)) as RawAiToolsModule\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many tool handlers delegate to `aiApiOperationRunner`\n * which fails closed when no manifest is registered. Idempotent: registering\n * the same array twice is safe.\n */\nasync function ensureApiRouteManifestsRegistered(generatedDir: string): Promise<void> {\n const tsPath = path.join(generatedDir, 'api-routes.generated.ts')\n if (!fs.existsSync(tsPath)) return\n try {\n const mod = (await compileAndImportGenerated(tsPath)) as Record<string, unknown>\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) {\n console.warn('[tool-test-runner] api-routes.generated.mjs returned no apiRoutes array')\n return\n }\n const registry = await import('@open-mercato/shared/modules/registry')\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.log(\n `[tool-test-runner] Registered ${apiRoutes.length} api-route manifests; getApiRouteManifests().length=${registry.getApiRouteManifests().length}`,\n )\n }\n } catch (error) {\n console.warn(\n '[tool-test-runner] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nasync function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {\n const tsPath = findGeneratedAiToolsPath()\n if (!tsPath) {\n throw new Error(\n 'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',\n )\n }\n await ensureApiRouteManifestsRegistered(path.dirname(tsPath))\n const mod = await compileAndImportGenerated(tsPath)\n const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []\n const result: { moduleId: string; tools: AiToolDefinition[] }[] = []\n for (const entry of entries) {\n if (!entry || typeof entry.moduleId !== 'string') continue\n const tools = Array.isArray(entry.tools)\n ? entry.tools.filter(isToolDefinition)\n : []\n result.push({ moduleId: entry.moduleId, tools })\n }\n return result\n}\n\nexport async function pickDefaultTenant(\n container: AwilixContainer,\n): Promise<{ tenantId: string; organizationId: string | null; userId: string | null } | null> {\n try {\n const em = container.resolve<{\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }>('em') as unknown as {\n getConnection: () => {\n execute: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>\n }\n }\n const conn = em.getConnection()\n const tenantRows = await conn.execute(\n `SELECT id FROM tenants WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n )\n const tenantId =\n Array.isArray(tenantRows) && tenantRows[0]\n ? String((tenantRows[0] as Record<string, unknown>).id)\n : null\n if (!tenantId) return null\n const orgRows = await conn.execute(\n `SELECT id FROM organizations WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const organizationId =\n Array.isArray(orgRows) && orgRows[0]\n ? String((orgRows[0] as Record<string, unknown>).id)\n : null\n const userRows = await conn.execute(\n `SELECT id FROM users WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,\n [tenantId],\n )\n const userId =\n Array.isArray(userRows) && userRows[0]\n ? String((userRows[0] as Record<string, unknown>).id)\n : null\n return { tenantId, organizationId, userId }\n } catch (error) {\n if (process.env.OM_TOOL_TEST_DEBUG === '1') {\n console.warn('[tool-test-runner] pickDefaultTenant failed:', error)\n }\n return null\n }\n}\n\nfunction buildSuperAdminContext(\n container: AwilixContainer,\n tenantId: string | null,\n organizationId: string | null,\n userId: string | null,\n): McpToolContext {\n return {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures: ['*'],\n isSuperAdmin: true,\n }\n}\n\nfunction clipPreview(value: unknown): unknown {\n try {\n const json = JSON.stringify(value)\n if (json.length <= 400) return value\n return `${json.slice(0, 400)}\u2026(truncated, ${json.length} bytes)`\n } catch {\n return '[unserializable]'\n }\n}\n\nfunction dummyAgentForTool(tool: AiToolDefinition): AiAgentDefinition {\n // The runner never registers this agent; it's only used as input to\n // `prepareMutation` for shape compatibility. The id namespace is namespaced\n // under `__test__` so it cannot collide with real agents.\n const policy: AiAgentMutationPolicy = 'destructive-confirm-required'\n return {\n id: `__test__.${tool.name}`,\n moduleId: tool.name.split('.')[0] ?? '__test__',\n label: 'Tool Test Runner',\n description: 'Synthetic agent used by the tool test runner',\n systemPrompt: '',\n allowedTools: [tool.name],\n readOnly: false,\n mutationPolicy: policy,\n } as AiAgentDefinition\n}\n\nasync function executeReadTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Mirror the dispatcher: resolve a fresh container per call so EM identity\n // map state is clean between tools.\n const fresh = await createRequestContainer()\n const freshCtx: McpToolContext = { ...ctx, container: fresh, tool }\n void container // keep arg for future use\n return tool.handler(args as never, freshCtx)\n}\n\nasync function executeMutationTool(\n tool: AiToolDefinition,\n args: Record<string, unknown>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<unknown> {\n // Route through prepareMutation so we assert the approval contract still\n // holds \u2014 the handler must NEVER write directly. We pass a destructive\n // policy so the runtime treats the call as a confirmation candidate and\n // creates a pending-action row.\n const fresh = await createRequestContainer()\n const agent = dummyAgentForTool(tool)\n const userId = ctx.userId ?? '00000000-0000-0000-0000-000000000000'\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent,\n tool,\n toolCallArgs: args,\n conversationId: null,\n mutationPolicyOverride: null,\n },\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId,\n features: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n container: fresh,\n },\n )\n // Exercise the actual handler via the same path the production confirm\n // route uses. Without this, mutation tools whose handler crashes (e.g.\n // missing `ctx.tool` for the API operation runner) would be reported as\n // passing because `prepareMutation` never invokes the handler.\n // `prepareMutation` already enforced `tenantId` is non-null above; cast\n // here for the stricter `PendingActionExecuteContext` shape.\n const confirmTenantId = ctx.tenantId as string\n const confirmContainer = await createRequestContainer()\n const confirmation = await executePendingActionConfirm({\n action: pendingAction,\n agent,\n tool,\n ctx: {\n tenantId: confirmTenantId,\n organizationId: ctx.organizationId,\n userId,\n container: confirmContainer,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n },\n emitEvent: async () => {},\n })\n if (!confirmation.ok) {\n const error = (confirmation.executionResult as { error?: { message?: string } } | undefined)?.error\n const cause = confirmation.cause\n const causeMessage =\n cause instanceof Error\n ? cause.message\n : typeof cause === 'string'\n ? cause\n : null\n const message = error?.message ?? causeMessage ?? 'handler invocation failed'\n throw new Error(message)\n }\n return {\n status: 'pending-confirmation',\n pendingActionId: pendingAction.id,\n expiresAt: pendingAction.expiresAt.toISOString(),\n uiPartType: (uiPart as { type?: string } | undefined)?.type ?? null,\n handlerExecuted: true,\n }\n}\n\nasync function resolveFixtureInput(\n fixture: ToolFixture,\n resolved: Map<string, AiToolDefinition>,\n container: AwilixContainer,\n ctx: McpToolContext,\n): Promise<{ ok: true; input: Record<string, unknown> } | { ok: false; reason: string }> {\n if (fixture.input) return { ok: true, input: { ...fixture.input } }\n if (!fixture.idFrom) {\n return { ok: false, reason: 'fixture has neither input nor idFrom' }\n }\n const sourceTool = resolved.get(fixture.idFrom)\n if (!sourceTool) {\n return { ok: false, reason: `idFrom tool not found: ${fixture.idFrom}` }\n }\n const sourceFixture = getFixture(fixture.idFrom)\n if (!sourceFixture?.input) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} has no static input fixture`,\n }\n }\n let listResult: unknown\n try {\n listResult = await executeReadTool(\n sourceTool,\n { ...sourceFixture.input },\n container,\n ctx,\n )\n } catch (error) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} threw: ${\n error instanceof Error ? error.message : String(error)\n }`,\n }\n }\n const records = extractRecords(listResult)\n if (!records.length) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} returned no records`,\n }\n }\n const idField = 'id'\n const id = (records[0] as Record<string, unknown>)[idField]\n if (typeof id !== 'string' || !id) {\n return {\n ok: false,\n reason: `idFrom source ${fixture.idFrom} first record has no string id`,\n }\n }\n const bindAs = fixture.bindAs ?? 'id'\n return {\n ok: true,\n input: { [bindAs]: id, ...(fixture.extra ?? {}) },\n }\n}\n\nfunction extractRecords(value: unknown): unknown[] {\n if (!value || typeof value !== 'object') return []\n const candidate = value as Record<string, unknown>\n for (const key of ['records', 'items', 'people', 'companies', 'deals', 'results', 'data']) {\n const arr = candidate[key]\n if (Array.isArray(arr)) return arr\n }\n return []\n}\n\nexport interface RunToolTestsOptions {\n tenantId?: string | null\n organizationId?: string | null\n userId?: string | null\n moduleFilter?: string | null\n includeMutations?: boolean\n}\n\nexport async function runToolTests(\n options: RunToolTestsOptions = {},\n): Promise<ToolTestReport> {\n const includeMutations = options.includeMutations ?? true\n const container = await createRequestContainer()\n let tenantId: string | null = options.tenantId ?? null\n let organizationId: string | null = options.organizationId ?? null\n let userId: string | null = options.userId ?? null\n if (!tenantId) {\n const picked = await pickDefaultTenant(container)\n if (picked) {\n tenantId = picked.tenantId\n organizationId = picked.organizationId\n userId = userId ?? picked.userId\n }\n }\n const ctx = buildSuperAdminContext(container, tenantId, organizationId, userId)\n const grouped = await loadGeneratedTools()\n\n const flat: { moduleId: string; tool: AiToolDefinition }[] = []\n for (const group of grouped) {\n if (options.moduleFilter && group.moduleId !== options.moduleFilter) continue\n for (const tool of group.tools) flat.push({ moduleId: group.moduleId, tool })\n }\n const resolved = new Map<string, AiToolDefinition>()\n for (const { tool } of flat) resolved.set(tool.name, tool)\n\n const records: ToolTestRecord[] = []\n for (const { moduleId, tool } of flat) {\n const start = Date.now()\n const isMutation = tool.isMutation === true\n const fixture = getFixture(tool.name)\n if (!fixture) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'no fixture',\n })\n continue\n }\n if (fixture.skip) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: fixture.note ?? 'skip-by-fixture',\n })\n continue\n }\n if (isMutation && !includeMutations) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: 'mutations excluded',\n })\n continue\n }\n const inputResult = await resolveFixtureInput(fixture, resolved, container, ctx)\n if (!inputResult.ok) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'skip',\n durationMs: Date.now() - start,\n reason: inputResult.reason,\n })\n continue\n }\n try {\n const result = isMutation\n ? await executeMutationTool(tool, inputResult.input, container, ctx)\n : await executeReadTool(tool, inputResult.input, container, ctx)\n // Result must be JSON-serializable.\n JSON.stringify(result)\n // Mutation tools must return a pending-confirmation envelope.\n if (\n isMutation &&\n (typeof result !== 'object' ||\n result === null ||\n (result as Record<string, unknown>).status !== 'pending-confirmation')\n ) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: 'mutation tool did not route through prepareMutation (no pending-confirmation envelope)',\n resultPreview: clipPreview(result),\n })\n continue\n }\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'pass',\n durationMs: Date.now() - start,\n resultPreview: clipPreview(result),\n })\n } catch (error) {\n records.push({\n module: moduleId,\n tool: tool.name,\n isMutation,\n status: 'fail',\n durationMs: Date.now() - start,\n reason: error instanceof Error ? error.message : String(error),\n })\n }\n }\n const passed = records.filter((r) => r.status === 'pass').length\n const failed = records.filter((r) => r.status === 'fail').length\n const skipped = records.filter((r) => r.status === 'skip').length\n return {\n tenantId,\n organizationId,\n total: records.length,\n passed,\n failed,\n skipped,\n records,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.4-develop.4382.1.6b4f656b77",
3
+ "version": "0.6.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -87,9 +87,9 @@
87
87
  "dependencies": {
88
88
  "@ai-sdk/anthropic": "^3.0.81",
89
89
  "@ai-sdk/google": "^3.0.80",
90
- "@ai-sdk/openai": "^3.0.67",
90
+ "@ai-sdk/openai": "^3.0.68",
91
91
  "@modelcontextprotocol/sdk": "^1.29.0",
92
- "ai": "^6.0.194",
92
+ "ai": "^6.0.197",
93
93
  "cmdk": "^1.0.0",
94
94
  "framer-motion": "^12.40.0",
95
95
  "isolated-vm": "^7.0.0",
@@ -98,17 +98,17 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.4-develop.4382.1.6b4f656b77",
102
- "@open-mercato/ui": "0.6.4-develop.4382.1.6b4f656b77",
101
+ "@open-mercato/shared": "0.6.4",
102
+ "@open-mercato/ui": "0.6.4",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.4-develop.4382.1.6b4f656b77",
109
- "@open-mercato/shared": "0.6.4-develop.4382.1.6b4f656b77",
110
- "@open-mercato/ui": "0.6.4-develop.4382.1.6b4f656b77",
111
- "@types/react": "^19.2.16",
108
+ "@open-mercato/cli": "0.6.4",
109
+ "@open-mercato/shared": "0.6.4",
110
+ "@open-mercato/ui": "0.6.4",
111
+ "@types/react": "^19.2.17",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
114
114
  "react-dom": "19.2.7",
@@ -122,6 +122,5 @@
122
122
  "type": "git",
123
123
  "url": "https://github.com/open-mercato/open-mercato",
124
124
  "directory": "packages/ai-assistant"
125
- },
126
- "stableVersion": "0.6.3"
125
+ }
127
126
  }
@@ -0,0 +1,209 @@
1
+ import { test, expect, request as playwrightRequest } from '@playwright/test';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';
4
+ import {
5
+ createRoleFixture,
6
+ deleteRoleIfExists,
7
+ createUserFixture,
8
+ deleteUserIfExists,
9
+ setUserAclVisibility,
10
+ } from '@open-mercato/core/helpers/integration/authFixtures';
11
+ import { deleteUserAclInDb } from '@open-mercato/core/helpers/integration/dbFixtures';
12
+ import { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';
13
+ import {
14
+ seedPendingActionInDb,
15
+ deletePendingActionInDb,
16
+ type SeedPendingActionInput,
17
+ } from './helpers/aiAssistantFixtures';
18
+
19
+ /**
20
+ * TC-AI-ACTIONS-PENDING-004 — Pending action confirm/cancel (mutation-approval gate).
21
+ * Source: GitHub issue #2495.
22
+ *
23
+ * Surfaces under test:
24
+ * - /api/ai_assistant/ai/actions/{id} (GET)
25
+ * - /api/ai_assistant/ai/actions/{id}/confirm (POST)
26
+ * - /api/ai_assistant/ai/actions/{id}/cancel (POST)
27
+ *
28
+ * Contract notes verified against the route handlers:
29
+ * - there is NO public route to CREATE a pending action (it is born only from
30
+ * the internal `prepareMutation` path), so rows are seeded directly via SQL.
31
+ * - GET strips `normalizedInput`, `createdByUserId`, `idempotencyKey`,
32
+ * `tenantId`, `organizationId` from the client serialization.
33
+ * - confirm/cancel are idempotent at the route layer; an already-terminal row
34
+ * short-circuits (200) rather than throwing.
35
+ * - cancelling an expired row -> 409 `expired`; confirming a cancelled row ->
36
+ * 409 `invalid_status`; unknown id -> 404 `pending_action_not_found`.
37
+ * - all three require `ai_assistant.view`.
38
+ *
39
+ * The confirm HAPPY path is exercised via the terminal short-circuit (a seeded
40
+ * `confirmed` row) so the test stays deterministic and provider-free — driving a
41
+ * real `pending -> confirmed` transition would execute a live tool mutation that
42
+ * depends on the agent/tool registry and an LLM-proposed payload.
43
+ */
44
+
45
+ const ACTIONS = '/api/ai_assistant/ai/actions';
46
+
47
+ interface SerializedPendingAction {
48
+ id: string;
49
+ agentId: string;
50
+ toolName: string;
51
+ status: string;
52
+ createdAt: string;
53
+ expiresAt: string;
54
+ executionResult: unknown;
55
+ normalizedInput?: unknown;
56
+ createdByUserId?: unknown;
57
+ idempotencyKey?: unknown;
58
+ tenantId?: unknown;
59
+ organizationId?: unknown;
60
+ }
61
+
62
+ test.describe('TC-AI-ACTIONS-PENDING-004: Pending action confirm/cancel', () => {
63
+ test('GET serialization, cancel + confirm idempotency, and state-machine guards', async ({ request }) => {
64
+ test.slow();
65
+ const adminToken = await getAuthToken(request, 'admin');
66
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
67
+ const seededIds: string[] = [];
68
+ const seed = async (overrides: Partial<SeedPendingActionInput>) => {
69
+ const row = await seedPendingActionInDb({
70
+ tenantId,
71
+ organizationId: organizationId || null,
72
+ createdByUserId: adminId,
73
+ ...overrides,
74
+ });
75
+ seededIds.push(row.id);
76
+ return row;
77
+ };
78
+
79
+ try {
80
+ // GET returns the client serialization with privileged fields stripped.
81
+ const pending = await seed({ status: 'pending' });
82
+ const getRes = await apiRequest(request, 'GET', `${ACTIONS}/${pending.id}`, { token: adminToken });
83
+ expect(getRes.status()).toBe(200);
84
+ const body = await readJsonSafe<SerializedPendingAction>(getRes);
85
+ expect(body?.id).toBe(pending.id);
86
+ expect(body?.status).toBe('pending');
87
+ expect(typeof body?.agentId).toBe('string');
88
+ expect(typeof body?.expiresAt).toBe('string');
89
+ expect(body?.normalizedInput, 'normalizedInput is stripped').toBeUndefined();
90
+ expect(body?.createdByUserId, 'createdByUserId is stripped').toBeUndefined();
91
+ expect(body?.idempotencyKey, 'idempotencyKey is stripped').toBeUndefined();
92
+ expect(body?.tenantId, 'tenantId is stripped').toBeUndefined();
93
+ expect(body?.organizationId, 'organizationId is stripped').toBeUndefined();
94
+
95
+ // Cancel happy path + idempotency.
96
+ const cancelable = await seed({ status: 'pending' });
97
+ const cancel = await apiRequest(request, 'POST', `${ACTIONS}/${cancelable.id}/cancel`, {
98
+ token: adminToken,
99
+ data: { reason: 'user_rejected' },
100
+ });
101
+ expect(cancel.status()).toBe(200);
102
+ const cancelBody = await readJsonSafe<{ ok: boolean; pendingAction: SerializedPendingAction }>(cancel);
103
+ expect(cancelBody?.ok).toBe(true);
104
+ expect(cancelBody?.pendingAction.status).toBe('cancelled');
105
+
106
+ const cancelAgain = await apiRequest(request, 'POST', `${ACTIONS}/${cancelable.id}/cancel`, {
107
+ token: adminToken,
108
+ data: {},
109
+ });
110
+ expect(cancelAgain.status(), 'cancel is idempotent').toBe(200);
111
+ expect((await readJsonSafe<{ pendingAction: SerializedPendingAction }>(cancelAgain))?.pendingAction.status).toBe(
112
+ 'cancelled',
113
+ );
114
+
115
+ // Confirm happy path via the terminal short-circuit (seeded confirmed row).
116
+ const confirmed = await seed({ status: 'confirmed', executionResult: { recordId: 'seeded-record' } });
117
+ const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${confirmed.id}/confirm`, {
118
+ token: adminToken,
119
+ data: {},
120
+ });
121
+ expect(confirm.status()).toBe(200);
122
+ const confirmBody = await readJsonSafe<{ ok: boolean; pendingAction: SerializedPendingAction; mutationResult: unknown }>(
123
+ confirm,
124
+ );
125
+ expect(confirmBody?.ok).toBe(true);
126
+ expect(confirmBody?.pendingAction.status).toBe('confirmed');
127
+ expect(confirmBody?.mutationResult).toEqual({ recordId: 'seeded-record' });
128
+
129
+ // Confirming a cancelled row -> 409 invalid_status.
130
+ const cancelledRow = await seed({ status: 'cancelled' });
131
+ const confirmCancelled = await apiRequest(request, 'POST', `${ACTIONS}/${cancelledRow.id}/confirm`, {
132
+ token: adminToken,
133
+ data: {},
134
+ });
135
+ expect(confirmCancelled.status()).toBe(409);
136
+ expect((await readJsonSafe<{ code?: string }>(confirmCancelled))?.code).toBe('invalid_status');
137
+
138
+ // Cancelling an expired row -> 409 expired.
139
+ const expiredRow = await seed({ status: 'pending', expiresInMinutes: -10 });
140
+ const cancelExpired = await apiRequest(request, 'POST', `${ACTIONS}/${expiredRow.id}/cancel`, {
141
+ token: adminToken,
142
+ data: {},
143
+ });
144
+ expect(cancelExpired.status()).toBe(409);
145
+ expect((await readJsonSafe<{ code?: string }>(cancelExpired))?.code).toBe('expired');
146
+
147
+ // Unknown id -> 404; over-long id -> 400 validation_error.
148
+ const notFound = await apiRequest(request, 'GET', `${ACTIONS}/${randomUUID()}`, { token: adminToken });
149
+ expect(notFound.status()).toBe(404);
150
+ expect((await readJsonSafe<{ code?: string }>(notFound))?.code).toBe('pending_action_not_found');
151
+
152
+ const tooLong = await apiRequest(request, 'GET', `${ACTIONS}/${'a'.repeat(200)}`, { token: adminToken });
153
+ expect(tooLong.status()).toBe(400);
154
+ expect((await readJsonSafe<{ code?: string }>(tooLong))?.code).toBe('validation_error');
155
+ } finally {
156
+ for (const id of seededIds) {
157
+ await deletePendingActionInDb(id).catch(() => undefined);
158
+ }
159
+ }
160
+ });
161
+
162
+ test('auth gates: unauthenticated 401 and missing ai_assistant.view 403', async ({ request, baseURL }) => {
163
+ test.slow();
164
+ const adminToken = await getAuthToken(request, 'admin');
165
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
166
+ const stamp = randomUUID().slice(0, 8);
167
+ const password = 'Secret123!';
168
+
169
+ let seededId: string | null = null;
170
+ let roleId: string | null = null;
171
+ let userId: string | null = null;
172
+ try {
173
+ const row = await seedPendingActionInDb({
174
+ tenantId,
175
+ organizationId: organizationId || null,
176
+ createdByUserId: adminId,
177
+ status: 'pending',
178
+ });
179
+ seededId = row.id;
180
+
181
+ // Unauthenticated GET -> 401 (fresh context, no session cookie).
182
+ const anon = await playwrightRequest.newContext({ baseURL });
183
+ try {
184
+ const res = await anon.fetch(`${ACTIONS}/${seededId}`, { method: 'GET' });
185
+ expect(res.status()).toBe(401);
186
+ } finally {
187
+ await anon.dispose();
188
+ }
189
+
190
+ // Authenticated user lacking ai_assistant.view -> 403.
191
+ roleId = await createRoleFixture(request, adminToken, { name: `IT Pending Role ${stamp}` });
192
+ userId = await createUserFixture(request, adminToken, {
193
+ email: `it-pending-${stamp}@example.com`,
194
+ password,
195
+ organizationId,
196
+ roles: [roleId],
197
+ });
198
+ await setUserAclVisibility(request, adminToken, { userId, features: [], organizations: null });
199
+ const viewlessToken = await getAuthToken(request, `it-pending-${stamp}@example.com`, password);
200
+ const denied = await apiRequest(request, 'GET', `${ACTIONS}/${seededId}`, { token: viewlessToken });
201
+ expect(denied.status(), 'caller without ai_assistant.view is 403').toBe(403);
202
+ } finally {
203
+ await deletePendingActionInDb(seededId).catch(() => undefined);
204
+ await deleteUserAclInDb(userId ?? '').catch(() => undefined);
205
+ await deleteUserIfExists(request, adminToken, userId);
206
+ await deleteRoleIfExists(request, adminToken, roleId);
207
+ }
208
+ });
209
+ });
@@ -372,10 +372,7 @@ test.describe('TC-AI-AGENT-LOOP-001–006: agentic loop controls', () => {
372
372
  test.setTimeout(60_000);
373
373
  await login(page, 'superadmin');
374
374
 
375
- let capturedAgentsPayload: typeof agentsPayload | null = null;
376
-
377
375
  await page.route('**/api/ai_assistant/ai/agents', async (route) => {
378
- capturedAgentsPayload = agentsPayload;
379
376
  await route.fulfill({
380
377
  status: 200,
381
378
  contentType: 'application/json',
@@ -383,31 +380,22 @@ test.describe('TC-AI-AGENT-LOOP-001–006: agentic loop controls', () => {
383
380
  });
384
381
  });
385
382
 
386
- // The agents request fires from a post-hydration `useQuery`, not from
387
- // navigation, so the route handler that sets `capturedAgentsPayload` runs
388
- // after `goto` resolves. Await the response deterministically instead of
389
- // asserting the captured payload immediately (which races hydration).
390
- const agentsResponsePromise = page.waitForResponse(
391
- (response) =>
392
- response.url().includes('/api/ai_assistant/ai/agents') &&
393
- response.request().method() === 'GET',
394
- );
395
-
396
- await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
397
- await agentsResponsePromise;
383
+ const capturedAgentsPayload = await page.evaluate(async () => {
384
+ const response = await fetch('/api/ai_assistant/ai/agents');
385
+ return (await response.json()) as typeof agentsPayload;
386
+ });
398
387
 
399
388
  // Verify that the mocked payload carrying executionEngine was served.
400
389
  // This asserts the agents API contract for Phase 5:
401
390
  // - tool-loop-agent entries include `executionEngine: 'tool-loop-agent'`
402
391
  // - stream-text entries either omit it or set `executionEngine: 'stream-text'`
403
- expect(capturedAgentsPayload).not.toBeNull();
404
- const toolLoopEntry = capturedAgentsPayload!.agents.find(
392
+ const toolLoopEntry = capturedAgentsPayload.agents.find(
405
393
  (a: (typeof agentsPayload)['agents'][number]) => a.id === 'catalog.tool_loop_assistant',
406
394
  );
407
395
  expect(toolLoopEntry).toBeDefined();
408
396
  expect(toolLoopEntry?.executionEngine).toBe('tool-loop-agent');
409
397
 
410
- const streamTextEntry = capturedAgentsPayload!.agents.find(
398
+ const streamTextEntry = capturedAgentsPayload.agents.find(
411
399
  (a: (typeof agentsPayload)['agents'][number]) => a.id === 'customers.account_assistant',
412
400
  );
413
401
  expect(streamTextEntry).toBeDefined();