@open-mercato/ai-assistant 0.6.4-develop.4371.1.8f3030407e → 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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -2
- 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-LOOP-001-006.spec.js +4 -8
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +2 -2
- 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-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- 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-TOKEN-USAGE-001-005.spec.js +6 -5
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +2 -2
- 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/dist/modules/ai_assistant/lib/auth.js +2 -11
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +17 -20
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +3 -2
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/log-redaction.js +25 -0
- package/dist/modules/ai_assistant/lib/log-redaction.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +5 -3
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +2 -2
- package/package.json +10 -11
- 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-LOOP-001-006.spec.ts +6 -18
- 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-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -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-TOKEN-USAGE-001-005.spec.ts +7 -5
- 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
- package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +27 -0
- package/src/modules/ai_assistant/lib/__tests__/codemode-tools.test.ts +128 -0
- package/src/modules/ai_assistant/lib/__tests__/log-redaction.test.ts +65 -0
- package/src/modules/ai_assistant/lib/__tests__/tool-test-runner-pick-default-tenant.test.ts +70 -0
- package/src/modules/ai_assistant/lib/auth.ts +9 -15
- package/src/modules/ai_assistant/lib/codemode-tools.ts +21 -29
- package/src/modules/ai_assistant/lib/http-server.ts +3 -2
- package/src/modules/ai_assistant/lib/log-redaction.ts +41 -0
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +11 -6
package/dist/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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 {
|
|
12
|
+
createOrganizationInDb,
|
|
13
|
+
deleteOrganizationInDb,
|
|
14
|
+
deleteUserAclInDb
|
|
15
|
+
} from "@open-mercato/core/helpers/integration/dbFixtures";
|
|
16
|
+
import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
17
|
+
import { deleteConversationCascadeInDb } from "./helpers/aiAssistantFixtures.js";
|
|
18
|
+
const CONVERSATIONS = "/api/ai_assistant/ai/conversations";
|
|
19
|
+
test.describe("TC-AI-CONVERSATIONS-001: Conversation CRUD lifecycle + org scoping", () => {
|
|
20
|
+
test("create (idempotent) -> list -> patch -> soft-delete hides from list", async ({ request }) => {
|
|
21
|
+
test.slow();
|
|
22
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
23
|
+
const { tenantId } = getTokenScope(adminToken);
|
|
24
|
+
const agentId = `it_conv.agent_${randomUUID().slice(0, 8)}`;
|
|
25
|
+
const conversationId = `it-conv-${randomUUID()}`;
|
|
26
|
+
try {
|
|
27
|
+
const createRes = await apiRequest(request, "POST", CONVERSATIONS, {
|
|
28
|
+
token: adminToken,
|
|
29
|
+
data: { agentId, conversationId, title: "Original title" }
|
|
30
|
+
});
|
|
31
|
+
expect(createRes.status(), "create returns 201").toBe(201);
|
|
32
|
+
const created = await readJsonSafe(createRes);
|
|
33
|
+
expect(created?.conversationId).toBe(conversationId);
|
|
34
|
+
expect(created?.agentId).toBe(agentId);
|
|
35
|
+
expect(created?.title).toBe("Original title");
|
|
36
|
+
expect(created?.status).toBe("open");
|
|
37
|
+
expect(created?.visibility).toBe("private");
|
|
38
|
+
expect(created?.isOwner).toBeNull();
|
|
39
|
+
const recreate = await apiRequest(request, "POST", CONVERSATIONS, {
|
|
40
|
+
token: adminToken,
|
|
41
|
+
data: { agentId, conversationId, title: "Original title" }
|
|
42
|
+
});
|
|
43
|
+
expect(recreate.status(), "re-create with same id is idempotent (200)").toBe(200);
|
|
44
|
+
const getItem = await apiRequest(
|
|
45
|
+
request,
|
|
46
|
+
"GET",
|
|
47
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
48
|
+
{ token: adminToken }
|
|
49
|
+
);
|
|
50
|
+
expect(getItem.status()).toBe(200);
|
|
51
|
+
const item = await readJsonSafe(getItem);
|
|
52
|
+
expect(item?.conversation.conversationId).toBe(conversationId);
|
|
53
|
+
expect(item?.conversation.isOwner).toBe(true);
|
|
54
|
+
const listRes = await apiRequest(
|
|
55
|
+
request,
|
|
56
|
+
"GET",
|
|
57
|
+
`${CONVERSATIONS}?agent=${encodeURIComponent(agentId)}`,
|
|
58
|
+
{ token: adminToken }
|
|
59
|
+
);
|
|
60
|
+
expect(listRes.status()).toBe(200);
|
|
61
|
+
const list = await readJsonSafe(listRes);
|
|
62
|
+
expect(Array.isArray(list?.items)).toBe(true);
|
|
63
|
+
expect(list?.items.some((c) => c.conversationId === conversationId)).toBe(true);
|
|
64
|
+
const patchRes = await apiRequest(
|
|
65
|
+
request,
|
|
66
|
+
"PATCH",
|
|
67
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
68
|
+
{ token: adminToken, data: { title: "Renamed title", status: "closed" } }
|
|
69
|
+
);
|
|
70
|
+
expect(patchRes.status()).toBe(200);
|
|
71
|
+
const patched = await readJsonSafe(patchRes);
|
|
72
|
+
expect(patched?.title).toBe("Renamed title");
|
|
73
|
+
expect(patched?.status).toBe("closed");
|
|
74
|
+
const delRes = await apiRequest(
|
|
75
|
+
request,
|
|
76
|
+
"DELETE",
|
|
77
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
78
|
+
{ token: adminToken }
|
|
79
|
+
);
|
|
80
|
+
expect(delRes.status(), "delete returns 200 (soft delete)").toBe(200);
|
|
81
|
+
expect((await readJsonSafe(delRes))?.ok).toBe(true);
|
|
82
|
+
const listAfter = await apiRequest(
|
|
83
|
+
request,
|
|
84
|
+
"GET",
|
|
85
|
+
`${CONVERSATIONS}?agent=${encodeURIComponent(agentId)}`,
|
|
86
|
+
{ token: adminToken }
|
|
87
|
+
);
|
|
88
|
+
expect(listAfter.status()).toBe(200);
|
|
89
|
+
const listAfterBody = await readJsonSafe(listAfter);
|
|
90
|
+
expect(listAfterBody?.items.some((c) => c.conversationId === conversationId)).toBe(false);
|
|
91
|
+
} finally {
|
|
92
|
+
await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => void 0);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
test("validation + auth gates: missing agentId -> 400, unauthenticated -> 401", async ({ request, baseURL }) => {
|
|
96
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
97
|
+
const badCreate = await apiRequest(request, "POST", CONVERSATIONS, {
|
|
98
|
+
token: adminToken,
|
|
99
|
+
data: { title: "no agent id" }
|
|
100
|
+
});
|
|
101
|
+
expect(badCreate.status()).toBe(400);
|
|
102
|
+
expect((await readJsonSafe(badCreate))?.code).toBe("validation_error");
|
|
103
|
+
const anon = await playwrightRequest.newContext({ baseURL });
|
|
104
|
+
try {
|
|
105
|
+
const res = await anon.fetch(CONVERSATIONS, { method: "GET" });
|
|
106
|
+
expect(res.status(), "unauthenticated list is 401").toBe(401);
|
|
107
|
+
} finally {
|
|
108
|
+
await anon.dispose();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
test("cross-org caller cannot GET/PATCH/DELETE another org conversation (404)", async ({ request }) => {
|
|
112
|
+
test.slow();
|
|
113
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
114
|
+
const { tenantId } = getTokenScope(adminToken);
|
|
115
|
+
const stamp = randomUUID().slice(0, 8);
|
|
116
|
+
const password = "Secret123!";
|
|
117
|
+
const agentId = `it_conv.xorg_${stamp}`;
|
|
118
|
+
const conversationId = `it-conv-xorg-${randomUUID()}`;
|
|
119
|
+
let orgBId = null;
|
|
120
|
+
let roleId = null;
|
|
121
|
+
let userId = null;
|
|
122
|
+
try {
|
|
123
|
+
orgBId = await createOrganizationInDb({ name: `IT Conv OrgB ${stamp}`, tenantId });
|
|
124
|
+
roleId = await createRoleFixture(request, adminToken, { name: `IT Conv Role ${stamp}` });
|
|
125
|
+
userId = await createUserFixture(request, adminToken, {
|
|
126
|
+
email: `it-conv-${stamp}@example.com`,
|
|
127
|
+
password,
|
|
128
|
+
organizationId: orgBId,
|
|
129
|
+
roles: [roleId]
|
|
130
|
+
});
|
|
131
|
+
await setUserAclVisibility(request, adminToken, {
|
|
132
|
+
userId,
|
|
133
|
+
features: ["ai_assistant.view", "ai_assistant.conversations.share"],
|
|
134
|
+
organizations: [orgBId]
|
|
135
|
+
});
|
|
136
|
+
const otherToken = await getAuthToken(request, `it-conv-${stamp}@example.com`, password);
|
|
137
|
+
expect(getTokenScope(otherToken).organizationId, "fixture user is homed in org B").toBe(orgBId);
|
|
138
|
+
const createRes = await apiRequest(request, "POST", CONVERSATIONS, {
|
|
139
|
+
token: adminToken,
|
|
140
|
+
data: { agentId, conversationId, title: "Org A private" }
|
|
141
|
+
});
|
|
142
|
+
expect(createRes.status()).toBe(201);
|
|
143
|
+
const getRes = await apiRequest(
|
|
144
|
+
request,
|
|
145
|
+
"GET",
|
|
146
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
147
|
+
{ token: otherToken }
|
|
148
|
+
);
|
|
149
|
+
expect(getRes.status(), "cross-org GET is 404 (not 403)").toBe(404);
|
|
150
|
+
expect((await readJsonSafe(getRes))?.code).toBe("conversation_not_found");
|
|
151
|
+
const patchRes = await apiRequest(
|
|
152
|
+
request,
|
|
153
|
+
"PATCH",
|
|
154
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
155
|
+
{ token: otherToken, data: { title: "hijack" } }
|
|
156
|
+
);
|
|
157
|
+
expect(patchRes.status(), "cross-org PATCH is 404").toBe(404);
|
|
158
|
+
const delRes = await apiRequest(
|
|
159
|
+
request,
|
|
160
|
+
"DELETE",
|
|
161
|
+
`${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,
|
|
162
|
+
{ token: otherToken }
|
|
163
|
+
);
|
|
164
|
+
expect(delRes.status(), "cross-org DELETE is 404").toBe(404);
|
|
165
|
+
} finally {
|
|
166
|
+
await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => void 0);
|
|
167
|
+
await deleteUserAclInDb(userId ?? "").catch(() => void 0);
|
|
168
|
+
await deleteUserIfExists(request, adminToken, userId);
|
|
169
|
+
await deleteRoleIfExists(request, adminToken, roleId);
|
|
170
|
+
await deleteOrganizationInDb(orgBId).catch(() => void 0);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=TC-AI-CONVERSATIONS-001-create-list-delete.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-CONVERSATIONS-001-create-list-delete.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { test, expect, request as playwrightRequest } from '@playwright/test';\nimport { randomUUID } from 'node:crypto';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n createRoleFixture,\n deleteRoleIfExists,\n createUserFixture,\n deleteUserIfExists,\n setUserAclVisibility,\n} from '@open-mercato/core/helpers/integration/authFixtures';\nimport {\n createOrganizationInDb,\n deleteOrganizationInDb,\n deleteUserAclInDb,\n} from '@open-mercato/core/helpers/integration/dbFixtures';\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport { deleteConversationCascadeInDb } from './helpers/aiAssistantFixtures';\n\n/**\n * TC-AI-CONVERSATIONS-001 \u2014 Conversation CRUD lifecycle (create, list, patch, delete).\n * Source: GitHub issue #2495.\n *\n * Surfaces under test:\n * - /api/ai_assistant/ai/conversations (POST, GET)\n * - /api/ai_assistant/ai/conversations/{id} (PATCH, DELETE)\n *\n * Contract notes verified against the route handlers (not the issue's guesses):\n * - create body field is `agentId` (required); response is a single serialized\n * conversation with `conversationId/agentId/title/status/visibility/isOwner`.\n * - re-creating the same `conversationId` is idempotent -> 200 (vs 201 first time).\n * - DELETE is a SOFT delete returning 200 `{ ok: true }` (NOT 204).\n * - item routes are scoped by tenant+organization+ownership; a caller in a\n * different organization sees the row as absent -> 404 `conversation_not_found`\n * (NOT 403).\n */\n\nconst CONVERSATIONS = '/api/ai_assistant/ai/conversations';\n\ninterface SerializedConversation {\n conversationId: string;\n agentId: string;\n title: string | null;\n status: string;\n visibility: string;\n isOwner: boolean | null;\n participantCount: number;\n}\n\ntest.describe('TC-AI-CONVERSATIONS-001: Conversation CRUD lifecycle + org scoping', () => {\n test('create (idempotent) -> list -> patch -> soft-delete hides from list', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId } = getTokenScope(adminToken);\n const agentId = `it_conv.agent_${randomUUID().slice(0, 8)}`;\n const conversationId = `it-conv-${randomUUID()}`;\n\n try {\n const createRes = await apiRequest(request, 'POST', CONVERSATIONS, {\n token: adminToken,\n data: { agentId, conversationId, title: 'Original title' },\n });\n expect(createRes.status(), 'create returns 201').toBe(201);\n const created = await readJsonSafe<SerializedConversation>(createRes);\n expect(created?.conversationId).toBe(conversationId);\n expect(created?.agentId).toBe(agentId);\n expect(created?.title).toBe('Original title');\n expect(created?.status).toBe('open');\n expect(created?.visibility).toBe('private');\n // `isOwner` is only enriched on the item GET (which passes callerUserId);\n // on the create/list responses it is intentionally null.\n expect(created?.isOwner).toBeNull();\n\n const recreate = await apiRequest(request, 'POST', CONVERSATIONS, {\n token: adminToken,\n data: { agentId, conversationId, title: 'Original title' },\n });\n expect(recreate.status(), 're-create with same id is idempotent (200)').toBe(200);\n\n // Item GET enriches ownership: the creator is the owner.\n const getItem = await apiRequest(\n request,\n 'GET',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: adminToken },\n );\n expect(getItem.status()).toBe(200);\n const item = await readJsonSafe<{ conversation: SerializedConversation }>(getItem);\n expect(item?.conversation.conversationId).toBe(conversationId);\n expect(item?.conversation.isOwner).toBe(true);\n\n const listRes = await apiRequest(\n request,\n 'GET',\n `${CONVERSATIONS}?agent=${encodeURIComponent(agentId)}`,\n { token: adminToken },\n );\n expect(listRes.status()).toBe(200);\n const list = await readJsonSafe<{ items: SerializedConversation[]; nextCursor: string | null }>(listRes);\n expect(Array.isArray(list?.items)).toBe(true);\n expect(list?.items.some((c) => c.conversationId === conversationId)).toBe(true);\n\n const patchRes = await apiRequest(\n request,\n 'PATCH',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: adminToken, data: { title: 'Renamed title', status: 'closed' } },\n );\n expect(patchRes.status()).toBe(200);\n const patched = await readJsonSafe<SerializedConversation>(patchRes);\n expect(patched?.title).toBe('Renamed title');\n expect(patched?.status).toBe('closed');\n\n const delRes = await apiRequest(\n request,\n 'DELETE',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: adminToken },\n );\n expect(delRes.status(), 'delete returns 200 (soft delete)').toBe(200);\n expect((await readJsonSafe<{ ok: boolean }>(delRes))?.ok).toBe(true);\n\n const listAfter = await apiRequest(\n request,\n 'GET',\n `${CONVERSATIONS}?agent=${encodeURIComponent(agentId)}`,\n { token: adminToken },\n );\n expect(listAfter.status()).toBe(200);\n const listAfterBody = await readJsonSafe<{ items: SerializedConversation[] }>(listAfter);\n expect(listAfterBody?.items.some((c) => c.conversationId === conversationId)).toBe(false);\n } finally {\n await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => undefined);\n }\n });\n\n test('validation + auth gates: missing agentId -> 400, unauthenticated -> 401', async ({ request, baseURL }) => {\n const adminToken = await getAuthToken(request, 'admin');\n\n const badCreate = await apiRequest(request, 'POST', CONVERSATIONS, {\n token: adminToken,\n data: { title: 'no agent id' },\n });\n expect(badCreate.status()).toBe(400);\n expect((await readJsonSafe<{ code?: string }>(badCreate))?.code).toBe('validation_error');\n\n const anon = await playwrightRequest.newContext({ baseURL });\n try {\n const res = await anon.fetch(CONVERSATIONS, { method: 'GET' });\n expect(res.status(), 'unauthenticated list is 401').toBe(401);\n } finally {\n await anon.dispose();\n }\n });\n\n test('cross-org caller cannot GET/PATCH/DELETE another org conversation (404)', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId } = getTokenScope(adminToken);\n const stamp = randomUUID().slice(0, 8);\n const password = 'Secret123!';\n const agentId = `it_conv.xorg_${stamp}`;\n const conversationId = `it-conv-xorg-${randomUUID()}`;\n\n let orgBId: string | null = null;\n let roleId: string | null = null;\n let userId: string | null = null;\n try {\n orgBId = await createOrganizationInDb({ name: `IT Conv OrgB ${stamp}`, tenantId });\n roleId = await createRoleFixture(request, adminToken, { name: `IT Conv Role ${stamp}` });\n userId = await createUserFixture(request, adminToken, {\n email: `it-conv-${stamp}@example.com`,\n password,\n organizationId: orgBId,\n roles: [roleId],\n });\n await setUserAclVisibility(request, adminToken, {\n userId,\n features: ['ai_assistant.view', 'ai_assistant.conversations.share'],\n organizations: [orgBId],\n });\n const otherToken = await getAuthToken(request, `it-conv-${stamp}@example.com`, password);\n expect(getTokenScope(otherToken).organizationId, 'fixture user is homed in org B').toBe(orgBId);\n\n const createRes = await apiRequest(request, 'POST', CONVERSATIONS, {\n token: adminToken,\n data: { agentId, conversationId, title: 'Org A private' },\n });\n expect(createRes.status()).toBe(201);\n\n const getRes = await apiRequest(\n request,\n 'GET',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: otherToken },\n );\n expect(getRes.status(), 'cross-org GET is 404 (not 403)').toBe(404);\n expect((await readJsonSafe<{ code?: string }>(getRes))?.code).toBe('conversation_not_found');\n\n const patchRes = await apiRequest(\n request,\n 'PATCH',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: otherToken, data: { title: 'hijack' } },\n );\n expect(patchRes.status(), 'cross-org PATCH is 404').toBe(404);\n\n const delRes = await apiRequest(\n request,\n 'DELETE',\n `${CONVERSATIONS}/${encodeURIComponent(conversationId)}`,\n { token: otherToken },\n );\n expect(delRes.status(), 'cross-org DELETE is 404').toBe(404);\n } finally {\n await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => undefined);\n await deleteUserAclInDb(userId ?? '').catch(() => undefined);\n await deleteUserIfExists(request, adminToken, userId);\n await deleteRoleIfExists(request, adminToken, roleId);\n await deleteOrganizationInDb(orgBId).catch(() => undefined);\n }\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { test, expect } 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 {
|
|
12
|
+
createOrganizationInDb,
|
|
13
|
+
deleteOrganizationInDb,
|
|
14
|
+
deleteUserAclInDb
|
|
15
|
+
} from "@open-mercato/core/helpers/integration/dbFixtures";
|
|
16
|
+
import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
17
|
+
import { deleteConversationCascadeInDb } from "./helpers/aiAssistantFixtures.js";
|
|
18
|
+
const CONVERSATIONS = "/api/ai_assistant/ai/conversations";
|
|
19
|
+
test.describe("TC-AI-PARTICIPANTS-002: Conversation participants", () => {
|
|
20
|
+
test("add -> list -> remove lifecycle plus owner/duplicate/self/cross-org/RBAC guards", async ({ request }) => {
|
|
21
|
+
test.slow();
|
|
22
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
23
|
+
const { tenantId, organizationId: orgAId, userId: ownerId } = getTokenScope(adminToken);
|
|
24
|
+
const stamp = randomUUID().slice(0, 8);
|
|
25
|
+
const password = "Secret123!";
|
|
26
|
+
const agentId = `it_part.agent_${stamp}`;
|
|
27
|
+
const conversationId = `it-part-${randomUUID()}`;
|
|
28
|
+
let roleId = null;
|
|
29
|
+
let memberId = null;
|
|
30
|
+
let foreignOrgId = null;
|
|
31
|
+
let foreignUserId = null;
|
|
32
|
+
try {
|
|
33
|
+
roleId = await createRoleFixture(request, adminToken, { name: `IT Part Role ${stamp}` });
|
|
34
|
+
memberId = await createUserFixture(request, adminToken, {
|
|
35
|
+
email: `it-part-member-${stamp}@example.com`,
|
|
36
|
+
password,
|
|
37
|
+
organizationId: orgAId,
|
|
38
|
+
roles: [roleId]
|
|
39
|
+
});
|
|
40
|
+
await setUserAclVisibility(request, adminToken, {
|
|
41
|
+
userId: memberId,
|
|
42
|
+
features: ["ai_assistant.view"],
|
|
43
|
+
organizations: [orgAId]
|
|
44
|
+
});
|
|
45
|
+
foreignOrgId = await createOrganizationInDb({ name: `IT Part OrgB ${stamp}`, tenantId });
|
|
46
|
+
foreignUserId = await createUserFixture(request, adminToken, {
|
|
47
|
+
email: `it-part-foreign-${stamp}@example.com`,
|
|
48
|
+
password,
|
|
49
|
+
organizationId: foreignOrgId,
|
|
50
|
+
roles: [roleId]
|
|
51
|
+
});
|
|
52
|
+
const createRes = await apiRequest(request, "POST", CONVERSATIONS, {
|
|
53
|
+
token: adminToken,
|
|
54
|
+
data: { agentId, conversationId, title: "Shareable conversation" }
|
|
55
|
+
});
|
|
56
|
+
expect(createRes.status()).toBe(201);
|
|
57
|
+
const participantsPath = `${CONVERSATIONS}/${encodeURIComponent(conversationId)}/participants`;
|
|
58
|
+
const addRes = await apiRequest(request, "POST", participantsPath, {
|
|
59
|
+
token: adminToken,
|
|
60
|
+
data: { userId: memberId }
|
|
61
|
+
});
|
|
62
|
+
expect(addRes.status(), "add participant returns 201").toBe(201);
|
|
63
|
+
const added = await readJsonSafe(addRes);
|
|
64
|
+
expect(added?.participant.userId).toBe(memberId);
|
|
65
|
+
expect(added?.participant.role).toBe("viewer");
|
|
66
|
+
const listRes = await apiRequest(request, "GET", participantsPath, { token: adminToken });
|
|
67
|
+
expect(listRes.status()).toBe(200);
|
|
68
|
+
const list = await readJsonSafe(listRes);
|
|
69
|
+
expect(list?.ownerUserId).toBe(ownerId);
|
|
70
|
+
expect(list?.participants.some((p) => p.userId === ownerId && p.role === "owner")).toBe(true);
|
|
71
|
+
expect(list?.participants.some((p) => p.userId === memberId && p.role === "viewer")).toBe(true);
|
|
72
|
+
const selfAdd = await apiRequest(request, "POST", participantsPath, {
|
|
73
|
+
token: adminToken,
|
|
74
|
+
data: { userId: ownerId }
|
|
75
|
+
});
|
|
76
|
+
expect(selfAdd.status()).toBe(400);
|
|
77
|
+
expect((await readJsonSafe(selfAdd))?.code).toBe("self_share_not_allowed");
|
|
78
|
+
const dup = await apiRequest(request, "POST", participantsPath, {
|
|
79
|
+
token: adminToken,
|
|
80
|
+
data: { userId: memberId }
|
|
81
|
+
});
|
|
82
|
+
expect(dup.status()).toBe(409);
|
|
83
|
+
expect((await readJsonSafe(dup))?.code).toBe("duplicate_participant");
|
|
84
|
+
const foreign = await apiRequest(request, "POST", participantsPath, {
|
|
85
|
+
token: adminToken,
|
|
86
|
+
data: { userId: foreignUserId }
|
|
87
|
+
});
|
|
88
|
+
expect(foreign.status()).toBe(400);
|
|
89
|
+
expect((await readJsonSafe(foreign))?.code).toBe("user_not_found");
|
|
90
|
+
const memberToken = await getAuthToken(request, `it-part-member-${stamp}@example.com`, password);
|
|
91
|
+
const denied = await apiRequest(request, "POST", participantsPath, {
|
|
92
|
+
token: memberToken,
|
|
93
|
+
data: { userId: randomUUID() }
|
|
94
|
+
});
|
|
95
|
+
expect(denied.status(), "caller without conversations.share is 403").toBe(403);
|
|
96
|
+
const removeOwner = await apiRequest(
|
|
97
|
+
request,
|
|
98
|
+
"DELETE",
|
|
99
|
+
`${participantsPath}/${encodeURIComponent(ownerId)}`,
|
|
100
|
+
{ token: adminToken }
|
|
101
|
+
);
|
|
102
|
+
expect(removeOwner.status(), "cannot revoke the owner").toBe(403);
|
|
103
|
+
const removeMember = await apiRequest(
|
|
104
|
+
request,
|
|
105
|
+
"DELETE",
|
|
106
|
+
`${participantsPath}/${encodeURIComponent(memberId)}`,
|
|
107
|
+
{ token: adminToken }
|
|
108
|
+
);
|
|
109
|
+
expect(removeMember.status(), "revoke participant returns 204").toBe(204);
|
|
110
|
+
const listAfter = await apiRequest(request, "GET", participantsPath, { token: adminToken });
|
|
111
|
+
expect(listAfter.status()).toBe(200);
|
|
112
|
+
const after = await readJsonSafe(listAfter);
|
|
113
|
+
expect(after?.participants.some((p) => p.userId === memberId)).toBe(false);
|
|
114
|
+
const removeAgain = await apiRequest(
|
|
115
|
+
request,
|
|
116
|
+
"DELETE",
|
|
117
|
+
`${participantsPath}/${encodeURIComponent(memberId)}`,
|
|
118
|
+
{ token: adminToken }
|
|
119
|
+
);
|
|
120
|
+
expect(removeAgain.status()).toBe(404);
|
|
121
|
+
expect((await readJsonSafe(removeAgain))?.code).toBe("participant_not_found");
|
|
122
|
+
} finally {
|
|
123
|
+
await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => void 0);
|
|
124
|
+
await deleteUserAclInDb(memberId ?? "").catch(() => void 0);
|
|
125
|
+
await deleteUserIfExists(request, adminToken, memberId);
|
|
126
|
+
await deleteUserIfExists(request, adminToken, foreignUserId);
|
|
127
|
+
await deleteRoleIfExists(request, adminToken, roleId);
|
|
128
|
+
await deleteOrganizationInDb(foreignOrgId).catch(() => void 0);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=TC-AI-PARTICIPANTS-002-add-remove-participants.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-PARTICIPANTS-002-add-remove-participants.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { randomUUID } from 'node:crypto';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n createRoleFixture,\n deleteRoleIfExists,\n createUserFixture,\n deleteUserIfExists,\n setUserAclVisibility,\n} from '@open-mercato/core/helpers/integration/authFixtures';\nimport {\n createOrganizationInDb,\n deleteOrganizationInDb,\n deleteUserAclInDb,\n} from '@open-mercato/core/helpers/integration/dbFixtures';\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport { deleteConversationCascadeInDb } from './helpers/aiAssistantFixtures';\n\n/**\n * TC-AI-PARTICIPANTS-002 \u2014 Conversation participants (add / list / remove).\n * Source: GitHub issue #2495.\n *\n * Surfaces under test:\n * - /api/ai_assistant/ai/conversations/{id}/participants (POST, GET)\n * - /api/ai_assistant/ai/conversations/{id}/participants/{userId} (DELETE)\n *\n * Contract notes verified against the route handlers:\n * - add requires `ai_assistant.conversations.share`; only the owner may add.\n * - the only wire role is `viewer` (the owner row is implicit).\n * - add returns 201 `{ participant: { userId, role, lastReadAt, addedAt } }`.\n * - removing a participant returns 204; removing the OWNER is 403; a missing\n * participant is 404 `participant_not_found`.\n * - self-add is 400 `self_share_not_allowed`; a duplicate is 409\n * `duplicate_participant`; a target in another org is 400 `user_not_found`.\n */\n\nconst CONVERSATIONS = '/api/ai_assistant/ai/conversations';\n\ninterface ParticipantRow {\n userId: string;\n role: string;\n lastReadAt: string | null;\n addedAt: string;\n}\n\ntest.describe('TC-AI-PARTICIPANTS-002: Conversation participants', () => {\n test('add -> list -> remove lifecycle plus owner/duplicate/self/cross-org/RBAC guards', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId: orgAId, userId: ownerId } = getTokenScope(adminToken);\n const stamp = randomUUID().slice(0, 8);\n const password = 'Secret123!';\n const agentId = `it_part.agent_${stamp}`;\n const conversationId = `it-part-${randomUUID()}`;\n\n let roleId: string | null = null;\n let memberId: string | null = null;\n let foreignOrgId: string | null = null;\n let foreignUserId: string | null = null;\n try {\n roleId = await createRoleFixture(request, adminToken, { name: `IT Part Role ${stamp}` });\n // Member lives in the SAME org as the owner and carries only view -> valid\n // share target AND a deterministic \"lacks conversations.share\" caller.\n memberId = await createUserFixture(request, adminToken, {\n email: `it-part-member-${stamp}@example.com`,\n password,\n organizationId: orgAId,\n roles: [roleId],\n });\n await setUserAclVisibility(request, adminToken, {\n userId: memberId,\n features: ['ai_assistant.view'],\n organizations: [orgAId],\n });\n // Foreign user lives in another org -> cannot be shared into this conversation.\n foreignOrgId = await createOrganizationInDb({ name: `IT Part OrgB ${stamp}`, tenantId });\n foreignUserId = await createUserFixture(request, adminToken, {\n email: `it-part-foreign-${stamp}@example.com`,\n password,\n organizationId: foreignOrgId,\n roles: [roleId],\n });\n\n const createRes = await apiRequest(request, 'POST', CONVERSATIONS, {\n token: adminToken,\n data: { agentId, conversationId, title: 'Shareable conversation' },\n });\n expect(createRes.status()).toBe(201);\n\n const participantsPath = `${CONVERSATIONS}/${encodeURIComponent(conversationId)}/participants`;\n\n // Add member -> 201 viewer\n const addRes = await apiRequest(request, 'POST', participantsPath, {\n token: adminToken,\n data: { userId: memberId },\n });\n expect(addRes.status(), 'add participant returns 201').toBe(201);\n const added = await readJsonSafe<{ participant: ParticipantRow }>(addRes);\n expect(added?.participant.userId).toBe(memberId);\n expect(added?.participant.role).toBe('viewer');\n\n // List -> owner + member\n const listRes = await apiRequest(request, 'GET', participantsPath, { token: adminToken });\n expect(listRes.status()).toBe(200);\n const list = await readJsonSafe<{ ownerUserId: string; participants: ParticipantRow[] }>(listRes);\n expect(list?.ownerUserId).toBe(ownerId);\n expect(list?.participants.some((p) => p.userId === ownerId && p.role === 'owner')).toBe(true);\n expect(list?.participants.some((p) => p.userId === memberId && p.role === 'viewer')).toBe(true);\n\n // Self-add -> 400 self_share_not_allowed\n const selfAdd = await apiRequest(request, 'POST', participantsPath, {\n token: adminToken,\n data: { userId: ownerId },\n });\n expect(selfAdd.status()).toBe(400);\n expect((await readJsonSafe<{ code?: string }>(selfAdd))?.code).toBe('self_share_not_allowed');\n\n // Duplicate -> 409 duplicate_participant\n const dup = await apiRequest(request, 'POST', participantsPath, {\n token: adminToken,\n data: { userId: memberId },\n });\n expect(dup.status()).toBe(409);\n expect((await readJsonSafe<{ code?: string }>(dup))?.code).toBe('duplicate_participant');\n\n // Foreign-org target -> 400 user_not_found\n const foreign = await apiRequest(request, 'POST', participantsPath, {\n token: adminToken,\n data: { userId: foreignUserId },\n });\n expect(foreign.status()).toBe(400);\n expect((await readJsonSafe<{ code?: string }>(foreign))?.code).toBe('user_not_found');\n\n // Caller lacking conversations.share -> 403 (feature gate fires first)\n const memberToken = await getAuthToken(request, `it-part-member-${stamp}@example.com`, password);\n const denied = await apiRequest(request, 'POST', participantsPath, {\n token: memberToken,\n data: { userId: randomUUID() },\n });\n expect(denied.status(), 'caller without conversations.share is 403').toBe(403);\n\n // Remove the OWNER -> 403\n const removeOwner = await apiRequest(\n request,\n 'DELETE',\n `${participantsPath}/${encodeURIComponent(ownerId)}`,\n { token: adminToken },\n );\n expect(removeOwner.status(), 'cannot revoke the owner').toBe(403);\n\n // Remove the member -> 204\n const removeMember = await apiRequest(\n request,\n 'DELETE',\n `${participantsPath}/${encodeURIComponent(memberId)}`,\n { token: adminToken },\n );\n expect(removeMember.status(), 'revoke participant returns 204').toBe(204);\n\n // List -> member gone\n const listAfter = await apiRequest(request, 'GET', participantsPath, { token: adminToken });\n expect(listAfter.status()).toBe(200);\n const after = await readJsonSafe<{ participants: ParticipantRow[] }>(listAfter);\n expect(after?.participants.some((p) => p.userId === memberId)).toBe(false);\n\n // Remove again -> 404 participant_not_found\n const removeAgain = await apiRequest(\n request,\n 'DELETE',\n `${participantsPath}/${encodeURIComponent(memberId)}`,\n { token: adminToken },\n );\n expect(removeAgain.status()).toBe(404);\n expect((await readJsonSafe<{ code?: string }>(removeAgain))?.code).toBe('participant_not_found');\n } finally {\n await deleteConversationCascadeInDb({ tenantId, conversationId }).catch(() => undefined);\n await deleteUserAclInDb(memberId ?? '').catch(() => undefined);\n await deleteUserIfExists(request, adminToken, memberId);\n await deleteUserIfExists(request, adminToken, foreignUserId);\n await deleteRoleIfExists(request, adminToken, roleId);\n await deleteOrganizationInDb(foreignOrgId).catch(() => undefined);\n }\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js
CHANGED
|
@@ -136,6 +136,13 @@ test.describe("TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides", () => {
|
|
|
136
136
|
body: JSON.stringify(settingsPayload)
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
|
+
await page.route("**/api/ai_assistant/ai/agents", async (route) => {
|
|
140
|
+
await route.fulfill({
|
|
141
|
+
status: 200,
|
|
142
|
+
contentType: "application/json",
|
|
143
|
+
body: JSON.stringify(agentsPayload)
|
|
144
|
+
});
|
|
145
|
+
});
|
|
139
146
|
await page.route("**/api/ai_assistant/health", async (route) => {
|
|
140
147
|
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ status: "ok", url: "http://localhost", mcpUrl: "http://localhost:3001" }) });
|
|
141
148
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-RUNTIME-OVERRIDES-006: Phase 4b \u2014 runtime model overrides (ModelPicker,\n * editable settings form, playground resolution panel).\n *\n * Coverage:\n * - /backend/config/ai-assistant/settings page loads with override form\n * - GlobalOverrideForm: provider + model selects, save, clear\n * - PerAgentOverrideList: table rows, source column, Clear override button\n * - /backend/config/ai-assistant/playground: ModelResolutionPanel renders\n * - ModelPicker renders in the playground's <AiChat> composer when the\n * agent allows runtime model override\n * - ModelPicker is absent when allowRuntimeOverride === false\n *\n * All API calls that would hit a real LLM or require a configured provider\n * are intercepted via page.route() stubs.\n */\ntest.describe('TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n // ---------------------------------------------------------------------------\n // Shared stubs\n // ---------------------------------------------------------------------------\n const settingsPayload = {\n provider: {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n {\n id: 'openai',\n name: 'OpenAI',\n model: 'gpt-5-mini',\n defaultModel: 'gpt-5-mini',\n envKey: 'OPENAI_API_KEY',\n configured: false,\n defaultModels: [{ id: 'gpt-5-mini', name: 'GPT-5 Mini' }],\n },\n ],\n mcpKeyConfigured: true,\n resolvedDefault: {\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n tenantOverride: null,\n agents: [\n {\n agentId: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n allowRuntimeOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeOverride: false,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n source: 'tenant_override',\n },\n ],\n };\n\n const agentsPayload = {\n agents: [\n {\n id: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n label: 'Merchandising Assistant',\n description: 'Catalog merchandising tool.',\n systemPrompt: 'You are a merchandising assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['catalog.list_products'],\n tools: [{ name: 'catalog.list_products', displayName: 'List products', isMutation: false, registered: true }],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n };\n\n // ---------------------------------------------------------------------------\n // Settings page\n // ---------------------------------------------------------------------------\n test.describe('Settings page (/backend/config/ai-assistant/settings)', () => {\n test('renders override form and per-agent resolution table', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }),\n });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ tools: [] }),\n });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The main settings container should be visible\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n\n // Global override form\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 15_000 });\n\n // Per-agent override table\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 15_000 });\n\n // Both registered agents appear as rows\n const catalogRow = page.locator('[data-ai-settings-agent-row=\"catalog.merchandising_assistant\"]');\n await expect(catalogRow).toBeVisible();\n\n const customersRow = page.locator('[data-ai-settings-agent-row=\"customers.account_assistant\"]');\n await expect(customersRow).toBeVisible();\n });\n\n test('shows Clear override button only for agents with non-default source', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 30_000 });\n\n // customers.account_assistant has source='tenant_override' \u2192 should have Clear button\n const customersClear = page.locator('[data-ai-settings-clear-agent-override=\"customers.account_assistant\"]');\n await expect(customersClear).toBeVisible();\n\n // catalog.merchandising_assistant has source='provider_default' \u2192 no Clear button\n const catalogClear = page.locator('[data-ai-settings-clear-agent-override=\"catalog.merchandising_assistant\"]');\n await expect(catalogClear).not.toBeVisible();\n });\n\n test('save override calls PUT /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n let putCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'PUT') {\n putCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: 'org-1',\n agentId: null,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseUrl: null,\n updatedAt: new Date().toISOString(),\n }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 30_000 });\n\n // Select provider\n const providerSelect = page.locator('[data-ai-settings-provider-select]');\n await providerSelect.click();\n const anthropicOption = page.getByRole('option', { name: 'Anthropic' });\n await anthropicOption.click();\n\n // Select model\n const modelSelect = page.locator('[data-ai-settings-model-select]');\n await modelSelect.click();\n const sonnetOption = page.getByRole('option', { name: 'Claude Sonnet 4.5' });\n await sonnetOption.click();\n\n // Save\n const saveButton = page.locator('[data-ai-settings-save-override]');\n await saveButton.click();\n\n await page.waitForTimeout(500);\n expect(putCalls).toBeGreaterThanOrEqual(1);\n });\n\n test('clear override calls DELETE /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const settingsWithOverride = {\n ...settingsPayload,\n tenantOverride: {\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n agentId: null,\n updatedAt: new Date().toISOString(),\n },\n };\n\n let deleteCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'DELETE') {\n deleteCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ cleared: true }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsWithOverride),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The \"Clear override\" button for the active override\n const clearButton = page.locator('[data-ai-settings-clear-override]');\n await expect(clearButton).toBeVisible({ timeout: 30_000 });\n await clearButton.click();\n\n await page.waitForTimeout(500);\n expect(deleteCalls).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ---------------------------------------------------------------------------\n // Playground page \u2014 ModelResolutionPanel\n // ---------------------------------------------------------------------------\n test.describe('Playground page (/backend/config/ai-assistant/playground)', () => {\n test('renders ModelResolutionPanel with provider/model/source for the selected agent', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for agent list to load\n const agentSection = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(agentSection).toBeVisible({ timeout: 30_000 });\n\n // The resolution panel should show provider info\n const resolutionPanel = page.locator('[data-ai-playground-model-resolution=\"catalog.merchandising_assistant\"]');\n await expect(resolutionPanel).toBeVisible({ timeout: 15_000 });\n\n // Provider field should be present\n const providerField = page.locator('[data-ai-playground-resolution-provider]');\n await expect(providerField).toBeVisible();\n\n // Model field should be present\n const modelField = page.locator('[data-ai-playground-resolution-model]');\n await expect(modelField).toBeVisible();\n\n // Source field should be present\n const sourceField = page.locator('[data-ai-playground-resolution-source]');\n await expect(sourceField).toBeVisible();\n });\n\n test('ModelPicker is present in AiChat composer when allowRuntimeOverride is true', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n // Stub the models endpoint\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.merchandising_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isDefault: true,\n models: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', isDefault: true },\n ],\n },\n ],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for the chat area to be visible under the selected agent\n const chatContainer = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(chatContainer).toBeVisible({ timeout: 30_000 });\n\n // The ModelPicker trigger should be visible inside the chat container\n const modelPickerTrigger = chatContainer.locator('[data-ai-model-picker-trigger]');\n // Use a soft assertion \u2014 the picker requires the models endpoint to resolve;\n // if the CI environment skips the endpoint, we verify the playground itself loaded.\n const pickerVisible = await modelPickerTrigger.isVisible().catch(() => false);\n if (pickerVisible) {\n await expect(modelPickerTrigger).toBeVisible();\n } else {\n // At minimum the chat area must be visible\n await expect(chatContainer).toBeVisible();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // API contract tests (no browser needed)\n // ---------------------------------------------------------------------------\n test.describe('API contract \u2014 GET /api/ai_assistant/settings', () => {\n test('unauthenticated request returns 401 or redirect', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/settings');\n expect([200, 401, 302, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 PUT /api/ai_assistant/settings', () => {\n test('unauthenticated PUT returns 401', async ({ request }) => {\n const response = await request.put('/api/ai_assistant/settings', {\n data: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' },\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 DELETE /api/ai_assistant/settings', () => {\n test('unauthenticated DELETE returns 401', async ({ request }) => {\n const response = await request.delete('/api/ai_assistant/settings', {\n data: {},\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 GET /api/ai_assistant/ai/agents/:agentId/models', () => {\n test('route is mounted and returns 401 or JSON payload', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents/catalog.merchandising_assistant/models');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agentId');\n expect(body).toHaveProperty('allowRuntimeOverride');\n expect(body).toHaveProperty('providers');\n }\n });\n });\n});\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-RUNTIME-OVERRIDES-006: Phase 4b \u2014 runtime model overrides (ModelPicker,\n * editable settings form, playground resolution panel).\n *\n * Coverage:\n * - /backend/config/ai-assistant/settings page loads with override form\n * - GlobalOverrideForm: provider + model selects, save, clear\n * - PerAgentOverrideList: table rows, source column, Clear override button\n * - /backend/config/ai-assistant/playground: ModelResolutionPanel renders\n * - ModelPicker renders in the playground's <AiChat> composer when the\n * agent allows runtime model override\n * - ModelPicker is absent when allowRuntimeOverride === false\n *\n * All API calls that would hit a real LLM or require a configured provider\n * are intercepted via page.route() stubs.\n */\ntest.describe('TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n // ---------------------------------------------------------------------------\n // Shared stubs\n // ---------------------------------------------------------------------------\n const settingsPayload = {\n provider: {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n {\n id: 'openai',\n name: 'OpenAI',\n model: 'gpt-5-mini',\n defaultModel: 'gpt-5-mini',\n envKey: 'OPENAI_API_KEY',\n configured: false,\n defaultModels: [{ id: 'gpt-5-mini', name: 'GPT-5 Mini' }],\n },\n ],\n mcpKeyConfigured: true,\n resolvedDefault: {\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n tenantOverride: null,\n agents: [\n {\n agentId: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n allowRuntimeOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeOverride: false,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n source: 'tenant_override',\n },\n ],\n };\n\n const agentsPayload = {\n agents: [\n {\n id: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n label: 'Merchandising Assistant',\n description: 'Catalog merchandising tool.',\n systemPrompt: 'You are a merchandising assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['catalog.list_products'],\n tools: [{ name: 'catalog.list_products', displayName: 'List products', isMutation: false, registered: true }],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n };\n\n // ---------------------------------------------------------------------------\n // Settings page\n // ---------------------------------------------------------------------------\n test.describe('Settings page (/backend/config/ai-assistant/settings)', () => {\n test('renders override form and per-agent resolution table', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }),\n });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ tools: [] }),\n });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The main settings container should be visible\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n\n // Global override form\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 15_000 });\n\n // Per-agent override table\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 15_000 });\n\n // Both registered agents appear as rows\n const catalogRow = page.locator('[data-ai-settings-agent-row=\"catalog.merchandising_assistant\"]');\n await expect(catalogRow).toBeVisible();\n\n const customersRow = page.locator('[data-ai-settings-agent-row=\"customers.account_assistant\"]');\n await expect(customersRow).toBeVisible();\n });\n\n test('shows Clear override button only for agents with non-default source', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 30_000 });\n\n // customers.account_assistant has source='tenant_override' \u2192 should have Clear button\n const customersClear = page.locator('[data-ai-settings-clear-agent-override=\"customers.account_assistant\"]');\n await expect(customersClear).toBeVisible();\n\n // catalog.merchandising_assistant has source='provider_default' \u2192 no Clear button\n const catalogClear = page.locator('[data-ai-settings-clear-agent-override=\"catalog.merchandising_assistant\"]');\n await expect(catalogClear).not.toBeVisible();\n });\n\n test('save override calls PUT /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n let putCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'PUT') {\n putCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: 'org-1',\n agentId: null,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseUrl: null,\n updatedAt: new Date().toISOString(),\n }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 30_000 });\n\n // Select provider\n const providerSelect = page.locator('[data-ai-settings-provider-select]');\n await providerSelect.click();\n const anthropicOption = page.getByRole('option', { name: 'Anthropic' });\n await anthropicOption.click();\n\n // Select model\n const modelSelect = page.locator('[data-ai-settings-model-select]');\n await modelSelect.click();\n const sonnetOption = page.getByRole('option', { name: 'Claude Sonnet 4.5' });\n await sonnetOption.click();\n\n // Save\n const saveButton = page.locator('[data-ai-settings-save-override]');\n await saveButton.click();\n\n await page.waitForTimeout(500);\n expect(putCalls).toBeGreaterThanOrEqual(1);\n });\n\n test('clear override calls DELETE /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const settingsWithOverride = {\n ...settingsPayload,\n tenantOverride: {\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n agentId: null,\n updatedAt: new Date().toISOString(),\n },\n };\n\n let deleteCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'DELETE') {\n deleteCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ cleared: true }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsWithOverride),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The \"Clear override\" button for the active override\n const clearButton = page.locator('[data-ai-settings-clear-override]');\n await expect(clearButton).toBeVisible({ timeout: 30_000 });\n await clearButton.click();\n\n await page.waitForTimeout(500);\n expect(deleteCalls).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ---------------------------------------------------------------------------\n // Playground page \u2014 ModelResolutionPanel\n // ---------------------------------------------------------------------------\n test.describe('Playground page (/backend/config/ai-assistant/playground)', () => {\n test('renders ModelResolutionPanel with provider/model/source for the selected agent', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for agent list to load\n const agentSection = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(agentSection).toBeVisible({ timeout: 30_000 });\n\n // The resolution panel should show provider info\n const resolutionPanel = page.locator('[data-ai-playground-model-resolution=\"catalog.merchandising_assistant\"]');\n await expect(resolutionPanel).toBeVisible({ timeout: 15_000 });\n\n // Provider field should be present\n const providerField = page.locator('[data-ai-playground-resolution-provider]');\n await expect(providerField).toBeVisible();\n\n // Model field should be present\n const modelField = page.locator('[data-ai-playground-resolution-model]');\n await expect(modelField).toBeVisible();\n\n // Source field should be present\n const sourceField = page.locator('[data-ai-playground-resolution-source]');\n await expect(sourceField).toBeVisible();\n });\n\n test('ModelPicker is present in AiChat composer when allowRuntimeOverride is true', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n // Stub the models endpoint\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.merchandising_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isDefault: true,\n models: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', isDefault: true },\n ],\n },\n ],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for the chat area to be visible under the selected agent\n const chatContainer = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(chatContainer).toBeVisible({ timeout: 30_000 });\n\n // The ModelPicker trigger should be visible inside the chat container\n const modelPickerTrigger = chatContainer.locator('[data-ai-model-picker-trigger]');\n // Use a soft assertion \u2014 the picker requires the models endpoint to resolve;\n // if the CI environment skips the endpoint, we verify the playground itself loaded.\n const pickerVisible = await modelPickerTrigger.isVisible().catch(() => false);\n if (pickerVisible) {\n await expect(modelPickerTrigger).toBeVisible();\n } else {\n // At minimum the chat area must be visible\n await expect(chatContainer).toBeVisible();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // API contract tests (no browser needed)\n // ---------------------------------------------------------------------------\n test.describe('API contract \u2014 GET /api/ai_assistant/settings', () => {\n test('unauthenticated request returns 401 or redirect', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/settings');\n expect([200, 401, 302, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 PUT /api/ai_assistant/settings', () => {\n test('unauthenticated PUT returns 401', async ({ request }) => {\n const response = await request.put('/api/ai_assistant/settings', {\n data: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' },\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 DELETE /api/ai_assistant/settings', () => {\n test('unauthenticated DELETE returns 401', async ({ request }) => {\n const response = await request.delete('/api/ai_assistant/settings', {\n data: {},\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 GET /api/ai_assistant/ai/agents/:agentId/models', () => {\n test('route is mounted and returns 401 or JSON payload', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents/catalog.merchandising_assistant/models');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agentId');\n expect(body).toHaveProperty('allowRuntimeOverride');\n expect(body).toHaveProperty('providers');\n }\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/ai_assistant/__integration__/TC-AI-SESSION-KEY-006-create-session-token.spec.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
const SESSION_KEY = "/api/ai_assistant/session-key";
|
|
14
|
+
const SESSION_TOKEN_RE = /^sess_[0-9a-f]{32}$/;
|
|
15
|
+
const TTL_MINUTES = 120;
|
|
16
|
+
test.describe("TC-AI-SESSION-KEY-006: Session key generation", () => {
|
|
17
|
+
test("POST mints a unique sess_ token with a ~120 minute TTL", async ({ request }) => {
|
|
18
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
19
|
+
const first = await apiRequest(request, "POST", SESSION_KEY, { token: adminToken, data: {} });
|
|
20
|
+
expect(first.status(), "session-key POST returns 200").toBe(200);
|
|
21
|
+
const body = await readJsonSafe(first);
|
|
22
|
+
expect(body?.sessionToken).toMatch(SESSION_TOKEN_RE);
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const expiresAt = Date.parse(body.expiresAt);
|
|
25
|
+
expect(Number.isNaN(expiresAt)).toBe(false);
|
|
26
|
+
expect(expiresAt).toBeGreaterThan(now + (TTL_MINUTES - 5) * 60 * 1e3);
|
|
27
|
+
expect(expiresAt).toBeLessThan(now + (TTL_MINUTES + 5) * 60 * 1e3);
|
|
28
|
+
const second = await apiRequest(request, "POST", SESSION_KEY, { token: adminToken, data: {} });
|
|
29
|
+
expect(second.status()).toBe(200);
|
|
30
|
+
const secondBody = await readJsonSafe(second);
|
|
31
|
+
expect(secondBody?.sessionToken).toMatch(SESSION_TOKEN_RE);
|
|
32
|
+
expect(secondBody?.sessionToken, "each call mints a distinct token").not.toBe(body?.sessionToken);
|
|
33
|
+
});
|
|
34
|
+
test("auth gates: unauthenticated 401 and missing ai_assistant.view 403", async ({ request, baseURL }) => {
|
|
35
|
+
test.slow();
|
|
36
|
+
const adminToken = await getAuthToken(request, "admin");
|
|
37
|
+
const { organizationId } = getTokenScope(adminToken);
|
|
38
|
+
const stamp = randomUUID().slice(0, 8);
|
|
39
|
+
const password = "Secret123!";
|
|
40
|
+
const anon = await playwrightRequest.newContext({ baseURL });
|
|
41
|
+
try {
|
|
42
|
+
const res = await anon.fetch(SESSION_KEY, { method: "POST", data: "{}" });
|
|
43
|
+
expect(res.status(), "unauthenticated POST is 401").toBe(401);
|
|
44
|
+
} finally {
|
|
45
|
+
await anon.dispose();
|
|
46
|
+
}
|
|
47
|
+
let roleId = null;
|
|
48
|
+
let userId = null;
|
|
49
|
+
try {
|
|
50
|
+
roleId = await createRoleFixture(request, adminToken, { name: `IT Session Role ${stamp}` });
|
|
51
|
+
userId = await createUserFixture(request, adminToken, {
|
|
52
|
+
email: `it-session-${stamp}@example.com`,
|
|
53
|
+
password,
|
|
54
|
+
organizationId,
|
|
55
|
+
roles: [roleId]
|
|
56
|
+
});
|
|
57
|
+
await setUserAclVisibility(request, adminToken, { userId, features: [], organizations: null });
|
|
58
|
+
const viewlessToken = await getAuthToken(request, `it-session-${stamp}@example.com`, password);
|
|
59
|
+
const denied = await apiRequest(request, "POST", SESSION_KEY, { token: viewlessToken, data: {} });
|
|
60
|
+
expect(denied.status(), "caller without ai_assistant.view is 403").toBe(403);
|
|
61
|
+
} finally {
|
|
62
|
+
await deleteUserAclInDb(userId ?? "").catch(() => void 0);
|
|
63
|
+
await deleteUserIfExists(request, adminToken, userId);
|
|
64
|
+
await deleteRoleIfExists(request, adminToken, roleId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=TC-AI-SESSION-KEY-006-create-session-token.spec.js.map
|