@desplega.ai/agent-swarm 2.4.0 → 2.4.1
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/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-zvzr538d.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-jhtqdw69.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-bd84rhs2.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-kreq1gwg.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-3k22sbsq.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-e53x6b9v.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-f6z923ny.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-0538vsb7.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-6wxqkfk2.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gfts5044.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-56besmfz.js} +13 -13
- package/dist/{cli-nrrg0fvy.js → cli-0bh477zn.js} +18 -5
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-1cevz46c.js → cli-3kh4kpqy.js} +6 -6
- package/dist/{cli-tg9ksbn8.js → cli-3p8fqcnh.js} +2 -2
- package/dist/{cli-x1p5dhq2.js → cli-3r448dqn.js} +3 -3
- package/dist/{cli-ecshf504.js → cli-3tnpj2bh.js} +7 -7
- package/dist/{cli-ffbrkh1r.js → cli-471qk6s2.js} +46 -38
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-6ncjvx55.js} +3 -3
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mz1ezzzh.js → cli-ash6xxk7.js} +2 -2
- package/dist/{cli-rsd7ga7v.js → cli-axh29tyr.js} +11 -11
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-6ftrrwm1.js → cli-bn5kevnf.js} +5 -5
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-8cxkw1f5.js → cli-fz9kewbh.js} +2 -2
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-kz116kvw.js → cli-gf0kfg81.js} +7 -7
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-ywyrffff.js → cli-jh0qcrga.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-k9z4zd4p.js} +6 -5
- package/dist/{cli-nme7wery.js → cli-kmqgmcnb.js} +46 -13
- package/dist/{cli-x719j3vs.js → cli-kr2pmfqx.js} +392 -168
- package/dist/{cli-wrv09v7q.js → cli-m2qgh5sv.js} +1 -1
- package/dist/{cli-xfe63zx3.js → cli-mfe7f7b5.js} +2 -2
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-5s5hx57y.js → cli-nx462e4v.js} +1 -1
- package/dist/{cli-bfc3k1x5.js → cli-p0rakbrk.js} +9 -9
- package/dist/{cli-avmqhatn.js → cli-p6argxh9.js} +8 -8
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-rdq7tzwm.js} +13 -12
- package/dist/{cli-rg7ntypm.js → cli-rqqsfz30.js} +379 -87
- package/dist/{cli-kk2r80n5.js → cli-s48yvzb8.js} +4 -4
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-b80a1100.js → cli-sjyetj21.js} +2 -2
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-syhq378v.js} +2 -2
- package/dist/{cli-tj4j2ts4.js → cli-tbhwrxpt.js} +8 -8
- package/dist/{cli-4fhyq711.js → cli-vy0fay6k.js} +13 -13
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-tgvpmnwz.js → cli-xftrvxag.js} +5 -5
- package/dist/{cli-02vhhssk.js → cli-z071fb95.js} +5 -5
- package/dist/{cli-mvg2hf8p.js → cli-z1ew8vbq.js} +14 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-p6rmm7vv.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-8vd5t9t0.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-7ems2p13.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-bg33ae53.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-w505dwa8.js} +2 -2
- package/dist/{db-nc1rd869.js → db-ys2rbdft.js} +3 -3
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0s0mckqr.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-5b23fv8z.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-q9fghe5q.js} +10 -10
- package/dist/handlers-mqrym347.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-tww6yx1w.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-dx0pkvar.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-pvmh9b6m.js} +6 -6
- package/dist/{http-xpmmq028.js → http-k2ejbt05.js} +199 -131
- package/dist/{index-s7ckdajq.js → index-2z710v4x.js} +10 -10
- package/dist/{index-dg2tmj99.js → index-dgz61nff.js} +20 -20
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/index-h2ek08d6.js +40 -0
- package/dist/index-pqfwh9nn.js +41 -0
- package/dist/{index-cdzyfrsp.js → index-x5aktmp8.js} +1 -1
- package/dist/{keepalive-3nr4st3r.js → keepalive-nhvpw10z.js} +4 -4
- package/dist/lead-jb6sf69z.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-agakc519.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-4n2kwa4v.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-tfa12080.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-3cqx3f3x.js} +3 -3
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-9fae5rnv.js} +1 -1
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-y6zbfzve.js} +2 -2
- package/dist/{render-v2-7nqgvk2q.js → render-v2-emawjvc4.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-zhgts7h7.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-cm823e06.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-tkp99w1e.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-f722h263.js} +23 -23
- package/dist/worker-p7f336rq.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +99 -52
- package/package.json +2 -2
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/row-store.ts +4 -2
- package/src/apps/script-types.ts +1 -1
- package/src/be/db.ts +14 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +10 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +84 -181
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/tasks.ts +2 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +14 -3
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +360 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +234 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +41 -2
- package/templates/skills/apps/content.md +41 -2
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
|
-
import { closeDb, createUser, initDb } from "../be/db";
|
|
3
|
+
import { closeDb, createUser, getDbClient, initDb, updateUser, upsertSwarmConfig } from "../be/db";
|
|
4
4
|
import { attachRole, detachRole } from "../be/rbac-roles";
|
|
5
|
+
import { loadGlobalConfigsIntoEnv } from "../http/core";
|
|
5
6
|
import {
|
|
6
7
|
_getOrgIdForTests,
|
|
7
8
|
_resetTelemetryStateForTests,
|
|
8
9
|
initTelemetry,
|
|
10
|
+
setTelemetryFunctionResolver,
|
|
9
11
|
setTelemetryOrgDomain,
|
|
10
12
|
track,
|
|
11
13
|
} from "../telemetry";
|
|
@@ -19,12 +21,22 @@ import {
|
|
|
19
21
|
validOrgDomain,
|
|
20
22
|
validOrgId,
|
|
21
23
|
} from "../telemetry-context";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
_resetTelemetryIdentityForTests,
|
|
26
|
+
cachedUserFunction,
|
|
27
|
+
computeOrgDomain,
|
|
28
|
+
primeUserRole,
|
|
29
|
+
} from "../telemetry-identity";
|
|
23
30
|
|
|
24
31
|
// initTelemetry no-ops when ANONYMIZED_TELEMETRY=false, and the CI env may set it.
|
|
25
32
|
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
26
33
|
|
|
27
34
|
const TEST_DB_PATH = "./test-telemetry-context.sqlite";
|
|
35
|
+
const PROFILE_KEYS = [
|
|
36
|
+
"SWARM_ORG_TEAM_SIZE",
|
|
37
|
+
"SWARM_ORG_USE_CASE",
|
|
38
|
+
"SWARM_ORG_ACQUISITION_SOURCE",
|
|
39
|
+
] as const;
|
|
28
40
|
|
|
29
41
|
async function removeTestDb(): Promise<void> {
|
|
30
42
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -116,6 +128,30 @@ describe("telemetry context helpers", () => {
|
|
|
116
128
|
expect(mapTriggerSurface(null)).toBe("other");
|
|
117
129
|
});
|
|
118
130
|
|
|
131
|
+
test("buildContext omits invalid profile values and a function without an actor", () => {
|
|
132
|
+
const base = {
|
|
133
|
+
orgId: "org_0123456789abcdef",
|
|
134
|
+
installationId: "install_test",
|
|
135
|
+
isCloud: false,
|
|
136
|
+
isE2b: false,
|
|
137
|
+
swarmVersion: "1.2.3",
|
|
138
|
+
};
|
|
139
|
+
const context = buildContext({
|
|
140
|
+
...base,
|
|
141
|
+
teamSize: "invalid",
|
|
142
|
+
useCase: "invalid",
|
|
143
|
+
acquisitionSource: "invalid",
|
|
144
|
+
userFunction: "founder",
|
|
145
|
+
});
|
|
146
|
+
expect(context).not.toHaveProperty("team_size");
|
|
147
|
+
expect(context).not.toHaveProperty("use_case");
|
|
148
|
+
expect(context).not.toHaveProperty("user_function");
|
|
149
|
+
expect(context.acquisition_source).toBeNull();
|
|
150
|
+
expect(
|
|
151
|
+
buildContext({ ...base, actor: { userId: "u-1" }, userFunction: "administrator" }),
|
|
152
|
+
).not.toHaveProperty("user_function");
|
|
153
|
+
});
|
|
154
|
+
|
|
119
155
|
test("normalizeProperties keeps an event alive when one optional value is unsendable", () => {
|
|
120
156
|
const out = normalizeProperties("task.completed", {
|
|
121
157
|
taskId: "t-1",
|
|
@@ -248,6 +284,19 @@ describe("telemetry envelope", () => {
|
|
|
248
284
|
}
|
|
249
285
|
});
|
|
250
286
|
|
|
287
|
+
test("track resolves the actor's job function without changing the RBAC role", async () => {
|
|
288
|
+
const config = memoryConfig({ telemetry_installation_id: "install_function" });
|
|
289
|
+
await initTelemetry("api-server", config.get, config.set, { generateIfMissing: true });
|
|
290
|
+
setTelemetryFunctionResolver(() => "founder");
|
|
291
|
+
track({
|
|
292
|
+
event: "workflow.deleted",
|
|
293
|
+
properties: { workflowId: "wf-1" },
|
|
294
|
+
actor: { userId: "u-1", role: "member" },
|
|
295
|
+
});
|
|
296
|
+
await tick();
|
|
297
|
+
expect(sent[0]?.context).toMatchObject({ user_function: "founder", user_role: "member" });
|
|
298
|
+
});
|
|
299
|
+
|
|
251
300
|
test("an org with no ID yet sends nothing instead of a half-identified event", async () => {
|
|
252
301
|
// A worker on an old server: installation ID known, no org ID stored.
|
|
253
302
|
const config = memoryConfig({ telemetry_installation_id: "install_noorg" });
|
|
@@ -270,18 +319,95 @@ describe("telemetry envelope", () => {
|
|
|
270
319
|
});
|
|
271
320
|
|
|
272
321
|
describe("org email domain", () => {
|
|
322
|
+
const savedProfile = Object.fromEntries(PROFILE_KEYS.map((key) => [key, process.env[key]]));
|
|
323
|
+
|
|
273
324
|
beforeEach(async () => {
|
|
274
325
|
closeDb();
|
|
275
326
|
await removeTestDb();
|
|
276
327
|
initDb(TEST_DB_PATH);
|
|
277
328
|
_resetTelemetryIdentityForTests();
|
|
329
|
+
for (const key of PROFILE_KEYS) delete process.env[key];
|
|
278
330
|
});
|
|
279
331
|
|
|
280
332
|
afterEach(async () => {
|
|
333
|
+
for (const key of PROFILE_KEYS) {
|
|
334
|
+
if (savedProfile[key] === undefined) delete process.env[key];
|
|
335
|
+
else process.env[key] = savedProfile[key];
|
|
336
|
+
}
|
|
281
337
|
closeDb();
|
|
282
338
|
await removeTestDb();
|
|
283
339
|
});
|
|
284
340
|
|
|
341
|
+
test("stored company profile reaches context after reload and clears without restart", async () => {
|
|
342
|
+
const values = ["11-50", "features", "hacker_news"];
|
|
343
|
+
for (const [index, key] of PROFILE_KEYS.entries()) {
|
|
344
|
+
await upsertSwarmConfig({ scope: "global", key, value: values[index] as string });
|
|
345
|
+
}
|
|
346
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
347
|
+
const originalFetch = globalThis.fetch;
|
|
348
|
+
const sent: Array<{ context: Record<string, unknown> }> = [];
|
|
349
|
+
globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
|
|
350
|
+
if (init?.body) sent.push(JSON.parse(init.body));
|
|
351
|
+
return new Response(null, { status: 204 });
|
|
352
|
+
}) as typeof fetch;
|
|
353
|
+
_resetTelemetryStateForTests();
|
|
354
|
+
try {
|
|
355
|
+
const config = memoryConfig({
|
|
356
|
+
telemetry_installation_id: "install_profile",
|
|
357
|
+
telemetry_org_id: "org_0123456789abcdef",
|
|
358
|
+
});
|
|
359
|
+
await initTelemetry("api-server", config.get, config.set);
|
|
360
|
+
track({ event: "server.started", properties: { port: 3013 } });
|
|
361
|
+
expect(sent).toHaveLength(1);
|
|
362
|
+
expect(sent[0]?.context).toMatchObject({
|
|
363
|
+
team_size: "11-50",
|
|
364
|
+
use_case: "features",
|
|
365
|
+
acquisition_source: "hacker_news",
|
|
366
|
+
});
|
|
367
|
+
for (const key of PROFILE_KEYS) {
|
|
368
|
+
await upsertSwarmConfig({ scope: "global", key, value: "" });
|
|
369
|
+
}
|
|
370
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
371
|
+
track({ event: "server.started", properties: { port: 3013 } });
|
|
372
|
+
expect(sent).toHaveLength(2);
|
|
373
|
+
expect(sent[1]?.context).not.toHaveProperty("team_size");
|
|
374
|
+
expect(sent[1]?.context).not.toHaveProperty("use_case");
|
|
375
|
+
expect(sent[1]?.context.acquisition_source).toBeNull();
|
|
376
|
+
} finally {
|
|
377
|
+
globalThis.fetch = originalFetch;
|
|
378
|
+
_resetTelemetryStateForTests();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("job function uses the role cache and invalidates after a committed user update", async () => {
|
|
383
|
+
const user = await createUser({
|
|
384
|
+
name: "Founder",
|
|
385
|
+
role: "admin",
|
|
386
|
+
metadata: { jobFunction: "founder" },
|
|
387
|
+
});
|
|
388
|
+
await primeUserRole(user.id);
|
|
389
|
+
expect(cachedUserFunction(user.id)).toBe("founder");
|
|
390
|
+
await getDbClient().transaction(async () => {
|
|
391
|
+
await updateUser(user.id, { metadata: { jobFunction: "engineer" } });
|
|
392
|
+
expect(cachedUserFunction(user.id)).toBe("founder");
|
|
393
|
+
});
|
|
394
|
+
await primeUserRole(user.id);
|
|
395
|
+
expect(cachedUserFunction(user.id)).toBe("engineer");
|
|
396
|
+
await updateUser(user.id, { metadata: { jobFunction: "a free-text legacy role" } });
|
|
397
|
+
await primeUserRole(user.id);
|
|
398
|
+
expect(cachedUserFunction(user.id)).toBeUndefined();
|
|
399
|
+
const context = buildContext({
|
|
400
|
+
orgId: "org_0123456789abcdef",
|
|
401
|
+
installationId: "install_profile",
|
|
402
|
+
isCloud: false,
|
|
403
|
+
isE2b: false,
|
|
404
|
+
swarmVersion: "1.2.3",
|
|
405
|
+
actor: { userId: user.id },
|
|
406
|
+
userFunction: cachedUserFunction(user.id),
|
|
407
|
+
});
|
|
408
|
+
expect(context).not.toHaveProperty("user_function");
|
|
409
|
+
});
|
|
410
|
+
|
|
285
411
|
test("is undefined with no users", async () => {
|
|
286
412
|
expect(await computeOrgDomain()).toBeUndefined();
|
|
287
413
|
});
|
|
@@ -5,6 +5,7 @@ import { isPublicRoute } from "../http/route-def";
|
|
|
5
5
|
import { handleTelemetryUi } from "../http/telemetry-ui";
|
|
6
6
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
7
7
|
import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
|
|
8
|
+
import catalog from "../telemetry-contract/catalog.json";
|
|
8
9
|
import { UI_EVENT_NAMES, validateUiEvent } from "../telemetry-ui-events";
|
|
9
10
|
import type { HttpRequestAuth } from "../utils/request-auth-context";
|
|
10
11
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
@@ -118,6 +119,128 @@ describe("POST /api/telemetry/ui-event", () => {
|
|
|
118
119
|
expect(sent).toHaveLength(0);
|
|
119
120
|
});
|
|
120
121
|
|
|
122
|
+
test.each([
|
|
123
|
+
{ event: "identity.lookup", properties: { matched: true } },
|
|
124
|
+
...["identity_modal", "setup"].map((surface) => ({
|
|
125
|
+
event: "identity.user_created",
|
|
126
|
+
properties: { surface, email_kind: "work", has_function: true, has_company: true },
|
|
127
|
+
})),
|
|
128
|
+
{
|
|
129
|
+
event: "fleet_benchmark.unlocked",
|
|
130
|
+
properties: { fields: ["website", "team_size", "use_case"] },
|
|
131
|
+
},
|
|
132
|
+
...["compose", "docker", "kubernetes"].map((target) => ({
|
|
133
|
+
event: "setup.worker_command_copied",
|
|
134
|
+
properties: { target, harness: "codex" },
|
|
135
|
+
})),
|
|
136
|
+
...[
|
|
137
|
+
"prompt",
|
|
138
|
+
"setup",
|
|
139
|
+
"settings",
|
|
140
|
+
"org_name_popup",
|
|
141
|
+
"feedback",
|
|
142
|
+
"fleet_benchmark",
|
|
143
|
+
"telemetry_banner",
|
|
144
|
+
"identity_modal",
|
|
145
|
+
].map((surface) => ({
|
|
146
|
+
event: "org.profile_updated",
|
|
147
|
+
properties: {
|
|
148
|
+
fields: ["name", "website", "team_size", "use_case", "acquisition_source"],
|
|
149
|
+
surface,
|
|
150
|
+
},
|
|
151
|
+
})),
|
|
152
|
+
{
|
|
153
|
+
event: "popup.submitted",
|
|
154
|
+
properties: {
|
|
155
|
+
popup: "feedback",
|
|
156
|
+
has_message: false,
|
|
157
|
+
has_contact: false,
|
|
158
|
+
answered: 3,
|
|
159
|
+
perks: ["used_by", "slack_connect"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
...["notification.viewed", "notification.dismissed", "notification.submitted"].flatMap(
|
|
163
|
+
(event) =>
|
|
164
|
+
["task_failed", "first_workflow", "default"].map((moment) => ({
|
|
165
|
+
event,
|
|
166
|
+
properties: {
|
|
167
|
+
key: "slack_connect_invite",
|
|
168
|
+
moment,
|
|
169
|
+
...(event === "notification.submitted" ? { user_function: "engineer" } : {}),
|
|
170
|
+
},
|
|
171
|
+
})),
|
|
172
|
+
),
|
|
173
|
+
])("accepts the company-profile catalog event $event with $properties", async ({
|
|
174
|
+
event,
|
|
175
|
+
properties,
|
|
176
|
+
}) => {
|
|
177
|
+
const result = await post({ event, properties });
|
|
178
|
+
await flush();
|
|
179
|
+
expect(result.statusCode).toBe(204);
|
|
180
|
+
expect(sent).toHaveLength(1);
|
|
181
|
+
expect(sent[0]).toMatchObject({ event, source: "ui", properties });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test.each([
|
|
185
|
+
{
|
|
186
|
+
event: "identity.user_created",
|
|
187
|
+
properties: {
|
|
188
|
+
surface: "invalid",
|
|
189
|
+
email_kind: "work",
|
|
190
|
+
has_function: true,
|
|
191
|
+
has_company: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{ event: "setup.worker_command_copied", properties: { target: "invalid", harness: "codex" } },
|
|
195
|
+
{ event: "org.profile_updated", properties: { fields: ["team_size"], surface: "invalid" } },
|
|
196
|
+
{ event: "org.profile_updated", properties: { fields: ["invalid"], surface: "settings" } },
|
|
197
|
+
{ event: "fleet_benchmark.unlocked", properties: { fields: ["invalid"] } },
|
|
198
|
+
{
|
|
199
|
+
event: "popup.submitted",
|
|
200
|
+
properties: { popup: "feedback", has_message: false, has_contact: false, perks: ["invalid"] },
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
event: "notification.submitted",
|
|
204
|
+
properties: { key: "slack_connect_invite", moment: "invalid" },
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
event: "notification.submitted",
|
|
208
|
+
properties: { key: "slack_connect_invite", user_function: "invalid" },
|
|
209
|
+
},
|
|
210
|
+
])("rejects a bad enum in $event with $properties", async ({ event, properties }) => {
|
|
211
|
+
const result = await post({ event, properties });
|
|
212
|
+
expect(result.statusCode).toBe(400);
|
|
213
|
+
expect(sent).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("array item references enforce the referenced enum", async () => {
|
|
217
|
+
const entry = catalog.events.find((entry) => entry.event === "fleet_benchmark.unlocked")!;
|
|
218
|
+
const fields = entry.properties.fields!;
|
|
219
|
+
const original = fields.items;
|
|
220
|
+
// Exercise a referenced item definition without changing the vendored catalog.
|
|
221
|
+
Object.assign(fields, { items: { ref: "use_case" } });
|
|
222
|
+
try {
|
|
223
|
+
expect(
|
|
224
|
+
(
|
|
225
|
+
await post({
|
|
226
|
+
event: "fleet_benchmark.unlocked",
|
|
227
|
+
properties: { fields: ["features"] },
|
|
228
|
+
})
|
|
229
|
+
).statusCode,
|
|
230
|
+
).toBe(204);
|
|
231
|
+
sent = [];
|
|
232
|
+
const result = await post({
|
|
233
|
+
event: "fleet_benchmark.unlocked",
|
|
234
|
+
properties: { fields: ["invalid"] },
|
|
235
|
+
});
|
|
236
|
+
expect(result.statusCode).toBe(400);
|
|
237
|
+
expect(result.body).toContain("items must be one of");
|
|
238
|
+
expect(sent).toHaveLength(0);
|
|
239
|
+
} finally {
|
|
240
|
+
fields.items = original;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
121
244
|
test("an unknown event name gets 400 and sends nothing", async () => {
|
|
122
245
|
const result = await post({ event: "task.created", properties: {} });
|
|
123
246
|
await flush();
|
|
@@ -376,9 +376,10 @@ describe("Tool Annotations & Classification", () => {
|
|
|
376
376
|
// (this suite forces ALL_CAPABILITIES, including default-disabled ones).
|
|
377
377
|
// Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
|
|
378
378
|
// native Kapso/WhatsApp tools (register/unregister number + send/reply message),
|
|
379
|
-
//
|
|
379
|
+
// the six Chat SDK tools (chat-* + register/unregister-chat-channel),
|
|
380
|
+
// and the seven app row tools plus app-action-run.
|
|
380
381
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
381
|
-
expect(count).toBeLessThanOrEqual(
|
|
382
|
+
expect(count).toBeLessThanOrEqual(169);
|
|
382
383
|
});
|
|
383
384
|
|
|
384
385
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -5,6 +5,7 @@ import { closeDb, createAgent, getSwarmConfigs, initDb, upsertSwarmConfig } from
|
|
|
5
5
|
import { fillOrgLogoFromWebsite } from "../be/org-profile";
|
|
6
6
|
import { handleUsedBy } from "../http/used-by";
|
|
7
7
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
8
|
+
import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
|
|
8
9
|
import type { HttpRequestAuth } from "../utils/request-auth-context";
|
|
9
10
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
10
11
|
|
|
@@ -94,26 +95,155 @@ describe("POST /api/used-by", () => {
|
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
test("a non-lead agent gets 403 and nothing reaches the proxy", async () => {
|
|
97
|
-
const result = await post({ granted: true }, null, WORKER_ID);
|
|
98
|
+
const result = await post({ granted: true, contactEmail: "admin@acme.com" }, null, WORKER_ID);
|
|
98
99
|
expect(result.statusCode).toBe(403);
|
|
99
100
|
expect(proxied).toEqual([]);
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
test("consent telemetry reports quote and function presence without their contents", async () => {
|
|
104
|
+
const savedFlag = process.env.ANONYMIZED_TELEMETRY;
|
|
105
|
+
try {
|
|
106
|
+
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
107
|
+
_resetTelemetryStateForTests();
|
|
108
|
+
await initTelemetry("api-server", configValue, async () => {}, { generateIfMissing: true });
|
|
109
|
+
await post(
|
|
110
|
+
{
|
|
111
|
+
granted: true,
|
|
112
|
+
contactEmail: "admin@acme.com",
|
|
113
|
+
contactFunction: "founder",
|
|
114
|
+
quote: "Private quote",
|
|
115
|
+
},
|
|
116
|
+
OPERATOR,
|
|
117
|
+
);
|
|
118
|
+
await post({ granted: false }, OPERATOR);
|
|
119
|
+
await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
120
|
+
const events = proxied.filter((request) => request.body.event === "used_by.consent_changed");
|
|
121
|
+
expect(events).toHaveLength(3);
|
|
122
|
+
expect(events.map((event) => event.body.properties)).toMatchObject([
|
|
123
|
+
{ granted: true, has_quote: true, has_function: true },
|
|
124
|
+
{ granted: false, has_quote: false, has_function: false },
|
|
125
|
+
{ granted: true, has_quote: false, has_function: false },
|
|
126
|
+
]);
|
|
127
|
+
for (const event of events) {
|
|
128
|
+
expect(JSON.stringify(event.body)).not.toContain("Private quote");
|
|
129
|
+
expect(JSON.stringify(event.body)).not.toContain("admin@acme.com");
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
if (savedFlag === undefined) delete process.env.ANONYMIZED_TELEMETRY;
|
|
133
|
+
else process.env.ANONYMIZED_TELEMETRY = savedFlag;
|
|
134
|
+
_resetTelemetryStateForTests();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
102
138
|
test("grant and revoke both reach the proxy and are stored", async () => {
|
|
103
|
-
const grant = await post(
|
|
139
|
+
const grant = await post(
|
|
140
|
+
{
|
|
141
|
+
granted: true,
|
|
142
|
+
contactEmail: "admin@acme.com",
|
|
143
|
+
contactFunction: "founder",
|
|
144
|
+
quote: "Our swarm ships features.",
|
|
145
|
+
},
|
|
146
|
+
OPERATOR,
|
|
147
|
+
);
|
|
104
148
|
expect(grant.statusCode).toBe(200);
|
|
105
149
|
expect(proxied[0]?.url).toBe("https://proxy.desplega.sh/v1/used-by");
|
|
106
150
|
expect(proxied[0]?.body).toMatchObject({
|
|
107
151
|
org_name: "Acme",
|
|
108
152
|
org_website: "acme.com",
|
|
109
153
|
granted: true,
|
|
154
|
+
contact_email: "admin@acme.com",
|
|
155
|
+
contact_function: "founder",
|
|
156
|
+
quote: "Our swarm ships features.",
|
|
110
157
|
});
|
|
111
158
|
expect(await configValue("SWARM_USED_BY_CONSENT")).toBe("true");
|
|
159
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_EMAIL")).toBe("admin@acme.com");
|
|
160
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_FUNCTION")).toBe("founder");
|
|
161
|
+
expect(await configValue("SWARM_USED_BY_QUOTE")).toBe("Our swarm ships features.");
|
|
162
|
+
// Contact details never reach agent env, and readers see the email masked.
|
|
163
|
+
const rows = await getSwarmConfigs({ scope: "global" });
|
|
164
|
+
const row = (key: string) => rows.find((entry) => entry.key === key);
|
|
165
|
+
for (const key of [
|
|
166
|
+
"SWARM_USED_BY_CONTACT_EMAIL",
|
|
167
|
+
"SWARM_USED_BY_CONTACT_FUNCTION",
|
|
168
|
+
"SWARM_USED_BY_QUOTE",
|
|
169
|
+
]) {
|
|
170
|
+
expect(row(key)?.exportToEnv).toBe(false);
|
|
171
|
+
}
|
|
172
|
+
expect(row("SWARM_USED_BY_CONTACT_EMAIL")?.isSecret).toBe(true);
|
|
112
173
|
|
|
113
174
|
const revoke = await post({ granted: false }, OPERATOR);
|
|
114
175
|
expect(revoke.statusCode).toBe(200);
|
|
115
176
|
expect(proxied[1]?.body).toMatchObject({ granted: false });
|
|
116
177
|
expect(await configValue("SWARM_USED_BY_CONSENT")).toBe("false");
|
|
178
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_EMAIL")).toBe("");
|
|
179
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_FUNCTION")).toBe("");
|
|
180
|
+
expect(await configValue("SWARM_USED_BY_QUOTE")).toBe("");
|
|
181
|
+
expect(proxied[1]?.body).not.toHaveProperty("contact_email");
|
|
182
|
+
expect(proxied[1]?.body).not.toHaveProperty("contact_function");
|
|
183
|
+
expect(proxied[1]?.body).not.toHaveProperty("quote");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a grant requires valid contact details and a quote of at most 280 characters", async () => {
|
|
187
|
+
for (const details of [
|
|
188
|
+
{},
|
|
189
|
+
{ contactEmail: "" },
|
|
190
|
+
{ contactEmail: "invalid" },
|
|
191
|
+
{ contactEmail: "admin@acme.com", contactFunction: "CEO" },
|
|
192
|
+
{ contactEmail: "admin@acme.com", quote: "x".repeat(281) },
|
|
193
|
+
]) {
|
|
194
|
+
const result = await post({ granted: true, ...details }, OPERATOR);
|
|
195
|
+
expect(result.statusCode).toBe(400);
|
|
196
|
+
expect(result.body).toContain("Validation error");
|
|
197
|
+
}
|
|
198
|
+
expect(proxied).toEqual([]);
|
|
199
|
+
expect((await post({ granted: false, contactEmail: "invalid" }, OPERATOR)).statusCode).toBe(
|
|
200
|
+
400,
|
|
201
|
+
);
|
|
202
|
+
expect(proxied).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("contact email accepts 320 characters and rejects longer values", async () => {
|
|
206
|
+
const contactEmail = `${"a".repeat(64)}@${Array(4).fill("b".repeat(63)).join(".")}`;
|
|
207
|
+
expect(contactEmail).toHaveLength(320);
|
|
208
|
+
expect((await post({ granted: true, contactEmail }, OPERATOR)).statusCode).toBe(200);
|
|
209
|
+
proxied = [];
|
|
210
|
+
const result = await post({ granted: true, contactEmail: `a${contactEmail}` }, OPERATOR);
|
|
211
|
+
expect(result.statusCode).toBe(400);
|
|
212
|
+
expect(result.body).toContain("Contact email must be at most 320 characters");
|
|
213
|
+
expect(proxied).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("an empty stored name overrides the deployment name", async () => {
|
|
217
|
+
process.env.SWARM_ORG_NAME = "Deployment name";
|
|
218
|
+
try {
|
|
219
|
+
await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_NAME", value: "" });
|
|
220
|
+
const result = await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
221
|
+
expect(result.statusCode).toBe(400);
|
|
222
|
+
expect(result.body).toContain("Set the organization name and website first");
|
|
223
|
+
expect(proxied).toEqual([]);
|
|
224
|
+
} finally {
|
|
225
|
+
delete process.env.SWARM_ORG_NAME;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a new grant clears omitted optional fields from the previous grant", async () => {
|
|
230
|
+
const result = await post(
|
|
231
|
+
{
|
|
232
|
+
granted: true,
|
|
233
|
+
contactEmail: "admin@acme.com",
|
|
234
|
+
contactFunction: "engineer",
|
|
235
|
+
quote: "x".repeat(280),
|
|
236
|
+
},
|
|
237
|
+
OPERATOR,
|
|
238
|
+
);
|
|
239
|
+
expect(result.statusCode).toBe(200);
|
|
240
|
+
expect(await configValue("SWARM_USED_BY_QUOTE")).toHaveLength(280);
|
|
241
|
+
expect((await post({ granted: true, contactEmail: "new@acme.com" }, OPERATOR)).statusCode).toBe(
|
|
242
|
+
200,
|
|
243
|
+
);
|
|
244
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_EMAIL")).toBe("new@acme.com");
|
|
245
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_FUNCTION")).toBe("");
|
|
246
|
+
expect(await configValue("SWARM_USED_BY_QUOTE")).toBe("");
|
|
117
247
|
});
|
|
118
248
|
|
|
119
249
|
test("with telemetry off the install ID stays home", async () => {
|
|
@@ -124,9 +254,9 @@ describe("POST /api/used-by", () => {
|
|
|
124
254
|
});
|
|
125
255
|
const saved = process.env.ANONYMIZED_TELEMETRY;
|
|
126
256
|
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
127
|
-
await post({ granted: true }, OPERATOR);
|
|
257
|
+
await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
128
258
|
process.env.ANONYMIZED_TELEMETRY = "false";
|
|
129
|
-
await post({ granted: true }, OPERATOR);
|
|
259
|
+
await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
130
260
|
if (saved === undefined) delete process.env.ANONYMIZED_TELEMETRY;
|
|
131
261
|
else process.env.ANONYMIZED_TELEMETRY = saved;
|
|
132
262
|
expect(proxied[0]?.body.install_id).toBe("install_0123456789abcdef");
|
|
@@ -139,7 +269,7 @@ describe("POST /api/used-by", () => {
|
|
|
139
269
|
key: "feedback_endpoint",
|
|
140
270
|
value: "http://stub.test/v1/feedback",
|
|
141
271
|
});
|
|
142
|
-
await post({ granted: true }, OPERATOR);
|
|
272
|
+
await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
143
273
|
expect(proxied[0]?.url).toBe("http://stub.test/v1/used-by");
|
|
144
274
|
await upsertSwarmConfig({
|
|
145
275
|
scope: "global",
|
|
@@ -151,13 +281,15 @@ describe("POST /api/used-by", () => {
|
|
|
151
281
|
test("a proxy failure is a 502 and leaves the stored consent alone", async () => {
|
|
152
282
|
await upsertSwarmConfig({ scope: "global", key: "SWARM_USED_BY_CONSENT", value: "false" });
|
|
153
283
|
proxyStatus = 500;
|
|
154
|
-
const result = await post({ granted: true }, OPERATOR);
|
|
284
|
+
const result = await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
155
285
|
expect(result.statusCode).toBe(502);
|
|
156
286
|
expect(await configValue("SWARM_USED_BY_CONSENT")).toBe("false");
|
|
157
287
|
});
|
|
158
288
|
|
|
159
289
|
test("a revoke after the website is cleared or changed uses the granted identity", async () => {
|
|
160
|
-
expect(
|
|
290
|
+
expect(
|
|
291
|
+
(await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR)).statusCode,
|
|
292
|
+
).toBe(200);
|
|
161
293
|
await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "" });
|
|
162
294
|
const revoke = await post({ granted: false }, OPERATOR);
|
|
163
295
|
expect(revoke.statusCode).toBe(200);
|
|
@@ -173,7 +305,9 @@ describe("POST /api/used-by", () => {
|
|
|
173
305
|
expect(proxied[2]?.body).toMatchObject({ org_name: "Acme", org_website: "acme.com" });
|
|
174
306
|
|
|
175
307
|
// A new grant sends, and from then on revokes with, the current identity.
|
|
176
|
-
expect(
|
|
308
|
+
expect(
|
|
309
|
+
(await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR)).statusCode,
|
|
310
|
+
).toBe(200);
|
|
177
311
|
expect(proxied[3]?.body).toMatchObject({ org_name: "Acme Labs", org_website: "acme.io" });
|
|
178
312
|
await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "" });
|
|
179
313
|
await post({ granted: false }, OPERATOR);
|
|
@@ -182,7 +316,7 @@ describe("POST /api/used-by", () => {
|
|
|
182
316
|
|
|
183
317
|
test("without a website it is a 400", async () => {
|
|
184
318
|
await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "" });
|
|
185
|
-
const result = await post({ granted: true }, OPERATOR);
|
|
319
|
+
const result = await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
186
320
|
expect(result.statusCode).toBe(400);
|
|
187
321
|
expect(JSON.parse(result.body)).toEqual({
|
|
188
322
|
error: "Set the organization name and website first",
|
|
@@ -192,7 +326,7 @@ describe("POST /api/used-by", () => {
|
|
|
192
326
|
|
|
193
327
|
test("a placeholder name still grants: the ranking's placeholder rule does not apply", async () => {
|
|
194
328
|
await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_NAME", value: "Swarm" });
|
|
195
|
-
const result = await post({ granted: true }, OPERATOR);
|
|
329
|
+
const result = await post({ granted: true, contactEmail: "admin@acme.com" }, OPERATOR);
|
|
196
330
|
expect(result.statusCode).toBe(200);
|
|
197
331
|
expect(proxied[0]?.body).toMatchObject({ org_name: "Swarm", org_website: "acme.com" });
|
|
198
332
|
});
|