@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.30
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/CHANGELOG.md +6 -47
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +12 -13
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/import-agent.d.ts +13 -13
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
- package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
- package/dist/actions/inspect-connect-agent-card.js +15 -0
- package/dist/actions/inspect-connect-agent-card.js.map +1 -0
- package/dist/actions/install-curated-community-app.d.ts +36 -0
- package/dist/actions/install-curated-community-app.d.ts.map +1 -0
- package/dist/actions/install-curated-community-app.js +26 -0
- package/dist/actions/install-curated-community-app.js.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.js +15 -0
- package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
- package/dist/actions/list-connected-agents.d.ts +2 -0
- package/dist/actions/list-curated-community-apps.d.ts +3 -0
- package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
- package/dist/actions/list-curated-community-apps.js +10 -0
- package/dist/actions/list-curated-community-apps.js.map +1 -0
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +6 -0
- package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
- package/dist/actions/update-workspace-app-metadata.js +26 -5
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/community-apps-section.d.ts +2 -0
- package/dist/components/community-apps-section.d.ts.map +1 -0
- package/dist/components/community-apps-section.js +90 -0
- package/dist/components/community-apps-section.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +63 -9
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/connect-apps.d.ts +1 -2
- package/dist/lib/connect-apps.d.ts.map +1 -1
- package/dist/lib/connect-apps.js +41 -65
- package/dist/lib/connect-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +2 -1
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/connect.d.ts.map +1 -1
- package/dist/routes/pages/connect.js +5 -7
- package/dist/routes/pages/connect.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +11 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +258 -39
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.d.ts +45 -0
- package/dist/server/lib/community-apps.d.ts.map +1 -0
- package/dist/server/lib/community-apps.js +267 -0
- package/dist/server/lib/community-apps.js.map +1 -0
- package/dist/server/lib/community-source.d.ts +19 -0
- package/dist/server/lib/community-source.d.ts.map +1 -0
- package/dist/server/lib/community-source.js +118 -0
- package/dist/server/lib/community-source.js.map +1 -0
- package/dist/server/lib/connect-apps.d.ts +4 -0
- package/dist/server/lib/connect-apps.d.ts.map +1 -0
- package/dist/server/lib/connect-apps.js +99 -0
- package/dist/server/lib/connect-apps.js.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.js +44 -0
- package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/labs.d.ts +2 -0
- package/dist/shared/labs.d.ts.map +1 -0
- package/dist/shared/labs.js +8 -0
- package/dist/shared/labs.js.map +1 -0
- package/package.json +5 -4
- package/src/actions/connect-external-agent.spec.ts +101 -3
- package/src/actions/connect-external-agent.ts +12 -11
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +8 -0
- package/src/actions/inspect-connect-agent-card.ts +17 -0
- package/src/actions/install-curated-community-app.ts +32 -0
- package/src/actions/list-connect-marketplace-apps.ts +17 -0
- package/src/actions/list-curated-community-apps.ts +12 -0
- package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
- package/src/actions/update-workspace-app-metadata.ts +48 -24
- package/src/components/community-apps-section.tsx +224 -0
- package/src/components/workspace-app-card.spec.tsx +226 -1
- package/src/components/workspace-app-card.tsx +115 -12
- package/src/lib/connect-apps.spec.ts +118 -66
- package/src/lib/connect-apps.ts +47 -82
- package/src/lib/workspace-apps.ts +1 -0
- package/src/routes/pages/apps.spec.tsx +201 -1
- package/src/routes/pages/apps.tsx +3 -0
- package/src/routes/pages/connect.spec.tsx +53 -6
- package/src/routes/pages/connect.tsx +14 -11
- package/src/server/index.ts +9 -0
- package/src/server/lib/app-creation-store.spec.ts +539 -11
- package/src/server/lib/app-creation-store.ts +332 -41
- package/src/server/lib/community-apps.spec.ts +367 -0
- package/src/server/lib/community-apps.ts +372 -0
- package/src/server/lib/community-source.spec.ts +151 -0
- package/src/server/lib/community-source.ts +186 -0
- package/src/server/lib/connect-apps.ts +149 -0
- package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
- package/src/server/lib/org-apps-directory-cache.ts +66 -0
- package/src/server/plugins/integrations.spec.ts +60 -0
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/labs.ts +9 -0
|
@@ -10,15 +10,21 @@ import {
|
|
|
10
10
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
+
archiveWorkspaceApp,
|
|
13
14
|
generateWorkspaceAppDescription,
|
|
14
15
|
getAppCreationSettings,
|
|
15
16
|
listAvailableWorkspaceTemplates,
|
|
17
|
+
listOrganizationVisibleWorkspaceApps,
|
|
16
18
|
listWorkspaceApps,
|
|
17
19
|
scaffoldWorkspaceAppFromTemplate,
|
|
18
20
|
setAppCreationSettings,
|
|
19
21
|
startWorkspaceAppCreation,
|
|
20
22
|
updateWorkspaceAppMetadata,
|
|
21
23
|
} from "./app-creation-store.js";
|
|
24
|
+
import {
|
|
25
|
+
orgAppsDirectoryAccessCache,
|
|
26
|
+
orgAppsDirectoryCache,
|
|
27
|
+
} from "./org-apps-directory-cache.js";
|
|
22
28
|
|
|
23
29
|
const originalFetch = globalThis.fetch;
|
|
24
30
|
const settingsKey = "dispatch-app-creation-settings:user:dev@example.test";
|
|
@@ -27,6 +33,7 @@ const mocks = vi.hoisted(() => {
|
|
|
27
33
|
const settings = new Map<string, unknown>();
|
|
28
34
|
const state = {
|
|
29
35
|
orgRole: "admin" as string | null,
|
|
36
|
+
workspaceApps: [] as Record<string, unknown>[],
|
|
30
37
|
};
|
|
31
38
|
const executedSql: string[] = [];
|
|
32
39
|
return {
|
|
@@ -62,6 +69,20 @@ const mocks = vi.hoisted(() => {
|
|
|
62
69
|
? statement
|
|
63
70
|
: String((statement as { sql?: unknown })?.sql ?? "");
|
|
64
71
|
executedSql.push(sql);
|
|
72
|
+
if (sql.includes("SELECT id, org_id, visibility, org_enabled")) {
|
|
73
|
+
const args = (statement as { args?: unknown[] }).args ?? [];
|
|
74
|
+
return {
|
|
75
|
+
rows: state.workspaceApps.filter((app) => args.includes(app.id)),
|
|
76
|
+
rowsAffected: 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (sql.includes("SELECT id, owner_email, org_id, visibility")) {
|
|
80
|
+
const args = (statement as { args?: unknown[] }).args ?? [];
|
|
81
|
+
return {
|
|
82
|
+
rows: state.workspaceApps.filter((app) => args.includes(app.id)),
|
|
83
|
+
rowsAffected: 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
65
86
|
if (sql.includes("SELECT id FROM workspace_apps")) {
|
|
66
87
|
return { rows: [], rowsAffected: 0 };
|
|
67
88
|
}
|
|
@@ -71,6 +92,22 @@ const mocks = vi.hoisted(() => {
|
|
|
71
92
|
};
|
|
72
93
|
}),
|
|
73
94
|
})),
|
|
95
|
+
getDb: vi.fn(() => ({
|
|
96
|
+
select: vi.fn(() => ({
|
|
97
|
+
from: vi.fn(() => ({
|
|
98
|
+
where: vi.fn(async () =>
|
|
99
|
+
state.workspaceApps
|
|
100
|
+
.filter(
|
|
101
|
+
(app) =>
|
|
102
|
+
app.org_id === "org-123" &&
|
|
103
|
+
app.visibility === "org" &&
|
|
104
|
+
app.org_enabled === 1,
|
|
105
|
+
)
|
|
106
|
+
.map((app) => ({ id: String(app.id) })),
|
|
107
|
+
),
|
|
108
|
+
})),
|
|
109
|
+
})),
|
|
110
|
+
})),
|
|
74
111
|
resolveBuilderCredentialsDetailed: vi.fn(async () => ({
|
|
75
112
|
privateKey: null as string | null,
|
|
76
113
|
publicKey: null as string | null,
|
|
@@ -94,6 +131,11 @@ const mocks = vi.hoisted(() => {
|
|
|
94
131
|
};
|
|
95
132
|
});
|
|
96
133
|
|
|
134
|
+
vi.mock("../../db/index.js", async (importOriginal) => {
|
|
135
|
+
const actual = await importOriginal<typeof import("../../db/index.js")>();
|
|
136
|
+
return { ...actual, getDb: mocks.getDb };
|
|
137
|
+
});
|
|
138
|
+
|
|
97
139
|
vi.mock("@agent-native/core/secrets", async (importOriginal) => {
|
|
98
140
|
const actual =
|
|
99
141
|
await importOriginal<typeof import("@agent-native/core/secrets")>();
|
|
@@ -187,6 +229,9 @@ afterEach(() => {
|
|
|
187
229
|
},
|
|
188
230
|
);
|
|
189
231
|
mocks.state.orgRole = "admin";
|
|
232
|
+
mocks.state.workspaceApps = [];
|
|
233
|
+
orgAppsDirectoryCache.clear();
|
|
234
|
+
orgAppsDirectoryAccessCache.clear();
|
|
190
235
|
mocks.getDbExec.mockReset();
|
|
191
236
|
mocks.getDbExec.mockImplementation(() => ({
|
|
192
237
|
execute: vi.fn(async (statement: unknown) => {
|
|
@@ -195,6 +240,24 @@ afterEach(() => {
|
|
|
195
240
|
? statement
|
|
196
241
|
: String((statement as { sql?: unknown })?.sql ?? "");
|
|
197
242
|
mocks.executedSql.push(sql);
|
|
243
|
+
if (sql.includes("SELECT id, org_id, visibility, org_enabled")) {
|
|
244
|
+
const args = (statement as { args?: unknown[] }).args ?? [];
|
|
245
|
+
return {
|
|
246
|
+
rows: mocks.state.workspaceApps.filter((app) =>
|
|
247
|
+
args.includes(app.id),
|
|
248
|
+
),
|
|
249
|
+
rowsAffected: 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (sql.includes("SELECT id, owner_email, org_id, visibility")) {
|
|
253
|
+
const args = (statement as { args?: unknown[] }).args ?? [];
|
|
254
|
+
return {
|
|
255
|
+
rows: mocks.state.workspaceApps.filter((app) =>
|
|
256
|
+
args.includes(app.id),
|
|
257
|
+
),
|
|
258
|
+
rowsAffected: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
198
261
|
if (sql.includes("SELECT id FROM workspace_apps")) {
|
|
199
262
|
return { rows: [], rowsAffected: 0 };
|
|
200
263
|
}
|
|
@@ -373,6 +436,71 @@ describe("listWorkspaceApps", () => {
|
|
|
373
436
|
]);
|
|
374
437
|
});
|
|
375
438
|
|
|
439
|
+
it("lists only ready org-visible apps for an organization principal", async () => {
|
|
440
|
+
stubNoPendingContext();
|
|
441
|
+
stubManifest([
|
|
442
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
443
|
+
{ id: "qr", name: "QR", path: "/qr" },
|
|
444
|
+
{ id: "private-app", name: "Private", path: "/private-app" },
|
|
445
|
+
{ id: "disabled-app", name: "Disabled", path: "/disabled-app" },
|
|
446
|
+
{ id: "foreign-app", name: "Foreign", path: "/foreign-app" },
|
|
447
|
+
]);
|
|
448
|
+
mocks.settings.set("dispatch-app-creation-settings:org:org-123", {
|
|
449
|
+
pendingApps: [pendingApp("pending-app")],
|
|
450
|
+
});
|
|
451
|
+
mocks.state.workspaceApps = [
|
|
452
|
+
{
|
|
453
|
+
id: "dispatch",
|
|
454
|
+
owner_email: "owner@example.test",
|
|
455
|
+
org_id: "org-123",
|
|
456
|
+
visibility: "org",
|
|
457
|
+
org_enabled: 1,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: "qr",
|
|
461
|
+
owner_email: "owner@example.test",
|
|
462
|
+
org_id: "org-123",
|
|
463
|
+
visibility: "org",
|
|
464
|
+
org_enabled: 1,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: "private-app",
|
|
468
|
+
owner_email: "dev@example.test",
|
|
469
|
+
org_id: "org-123",
|
|
470
|
+
visibility: "private",
|
|
471
|
+
org_enabled: 1,
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "disabled-app",
|
|
475
|
+
owner_email: "owner@example.test",
|
|
476
|
+
org_id: "org-123",
|
|
477
|
+
visibility: "org",
|
|
478
|
+
org_enabled: 0,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: "foreign-app",
|
|
482
|
+
owner_email: "owner@example.test",
|
|
483
|
+
org_id: "another-org",
|
|
484
|
+
visibility: "org",
|
|
485
|
+
org_enabled: 1,
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
const apps = await runWithRequestContext({ orgId: "org-123" }, () =>
|
|
490
|
+
listOrganizationVisibleWorkspaceApps(),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
expect(apps.map((app) => app.id)).toEqual(["dispatch", "qr"]);
|
|
494
|
+
expect(mocks.executedSql.some((sql) => sql.includes("INSERT INTO"))).toBe(
|
|
495
|
+
false,
|
|
496
|
+
);
|
|
497
|
+
expect(
|
|
498
|
+
mocks.executedSql.some((sql) =>
|
|
499
|
+
sql.includes("INSERT INTO workspace_apps"),
|
|
500
|
+
),
|
|
501
|
+
).toBe(false);
|
|
502
|
+
});
|
|
503
|
+
|
|
376
504
|
it.each([401, 403])(
|
|
377
505
|
"keeps a local gateway denial on the unverified fallback path (%i)",
|
|
378
506
|
async (status) => {
|
|
@@ -1508,6 +1636,51 @@ describe("listWorkspaceApps", () => {
|
|
|
1508
1636
|
);
|
|
1509
1637
|
});
|
|
1510
1638
|
|
|
1639
|
+
it("keeps the curated source id on a pending app summary", async () => {
|
|
1640
|
+
stubNoPendingContext();
|
|
1641
|
+
stubManifest();
|
|
1642
|
+
mocks.settings.set(settingsKey, {
|
|
1643
|
+
pendingApps: [
|
|
1644
|
+
pendingApp("community-qr", {
|
|
1645
|
+
communitySourceId: "community-qr",
|
|
1646
|
+
}),
|
|
1647
|
+
],
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
const apps = await runWithRequestContext(
|
|
1651
|
+
{ userEmail: "dev@example.test" },
|
|
1652
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
1653
|
+
);
|
|
1654
|
+
|
|
1655
|
+
expect(apps.find((app) => app.id === "community-qr")).toMatchObject({
|
|
1656
|
+
status: "pending",
|
|
1657
|
+
communitySourceId: "community-qr",
|
|
1658
|
+
});
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
it("keeps the curated source id on ready app metadata", async () => {
|
|
1662
|
+
stubNoPendingContext();
|
|
1663
|
+
stubManifest([
|
|
1664
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
1665
|
+
{ id: "community-qr", name: "QR", path: "/community-qr" },
|
|
1666
|
+
]);
|
|
1667
|
+
mocks.settings.set("workspace-app-metadata:user:dev@example.test", {
|
|
1668
|
+
apps: {
|
|
1669
|
+
"community-qr": { communitySourceId: "community-qr" },
|
|
1670
|
+
},
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
const apps = await runWithRequestContext(
|
|
1674
|
+
{ userEmail: "dev@example.test" },
|
|
1675
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1678
|
+
expect(apps.find((app) => app.id === "community-qr")).toMatchObject({
|
|
1679
|
+
status: "ready",
|
|
1680
|
+
communitySourceId: "community-qr",
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1511
1684
|
it("keeps unscoped legacy pending rows visible when there is no deploy context", async () => {
|
|
1512
1685
|
stubNoPendingContext();
|
|
1513
1686
|
stubManifest();
|
|
@@ -1643,7 +1816,7 @@ describe("listWorkspaceApps", () => {
|
|
|
1643
1816
|
expect(apps.find((app) => app.id === "mail")?.status).toBe("ready");
|
|
1644
1817
|
});
|
|
1645
1818
|
|
|
1646
|
-
it("lets workspace admins update app
|
|
1819
|
+
it("lets workspace admins update app profile metadata", async () => {
|
|
1647
1820
|
stubNoPendingContext();
|
|
1648
1821
|
stubManifest([
|
|
1649
1822
|
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
@@ -1662,24 +1835,26 @@ describe("listWorkspaceApps", () => {
|
|
|
1662
1835
|
appId: "todo",
|
|
1663
1836
|
name: "Todo Board",
|
|
1664
1837
|
description: "Tracks team work.",
|
|
1838
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
1665
1839
|
}),
|
|
1666
1840
|
);
|
|
1667
1841
|
|
|
1668
1842
|
expect(updated.name).toBe("Todo Board");
|
|
1669
1843
|
expect(updated.description).toBe("Tracks team work.");
|
|
1844
|
+
expect(updated.whenToUse).toEqual(["Create a task from a meeting note"]);
|
|
1670
1845
|
expect(mocks.settings.get("workspace-app-metadata:org:org-123")).toEqual({
|
|
1671
1846
|
apps: {
|
|
1672
1847
|
todo: expect.objectContaining({
|
|
1673
1848
|
name: "Todo Board",
|
|
1674
1849
|
description: "Tracks team work.",
|
|
1850
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
1675
1851
|
updatedBy: "dev@example.test",
|
|
1676
1852
|
}),
|
|
1677
1853
|
},
|
|
1678
1854
|
});
|
|
1679
1855
|
});
|
|
1680
1856
|
|
|
1681
|
-
it("
|
|
1682
|
-
mocks.state.orgRole = "member";
|
|
1857
|
+
it("does not overwrite app profile metadata when its settings are unreadable", async () => {
|
|
1683
1858
|
stubNoPendingContext();
|
|
1684
1859
|
stubManifest([
|
|
1685
1860
|
{
|
|
@@ -1689,30 +1864,262 @@ describe("listWorkspaceApps", () => {
|
|
|
1689
1864
|
path: "/todo",
|
|
1690
1865
|
},
|
|
1691
1866
|
]);
|
|
1867
|
+
const key = "workspace-app-metadata:org:org-123";
|
|
1868
|
+
const existing = { apps: { other: { name: "Preserve me" } } };
|
|
1869
|
+
mocks.settings.set(key, existing);
|
|
1870
|
+
let metadataReads = 0;
|
|
1871
|
+
mocks.getSetting.mockImplementation(async (settingKey: string) => {
|
|
1872
|
+
if (settingKey === key && ++metadataReads === 2) {
|
|
1873
|
+
throw new Error("metadata settings unavailable");
|
|
1874
|
+
}
|
|
1875
|
+
return mocks.settings.get(settingKey) ?? null;
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
await expect(
|
|
1879
|
+
runWithRequestContext(
|
|
1880
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
1881
|
+
() => updateWorkspaceAppMetadata({ appId: "todo", name: "Updated" }),
|
|
1882
|
+
),
|
|
1883
|
+
).rejects.toThrow("metadata settings unavailable");
|
|
1884
|
+
|
|
1885
|
+
expect(metadataReads).toBe(2);
|
|
1886
|
+
expect(mocks.putSetting).not.toHaveBeenCalled();
|
|
1887
|
+
expect(mocks.settings.get(key)).toEqual(existing);
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
it("does not overwrite workspace settings when archiving after a read failure", async () => {
|
|
1891
|
+
const key = "dispatch-app-creation-settings:org:org-123";
|
|
1892
|
+
const existing = {
|
|
1893
|
+
builderProjectId: "project-123",
|
|
1894
|
+
pendingCommunityReservations: ["community-qr"],
|
|
1895
|
+
archivedAppIds: [],
|
|
1896
|
+
};
|
|
1897
|
+
mocks.settings.set(key, existing);
|
|
1898
|
+
mocks.getSetting.mockRejectedValueOnce(
|
|
1899
|
+
new Error("workspace settings unavailable"),
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
await expect(
|
|
1903
|
+
runWithRequestContext(
|
|
1904
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
1905
|
+
() => archiveWorkspaceApp({ appId: "todo" }),
|
|
1906
|
+
),
|
|
1907
|
+
).rejects.toThrow("workspace settings unavailable");
|
|
1908
|
+
|
|
1909
|
+
expect(mocks.putSetting).not.toHaveBeenCalled();
|
|
1910
|
+
expect(mocks.settings.get(key)).toEqual(existing);
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
it("preserves generated name and description when only examples change", async () => {
|
|
1914
|
+
stubNoPendingContext();
|
|
1915
|
+
stubManifest([
|
|
1916
|
+
{
|
|
1917
|
+
id: "todo",
|
|
1918
|
+
name: "Todo",
|
|
1919
|
+
description: "Manifest description",
|
|
1920
|
+
path: "/todo",
|
|
1921
|
+
},
|
|
1922
|
+
]);
|
|
1923
|
+
mocks.settings.set("workspace-app-metadata:org:org-123", {
|
|
1924
|
+
apps: {
|
|
1925
|
+
todo: {
|
|
1926
|
+
description: "Generated description",
|
|
1927
|
+
generated: true,
|
|
1928
|
+
},
|
|
1929
|
+
},
|
|
1930
|
+
});
|
|
1692
1931
|
|
|
1693
1932
|
const updated = await runWithRequestContext(
|
|
1694
1933
|
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
1695
1934
|
() =>
|
|
1696
1935
|
updateWorkspaceAppMetadata({
|
|
1697
1936
|
appId: "todo",
|
|
1698
|
-
|
|
1699
|
-
description: "Tracks team work.",
|
|
1937
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
1700
1938
|
}),
|
|
1701
1939
|
);
|
|
1702
1940
|
|
|
1703
|
-
expect(updated).
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
});
|
|
1941
|
+
expect(updated.name).toBe("Todo");
|
|
1942
|
+
expect(updated.description).toBe("Manifest description");
|
|
1943
|
+
expect(updated.whenToUse).toEqual(["Create a task from a meeting note"]);
|
|
1707
1944
|
expect(mocks.settings.get("workspace-app-metadata:org:org-123")).toEqual({
|
|
1708
1945
|
apps: {
|
|
1709
1946
|
todo: expect.objectContaining({
|
|
1710
|
-
|
|
1711
|
-
|
|
1947
|
+
description: "Generated description",
|
|
1948
|
+
generated: true,
|
|
1949
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
1712
1950
|
updatedBy: "dev@example.test",
|
|
1713
1951
|
}),
|
|
1714
1952
|
},
|
|
1715
1953
|
});
|
|
1954
|
+
expect(
|
|
1955
|
+
(mocks.settings.get("workspace-app-metadata:org:org-123") as any).apps
|
|
1956
|
+
.todo,
|
|
1957
|
+
).not.toHaveProperty("name");
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
it("invalidates org directory discovery after a profile edit", async () => {
|
|
1961
|
+
stubNoPendingContext();
|
|
1962
|
+
stubManifest([
|
|
1963
|
+
{ id: "todo", name: "Todo", description: "Tasks", path: "/todo" },
|
|
1964
|
+
]);
|
|
1965
|
+
const load = vi.fn(async () => [
|
|
1966
|
+
{
|
|
1967
|
+
id: "todo",
|
|
1968
|
+
name: "Old profile",
|
|
1969
|
+
url: "https://todo.example.test",
|
|
1970
|
+
},
|
|
1971
|
+
]);
|
|
1972
|
+
await orgAppsDirectoryCache.get("org-123|organization", load);
|
|
1973
|
+
const loadAccess = vi.fn(async () => new Set(["todo"]));
|
|
1974
|
+
await orgAppsDirectoryAccessCache.get("org-123|organization", loadAccess);
|
|
1975
|
+
|
|
1976
|
+
await runWithRequestContext(
|
|
1977
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
1978
|
+
() =>
|
|
1979
|
+
updateWorkspaceAppMetadata({
|
|
1980
|
+
appId: "todo",
|
|
1981
|
+
description: "Updated profile",
|
|
1982
|
+
}),
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
const refreshedLoad = vi.fn(async () => [
|
|
1986
|
+
{
|
|
1987
|
+
id: "todo",
|
|
1988
|
+
name: "Updated profile",
|
|
1989
|
+
url: "https://todo.example.test",
|
|
1990
|
+
},
|
|
1991
|
+
]);
|
|
1992
|
+
await expect(
|
|
1993
|
+
orgAppsDirectoryCache.get("org-123|organization", refreshedLoad),
|
|
1994
|
+
).resolves.toEqual([
|
|
1995
|
+
{
|
|
1996
|
+
id: "todo",
|
|
1997
|
+
name: "Updated profile",
|
|
1998
|
+
url: "https://todo.example.test",
|
|
1999
|
+
},
|
|
2000
|
+
]);
|
|
2001
|
+
expect(refreshedLoad).toHaveBeenCalledOnce();
|
|
2002
|
+
|
|
2003
|
+
const refreshedAccess = vi.fn(async () => new Set(["todo"]));
|
|
2004
|
+
await expect(
|
|
2005
|
+
orgAppsDirectoryAccessCache.get("org-123|organization", refreshedAccess),
|
|
2006
|
+
).resolves.toEqual(new Set(["todo"]));
|
|
2007
|
+
expect(refreshedAccess).toHaveBeenCalledOnce();
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
it("clears explicit null metadata and preserves an empty when-to-use list", async () => {
|
|
2011
|
+
stubNoPendingContext();
|
|
2012
|
+
stubManifest([
|
|
2013
|
+
{
|
|
2014
|
+
id: "todo",
|
|
2015
|
+
name: "Manifest name",
|
|
2016
|
+
description: "Manifest description",
|
|
2017
|
+
path: "/todo",
|
|
2018
|
+
},
|
|
2019
|
+
]);
|
|
2020
|
+
mocks.settings.set("workspace-app-metadata:org:org-123", {
|
|
2021
|
+
apps: {
|
|
2022
|
+
todo: {
|
|
2023
|
+
name: "Override name",
|
|
2024
|
+
description: "Override description",
|
|
2025
|
+
whenToUse: ["Old example"],
|
|
2026
|
+
},
|
|
2027
|
+
},
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
await runWithRequestContext(
|
|
2031
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2032
|
+
() =>
|
|
2033
|
+
updateWorkspaceAppMetadata({
|
|
2034
|
+
appId: "todo",
|
|
2035
|
+
name: null,
|
|
2036
|
+
description: null,
|
|
2037
|
+
whenToUse: [],
|
|
2038
|
+
}),
|
|
2039
|
+
);
|
|
2040
|
+
|
|
2041
|
+
expect(mocks.settings.get("workspace-app-metadata:org:org-123")).toEqual({
|
|
2042
|
+
apps: {
|
|
2043
|
+
todo: expect.objectContaining({ whenToUse: [] }),
|
|
2044
|
+
},
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
it("reports owner-role lookup failures as unavailable instead of unauthorized", async () => {
|
|
2049
|
+
stubNoPendingContext();
|
|
2050
|
+
stubManifest([{ id: "todo", name: "Todo", path: "/todo" }]);
|
|
2051
|
+
mocks.getDbExec.mockReturnValue({
|
|
2052
|
+
execute: vi.fn(async () => {
|
|
2053
|
+
throw new Error("database unavailable");
|
|
2054
|
+
}),
|
|
2055
|
+
} as any);
|
|
2056
|
+
|
|
2057
|
+
await expect(
|
|
2058
|
+
runWithRequestContext(
|
|
2059
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2060
|
+
() => updateWorkspaceAppMetadata({ appId: "todo", name: "Updated" }),
|
|
2061
|
+
),
|
|
2062
|
+
).rejects.toMatchObject({
|
|
2063
|
+
name: "AppCreationOwnerRoleUnavailableError",
|
|
2064
|
+
statusCode: 503,
|
|
2065
|
+
});
|
|
2066
|
+
expect(mocks.settings.has("workspace-app-metadata:org:org-123")).toBe(
|
|
2067
|
+
false,
|
|
2068
|
+
);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
it("rejects malformed profile examples before writing", async () => {
|
|
2072
|
+
stubNoPendingContext();
|
|
2073
|
+
stubManifest([
|
|
2074
|
+
{ id: "todo", name: "Todo", description: "Tasks", path: "/todo" },
|
|
2075
|
+
]);
|
|
2076
|
+
const tooManyExamples = Array.from(
|
|
2077
|
+
{ length: 7 },
|
|
2078
|
+
(_, index) => `Example ${index}`,
|
|
2079
|
+
);
|
|
2080
|
+
|
|
2081
|
+
await expect(
|
|
2082
|
+
runWithRequestContext(
|
|
2083
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2084
|
+
() =>
|
|
2085
|
+
updateWorkspaceAppMetadata({
|
|
2086
|
+
appId: "todo",
|
|
2087
|
+
whenToUse: tooManyExamples,
|
|
2088
|
+
}),
|
|
2089
|
+
),
|
|
2090
|
+
).rejects.toThrow(/up to 6 non-empty examples/);
|
|
2091
|
+
expect(mocks.settings.has("workspace-app-metadata:org:org-123")).toBe(
|
|
2092
|
+
false,
|
|
2093
|
+
);
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
it("rejects workspace member updates to app profile metadata", async () => {
|
|
2097
|
+
mocks.state.orgRole = "member";
|
|
2098
|
+
stubNoPendingContext();
|
|
2099
|
+
stubManifest([
|
|
2100
|
+
{
|
|
2101
|
+
id: "todo",
|
|
2102
|
+
name: "Todo",
|
|
2103
|
+
description: "Original description",
|
|
2104
|
+
path: "/todo",
|
|
2105
|
+
},
|
|
2106
|
+
]);
|
|
2107
|
+
|
|
2108
|
+
await expect(
|
|
2109
|
+
runWithRequestContext(
|
|
2110
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2111
|
+
() =>
|
|
2112
|
+
updateWorkspaceAppMetadata({
|
|
2113
|
+
appId: "todo",
|
|
2114
|
+
name: "Todo Board",
|
|
2115
|
+
description: "Tracks team work.",
|
|
2116
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
2117
|
+
}),
|
|
2118
|
+
),
|
|
2119
|
+
).rejects.toMatchObject({ statusCode: 403 });
|
|
2120
|
+
expect(mocks.settings.has("workspace-app-metadata:org:org-123")).toBe(
|
|
2121
|
+
false,
|
|
2122
|
+
);
|
|
1716
2123
|
});
|
|
1717
2124
|
|
|
1718
2125
|
it("generates a concise seed description from an app prompt", () => {
|
|
@@ -1812,6 +2219,82 @@ describe("startWorkspaceAppCreation", () => {
|
|
|
1812
2219
|
});
|
|
1813
2220
|
});
|
|
1814
2221
|
|
|
2222
|
+
it("persists the curated source id for local-agent app creation", async () => {
|
|
2223
|
+
const result = await runWithRequestContext(
|
|
2224
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2225
|
+
() =>
|
|
2226
|
+
startWorkspaceAppCreation({
|
|
2227
|
+
prompt: "Install the curated QR template",
|
|
2228
|
+
appId: "community-qr",
|
|
2229
|
+
communitySourceId: "community-qr",
|
|
2230
|
+
attachments: [
|
|
2231
|
+
{
|
|
2232
|
+
type: "upload",
|
|
2233
|
+
contentType: "text/plain",
|
|
2234
|
+
name: "community-qr/README.md",
|
|
2235
|
+
text: "Pinned template source",
|
|
2236
|
+
dataUrl: "",
|
|
2237
|
+
id: "community-qr:README.md",
|
|
2238
|
+
},
|
|
2239
|
+
],
|
|
2240
|
+
}),
|
|
2241
|
+
);
|
|
2242
|
+
|
|
2243
|
+
expect(result).toMatchObject({
|
|
2244
|
+
mode: "local-agent",
|
|
2245
|
+
attachments: [
|
|
2246
|
+
{
|
|
2247
|
+
type: "file",
|
|
2248
|
+
contentType: "text/plain",
|
|
2249
|
+
name: "community-qr/README.md",
|
|
2250
|
+
text: "Pinned template source",
|
|
2251
|
+
},
|
|
2252
|
+
],
|
|
2253
|
+
});
|
|
2254
|
+
expect(
|
|
2255
|
+
mocks.settings.get("workspace-app-metadata:org:org-123"),
|
|
2256
|
+
).toMatchObject({
|
|
2257
|
+
apps: {
|
|
2258
|
+
"community-qr": { communitySourceId: "community-qr" },
|
|
2259
|
+
},
|
|
2260
|
+
});
|
|
2261
|
+
expect(
|
|
2262
|
+
mocks.settings.get("dispatch-app-creation-settings:org:org-123"),
|
|
2263
|
+
).toMatchObject({
|
|
2264
|
+
pendingApps: [
|
|
2265
|
+
expect.objectContaining({
|
|
2266
|
+
id: "community-qr",
|
|
2267
|
+
communitySourceId: "community-qr",
|
|
2268
|
+
createdBy: "dev@example.test",
|
|
2269
|
+
}),
|
|
2270
|
+
],
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
it("atomically reserves local community installs against duplicate requests", async () => {
|
|
2275
|
+
const input = {
|
|
2276
|
+
prompt: "Install the curated QR template",
|
|
2277
|
+
appId: "community-qr",
|
|
2278
|
+
communitySourceId: "community-qr",
|
|
2279
|
+
};
|
|
2280
|
+
const context = { userEmail: "dev@example.test", orgId: "org-123" };
|
|
2281
|
+
|
|
2282
|
+
const first = await runWithRequestContext(context, () =>
|
|
2283
|
+
startWorkspaceAppCreation(input),
|
|
2284
|
+
);
|
|
2285
|
+
const duplicate = await runWithRequestContext(context, () =>
|
|
2286
|
+
startWorkspaceAppCreation(input),
|
|
2287
|
+
);
|
|
2288
|
+
|
|
2289
|
+
expect(first.mode).toBe("local-agent");
|
|
2290
|
+
expect(duplicate).toMatchObject({
|
|
2291
|
+
mode: "app-id-taken",
|
|
2292
|
+
appId: "community-qr",
|
|
2293
|
+
conflict: "pending",
|
|
2294
|
+
owner: "dev@example.test",
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
|
|
1815
2298
|
it("rejects a cross-member collision with an active pending app id", async () => {
|
|
1816
2299
|
stubHostedRuntime();
|
|
1817
2300
|
stubBuilderProjectConfigured();
|
|
@@ -1961,6 +2444,51 @@ describe("startWorkspaceAppCreation", () => {
|
|
|
1961
2444
|
expect(mocks.mutateSetting).toHaveBeenCalled();
|
|
1962
2445
|
});
|
|
1963
2446
|
|
|
2447
|
+
it("persists the curated source id with a hosted pending reservation", async () => {
|
|
2448
|
+
stubHostedRuntime();
|
|
2449
|
+
stubBuilderProjectConfigured();
|
|
2450
|
+
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
|
|
2451
|
+
credentials({
|
|
2452
|
+
privateKey: "priv",
|
|
2453
|
+
publicKey: "pub",
|
|
2454
|
+
userId: "builder-user-42",
|
|
2455
|
+
}),
|
|
2456
|
+
);
|
|
2457
|
+
mocks.runBuilderAgent.mockResolvedValue({
|
|
2458
|
+
branchName: "community-qr1",
|
|
2459
|
+
url: "https://builder.io/app/projects/project-1/community-qr1",
|
|
2460
|
+
status: "processing",
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
await runWithRequestContext(
|
|
2464
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
2465
|
+
() =>
|
|
2466
|
+
startWorkspaceAppCreation({
|
|
2467
|
+
prompt: "Install the curated QR template",
|
|
2468
|
+
appId: "community-qr",
|
|
2469
|
+
communitySourceId: "community-qr",
|
|
2470
|
+
}),
|
|
2471
|
+
);
|
|
2472
|
+
|
|
2473
|
+
expect(
|
|
2474
|
+
mocks.settings.get("dispatch-app-creation-settings:org:org-123"),
|
|
2475
|
+
).toMatchObject({
|
|
2476
|
+
pendingApps: [
|
|
2477
|
+
expect.objectContaining({
|
|
2478
|
+
id: "community-qr",
|
|
2479
|
+
communitySourceId: "community-qr",
|
|
2480
|
+
}),
|
|
2481
|
+
],
|
|
2482
|
+
});
|
|
2483
|
+
expect(
|
|
2484
|
+
mocks.settings.get("workspace-app-metadata:org:org-123"),
|
|
2485
|
+
).toMatchObject({
|
|
2486
|
+
apps: {
|
|
2487
|
+
"community-qr": { communitySourceId: "community-qr" },
|
|
2488
|
+
},
|
|
2489
|
+
});
|
|
2490
|
+
});
|
|
2491
|
+
|
|
1964
2492
|
it("releases the reservation when Builder handoff fails", async () => {
|
|
1965
2493
|
stubHostedRuntime();
|
|
1966
2494
|
stubBuilderProjectConfigured();
|