@desplega.ai/agent-swarm 2.3.0 → 2.4.0
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 -0
- package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-30m5q8mc.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
- package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-a17tzmn0.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
- package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
- package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-kqypty34.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
- package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-j6vb6ya1.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
- package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-g8fh4c21.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-a86tkj6r.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-ypbzcapd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +844 -110
- package/package.json +1 -1
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +92 -33
- package/src/be/events.ts +11 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- package/src/tests/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +35 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -0
- package/dist/handlers-tb3xgjnm.js +0 -37
- package/dist/index-nyfcdpky.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
package/src/telemetry.ts
CHANGED
|
@@ -630,6 +630,7 @@ function emit(
|
|
|
630
630
|
metadata?: Record<string, unknown>,
|
|
631
631
|
actor?: TelemetryActor,
|
|
632
632
|
emitter: string = source,
|
|
633
|
+
identity?: { orgName: string; orgWebsite: string },
|
|
633
634
|
): void {
|
|
634
635
|
if (!isTelemetryEnabled() || !installationId) return;
|
|
635
636
|
maybeRefreshOrgIdentity();
|
|
@@ -639,7 +640,9 @@ function emit(
|
|
|
639
640
|
const occurredAt = new Date().toISOString();
|
|
640
641
|
const sentFor = installationId;
|
|
641
642
|
const isCloud = cachedIsCloud || isCloudDeployment();
|
|
642
|
-
const orgIdentity =
|
|
643
|
+
const orgIdentity = identity
|
|
644
|
+
? { ...getOrgIdentity(), organization_name: identity.orgName }
|
|
645
|
+
: getOrgIdentity();
|
|
643
646
|
const userId = actor?.userId?.trim() || undefined;
|
|
644
647
|
const payload = {
|
|
645
648
|
schema_version: 2 as const,
|
|
@@ -653,7 +656,7 @@ function emit(
|
|
|
653
656
|
orgId,
|
|
654
657
|
orgName: orgIdentity.organization_name,
|
|
655
658
|
orgDomain: cachedOrgDomain,
|
|
656
|
-
orgWebsite: normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
|
|
659
|
+
orgWebsite: identity?.orgWebsite ?? normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
|
|
657
660
|
installationId,
|
|
658
661
|
actor: userId ? { userId, role: actor?.role ?? roleResolver?.(userId) ?? null } : undefined,
|
|
659
662
|
isCloud,
|
|
@@ -820,6 +823,27 @@ export const telemetry = {
|
|
|
820
823
|
);
|
|
821
824
|
},
|
|
822
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Public-ranking events. The API passes the org identity it just read from
|
|
828
|
+
* config, so a listing built right after an identity edit still matches the
|
|
829
|
+
* envelope's `org_name` / `org_website` (the proxy rejects a mismatch) before
|
|
830
|
+
* the debounced config reload updates `process.env`.
|
|
831
|
+
*/
|
|
832
|
+
ranking<S extends EventSuffix<"ranking">>(
|
|
833
|
+
event: S,
|
|
834
|
+
props: FamilyProps<"ranking", S>,
|
|
835
|
+
identity?: { orgName: string; orgWebsite: string },
|
|
836
|
+
): void {
|
|
837
|
+
emit(
|
|
838
|
+
`ranking.${event}`,
|
|
839
|
+
props as Record<string, unknown>,
|
|
840
|
+
undefined,
|
|
841
|
+
undefined,
|
|
842
|
+
source,
|
|
843
|
+
identity,
|
|
844
|
+
);
|
|
845
|
+
},
|
|
846
|
+
|
|
823
847
|
org<S extends EventSuffix<"org">>(event: S, props: FamilyProps<"org", S>): void {
|
|
824
848
|
emit(`org.${event}`, props as Record<string, unknown>, undefined);
|
|
825
849
|
},
|
|
@@ -7,9 +7,20 @@ import {
|
|
|
7
7
|
type ServerResponse,
|
|
8
8
|
} from "node:http";
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
-
import {
|
|
10
|
+
import type { AppActionCaller } from "../apps/action-caller";
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createPage,
|
|
15
|
+
createUser,
|
|
16
|
+
getDbClient,
|
|
17
|
+
getTaskById,
|
|
18
|
+
initDb,
|
|
19
|
+
} from "../be/db";
|
|
20
|
+
import { createRuntimeController } from "../be/runtime-pools/db";
|
|
21
|
+
import { mintControllerToken } from "../be/runtime-pools/tokens";
|
|
11
22
|
import { upsertScriptByName } from "../be/scripts/db";
|
|
12
|
-
import { type IdentityActor, mintToken } from "../be/users";
|
|
23
|
+
import { type IdentityActor, mintRuntimeToken, mintToken } from "../be/users";
|
|
13
24
|
import { handleApps } from "../http/apps";
|
|
14
25
|
import { resolveHttpRequestAuth } from "../http/auth";
|
|
15
26
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
@@ -19,13 +30,18 @@ import { registerAppDiffTool } from "../tools/app-diff";
|
|
|
19
30
|
import { registerAppGetTool, registerAppQueryTool } from "../tools/app-get";
|
|
20
31
|
import { registerAppHistoryTool } from "../tools/app-history";
|
|
21
32
|
import { registerAppPatchTool } from "../tools/app-patch";
|
|
33
|
+
import { signPageSession } from "../utils/page-session";
|
|
22
34
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
35
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
36
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
|
|
23
37
|
|
|
24
38
|
const TEST_DB_PATH = `/private/tmp/test-apps-rbac-${process.pid}.sqlite`;
|
|
25
39
|
const API_KEY = "example-apps-rbac-test-key";
|
|
26
40
|
const AGENT_ID = crypto.randomUUID();
|
|
27
41
|
const LEAD_ID = crypto.randomUUID();
|
|
28
42
|
const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-rbac-test" };
|
|
43
|
+
const previousPageSecret = process.env.PAGE_SESSION_SECRET;
|
|
44
|
+
const previousSwarmKey = process.env.AGENT_SWARM_API_KEY;
|
|
29
45
|
|
|
30
46
|
const definition = {
|
|
31
47
|
models: { note: { columns: { title: { kind: "string" } } } },
|
|
@@ -45,6 +61,7 @@ let server: Server;
|
|
|
45
61
|
let base = "";
|
|
46
62
|
let userId = "";
|
|
47
63
|
let userToken = "";
|
|
64
|
+
let agentToken = "";
|
|
48
65
|
let appId = "";
|
|
49
66
|
let tools: Record<string, RegisteredTool>;
|
|
50
67
|
|
|
@@ -144,12 +161,15 @@ async function callTool(name: string, args: unknown): Promise<unknown> {
|
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
beforeAll(async () => {
|
|
164
|
+
process.env.PAGE_SESSION_SECRET = "example-apps-rbac-page-session-secret";
|
|
165
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
147
166
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
148
167
|
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
149
168
|
}
|
|
150
169
|
initDb(TEST_DB_PATH);
|
|
151
170
|
await createAgent({ id: AGENT_ID, name: "apps-rbac-worker", isLead: false, status: "idle" });
|
|
152
171
|
await createAgent({ id: LEAD_ID, name: "apps-rbac-lead", isLead: true, status: "idle" });
|
|
172
|
+
agentToken = (await mintRuntimeToken(AGENT_ID, crypto.randomUUID())).plaintext;
|
|
153
173
|
const user = await createUser({ name: "Apps RBAC User" });
|
|
154
174
|
userId = user.id;
|
|
155
175
|
userToken = (await mintToken(user.id, "apps-rbac", OPERATOR_ACTOR)).plaintext;
|
|
@@ -177,6 +197,10 @@ afterEach(() => {
|
|
|
177
197
|
afterAll(async () => {
|
|
178
198
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
179
199
|
closeDb();
|
|
200
|
+
if (previousPageSecret === undefined) delete process.env.PAGE_SESSION_SECRET;
|
|
201
|
+
else process.env.PAGE_SESSION_SECRET = previousPageSecret;
|
|
202
|
+
if (previousSwarmKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
203
|
+
else process.env.AGENT_SWARM_API_KEY = previousSwarmKey;
|
|
180
204
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
181
205
|
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
182
206
|
}
|
|
@@ -323,6 +347,9 @@ describe("app viewer identity", () => {
|
|
|
323
347
|
});
|
|
324
348
|
expect(action.status).toBe(200);
|
|
325
349
|
expect((await getTaskById(action.body.taskId))?.requestedByUserId).toBe(userId);
|
|
350
|
+
expect((await getTaskById(action.body.taskId))?.task).toContain(
|
|
351
|
+
`Caller (server-verified): ${JSON.stringify({ kind: "user", id: userId, name: "Apps RBAC User" })}`,
|
|
352
|
+
);
|
|
326
353
|
|
|
327
354
|
const operatorAction = await request<{ taskId: string }>(
|
|
328
355
|
`/api/apps/${appId}/actions/triage`,
|
|
@@ -342,6 +369,166 @@ describe("app viewer identity", () => {
|
|
|
342
369
|
});
|
|
343
370
|
});
|
|
344
371
|
|
|
372
|
+
describe("verified app action caller", () => {
|
|
373
|
+
beforeEach(async () => {
|
|
374
|
+
const saved = await upsertScriptByName({
|
|
375
|
+
name: `apps_caller_${crypto.randomUUID().replaceAll("-", "")}`,
|
|
376
|
+
scope: "agent",
|
|
377
|
+
scopeId: AGENT_ID,
|
|
378
|
+
agentId: AGENT_ID,
|
|
379
|
+
source: "export default function run(args: Record<string, unknown>) { return args; }",
|
|
380
|
+
description: "Action caller fixture",
|
|
381
|
+
intent: "Return the arguments received by a script action",
|
|
382
|
+
signatureJson: JSON.stringify({ args: { type: "object" }, result: { type: "object" } }),
|
|
383
|
+
typeChecked: true,
|
|
384
|
+
embeddingMode: "skip",
|
|
385
|
+
});
|
|
386
|
+
const patched = await request(`/api/apps/${appId}`, "operator", {
|
|
387
|
+
method: "PATCH",
|
|
388
|
+
body: JSON.stringify({
|
|
389
|
+
definition: {
|
|
390
|
+
actions: {
|
|
391
|
+
echo: {
|
|
392
|
+
kind: "script",
|
|
393
|
+
scriptId: saved.script.id,
|
|
394
|
+
args: { caller: { kind: "guest", id: "default-forgery" }, base: 2 },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
expect(patched.status).toBe(200);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const scriptTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
404
|
+
scriptTest.each([
|
|
405
|
+
["user", undefined],
|
|
406
|
+
["operator", AGENT_ID],
|
|
407
|
+
["operator", crypto.randomUUID()],
|
|
408
|
+
["operator", undefined],
|
|
409
|
+
["agent", AGENT_ID],
|
|
410
|
+
] as const)(
|
|
411
|
+
"%s caller with agent header %s preserves identity and overwrites forged input",
|
|
412
|
+
async (principal, agentId) => {
|
|
413
|
+
const caller: AppActionCaller =
|
|
414
|
+
principal === "user"
|
|
415
|
+
? { kind: "user", id: userId, name: "Apps RBAC User" }
|
|
416
|
+
: agentId === AGENT_ID
|
|
417
|
+
? { kind: "agent", id: AGENT_ID, name: "apps-rbac-worker" }
|
|
418
|
+
: { kind: "operator", id: null };
|
|
419
|
+
const requestHeaders = {
|
|
420
|
+
...(agentId ? { "X-Agent-ID": agentId } : {}),
|
|
421
|
+
...(principal === "agent" ? { Authorization: `Bearer ${agentToken}` } : {}),
|
|
422
|
+
};
|
|
423
|
+
const checks: RbacCheck[] = [];
|
|
424
|
+
setAuditSink((check) => checks.push(check));
|
|
425
|
+
const row = await request<{ row: { createdBy: string } }>(
|
|
426
|
+
`/api/apps/${appId}/models/note/rows`,
|
|
427
|
+
principal,
|
|
428
|
+
{
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: requestHeaders,
|
|
431
|
+
body: JSON.stringify({ values: { title: "caller row" } }),
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
expect(row.status).toBe(201);
|
|
435
|
+
expect(row.body.row.createdBy).toBe(caller.id ? `${caller.kind}:${caller.id}` : "operator");
|
|
436
|
+
|
|
437
|
+
const action = await request<{
|
|
438
|
+
ok: boolean;
|
|
439
|
+
result: { caller: AppActionCaller; app: { id: string }; base: number; add: number };
|
|
440
|
+
}>(`/api/apps/${appId}/actions/echo`, principal, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: requestHeaders,
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
input: { caller: { kind: "operator", id: "input-forgery" }, app: { id: "fake" }, add: 3 },
|
|
445
|
+
}),
|
|
446
|
+
});
|
|
447
|
+
expect(action.status).toBe(200);
|
|
448
|
+
expect(action.body.ok).toBe(true);
|
|
449
|
+
expect(action.body.result).toEqual({ caller, app: { id: appId }, base: 2, add: 3 });
|
|
450
|
+
|
|
451
|
+
const taskAction = await request<{ taskId: string }>(
|
|
452
|
+
`/api/apps/${appId}/actions/triage`,
|
|
453
|
+
principal,
|
|
454
|
+
{ method: "POST", headers: requestHeaders, body: JSON.stringify({ input: {} }) },
|
|
455
|
+
);
|
|
456
|
+
expect(taskAction.status).toBe(200);
|
|
457
|
+
expect((await getTaskById(taskAction.body.taskId))?.task).toContain(
|
|
458
|
+
`Caller (server-verified): ${JSON.stringify(caller)}`,
|
|
459
|
+
);
|
|
460
|
+
expect(checks).toHaveLength(3);
|
|
461
|
+
expect(checks.every((check) => check.principal.kind === principal)).toBe(true);
|
|
462
|
+
},
|
|
463
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
scriptTest(
|
|
467
|
+
"a signed page session includes the verified page id and viewer identity",
|
|
468
|
+
async () => {
|
|
469
|
+
const page = await createPage({
|
|
470
|
+
agentId: AGENT_ID,
|
|
471
|
+
slug: `caller-${crypto.randomUUID()}`,
|
|
472
|
+
title: "Caller page",
|
|
473
|
+
contentType: "text/html",
|
|
474
|
+
body: "<p>Caller page</p>",
|
|
475
|
+
});
|
|
476
|
+
const session = await signPageSession({
|
|
477
|
+
pageId: page.id,
|
|
478
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
479
|
+
uid: userId,
|
|
480
|
+
});
|
|
481
|
+
const action = await request<{ ok: boolean; result: { caller: AppActionCaller } }>(
|
|
482
|
+
`/api/apps/${appId}/actions/echo`,
|
|
483
|
+
"operator",
|
|
484
|
+
{
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: {
|
|
487
|
+
"X-Agent-ID": AGENT_ID,
|
|
488
|
+
"X-Page-ID": page.id,
|
|
489
|
+
"X-Page-Session": session,
|
|
490
|
+
},
|
|
491
|
+
body: JSON.stringify({ input: { caller: { pageId: "fake" } } }),
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
expect(action.status).toBe(200);
|
|
495
|
+
expect(action.body.ok).toBe(true);
|
|
496
|
+
expect(action.body.result.caller).toEqual({
|
|
497
|
+
kind: "user",
|
|
498
|
+
id: userId,
|
|
499
|
+
name: "Apps RBAC User",
|
|
500
|
+
pageId: page.id,
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
test("controller tokens use the guest principal even with a registered agent header", async () => {
|
|
507
|
+
const controller = await createRuntimeController({
|
|
508
|
+
name: "apps-caller-controller",
|
|
509
|
+
kind: "docker",
|
|
510
|
+
placement: "remote",
|
|
511
|
+
});
|
|
512
|
+
const token = await mintControllerToken(controller.id);
|
|
513
|
+
const checks: RbacCheck[] = [];
|
|
514
|
+
setAuditSink((check) => checks.push(check));
|
|
515
|
+
const action = await request(`/api/apps/${appId}/actions/echo`, "operator", {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { Authorization: `Bearer ${token.plaintext}`, "X-Agent-ID": AGENT_ID },
|
|
518
|
+
body: JSON.stringify({ input: {} }),
|
|
519
|
+
});
|
|
520
|
+
expect(action.status).toBe(403);
|
|
521
|
+
expect(checks).toEqual([
|
|
522
|
+
{
|
|
523
|
+
principal: { kind: "guest" },
|
|
524
|
+
verb: "app.use",
|
|
525
|
+
resource: { kind: "app", appId },
|
|
526
|
+
source: "http",
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
345
532
|
describe("user writers and the script ownership gate", () => {
|
|
346
533
|
async function saveScript(scope: "agent" | "global", ownerId: string | null): Promise<string> {
|
|
347
534
|
const result = await upsertScriptByName({
|
|
@@ -127,9 +127,16 @@ describe("renderAppTypes", () => {
|
|
|
127
127
|
expect(rendered).toContain("params: { value: never };");
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
test("renders
|
|
130
|
+
test("renders action names and arguments with server-stamped caller context", () => {
|
|
131
131
|
const rendered = renderAppTypes([app("PM Inbox", issueDefinition())]);
|
|
132
132
|
expect(rendered).toContain('export type ActionName = "closeIssue" | "notifyOwner";');
|
|
133
|
+
expect(rendered).toContain("export interface ActionArgs");
|
|
134
|
+
expect(rendered).toContain('app: { id: "PM Inbox-id" };');
|
|
135
|
+
expect(rendered).toContain("caller: AppActionCaller;");
|
|
136
|
+
expect(rendered).toContain('kind: "user" | "agent" | "operator" | "guest";');
|
|
137
|
+
expect(rendered).toContain("id: string | null;");
|
|
138
|
+
expect(rendered).toContain("name?: string;");
|
|
139
|
+
expect(rendered).toContain("pageId?: string;");
|
|
133
140
|
});
|
|
134
141
|
|
|
135
142
|
test("derives and dedupes namespaces and model interface names", () => {
|
|
@@ -262,14 +269,20 @@ describe("renderAppTypes", () => {
|
|
|
262
269
|
expect(rendered).not.toMatch(/^export const pwned2/m);
|
|
263
270
|
});
|
|
264
271
|
|
|
265
|
-
test("
|
|
272
|
+
test("model names do not collide with generated action types", () => {
|
|
266
273
|
const rendered = renderAppTypes([
|
|
267
274
|
app("Collision", {
|
|
268
|
-
models: {
|
|
275
|
+
models: {
|
|
276
|
+
actionName: { columns: { title: { kind: "string" } } },
|
|
277
|
+
actionArgs: { columns: { title: { kind: "string" } } },
|
|
278
|
+
appActionCaller: { columns: { title: { kind: "string" } } },
|
|
279
|
+
},
|
|
269
280
|
actions: { close: { kind: "task", prompt: "close" } },
|
|
270
281
|
}),
|
|
271
282
|
]);
|
|
272
283
|
expect(rendered).toContain("export interface ActionName_2");
|
|
284
|
+
expect(rendered).toContain("export interface ActionArgs_2");
|
|
285
|
+
expect(rendered).toContain("export interface AppActionCaller_2");
|
|
273
286
|
expect(rendered).toContain('export type ActionName = "close";');
|
|
274
287
|
});
|
|
275
288
|
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
checkPrBody,
|
|
4
4
|
isFixTitle,
|
|
5
|
+
isRankingEntryPr,
|
|
5
6
|
pickedChoices,
|
|
6
7
|
prBodyWarnings,
|
|
7
8
|
proseWordCount,
|
|
@@ -34,6 +35,28 @@ const TEMPLATE = `<!-- top comment -->
|
|
|
34
35
|
|
|
35
36
|
const URGENT = "## Urgency\n- [ ] asap\n- [x] nice to have\n";
|
|
36
37
|
|
|
38
|
+
describe("isRankingEntryPr", () => {
|
|
39
|
+
test("exempts only a one-file entry PR with the matching title", () => {
|
|
40
|
+
expect(
|
|
41
|
+
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.dev.json"]),
|
|
42
|
+
).toBe(true);
|
|
43
|
+
expect(
|
|
44
|
+
isRankingEntryPr("chore(ranking): remove acme.dev", ["community/ranking/acme.dev.json"]),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(
|
|
47
|
+
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.com.json"]),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
isRankingEntryPr("chore(ranking): add acme.dev", [
|
|
51
|
+
"community/ranking/acme.dev.json",
|
|
52
|
+
"src/server.ts",
|
|
53
|
+
]),
|
|
54
|
+
).toBe(false);
|
|
55
|
+
expect(isRankingEntryPr("feat: add acme.dev", ["community/ranking/acme.dev.json"])).toBe(false);
|
|
56
|
+
expect(isRankingEntryPr("chore(ranking): add acme.dev", undefined)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
37
60
|
describe("check-pr-body", () => {
|
|
38
61
|
test("template markers set when a section is required", () => {
|
|
39
62
|
expect(templateSections(TEMPLATE)).toEqual([
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
checkRankingEntries,
|
|
4
|
+
type Entry,
|
|
5
|
+
isEntryPath,
|
|
6
|
+
parseEntry,
|
|
7
|
+
type Verification,
|
|
8
|
+
} from "../../scripts/check-ranking-entry";
|
|
9
|
+
|
|
10
|
+
const CLAIM = "rk_3f9a1c0e7b2d4a6f8e1c3b5d7f9a2c4e";
|
|
11
|
+
const OTHER_CLAIM = "rk_00000000000000000000000000000001";
|
|
12
|
+
const file = (domain: string, claim = CLAIM) =>
|
|
13
|
+
JSON.stringify({ $schema: "./schema.json", domain, claim });
|
|
14
|
+
|
|
15
|
+
const VERIFIED: Verification = {
|
|
16
|
+
verified: true,
|
|
17
|
+
reasons: [],
|
|
18
|
+
telemetryActive7d: true,
|
|
19
|
+
websiteMatchesDomain: true,
|
|
20
|
+
listing: {
|
|
21
|
+
name: "Acme",
|
|
22
|
+
domain: "acme.dev",
|
|
23
|
+
category: "engineering",
|
|
24
|
+
subcategory: "qa-testing",
|
|
25
|
+
description: "QA | release automation",
|
|
26
|
+
sourceCodeUrl: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("parseEntry", () => {
|
|
31
|
+
test("accepts the opt-in file", () => {
|
|
32
|
+
expect(parseEntry("acme.dev.json", file("acme.dev"))).toEqual({
|
|
33
|
+
domain: "acme.dev",
|
|
34
|
+
claim: CLAIM,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test.each([
|
|
39
|
+
["acme.dev.json", "{", "valid JSON"],
|
|
40
|
+
["acme.dev.json", "[]", "JSON object"],
|
|
41
|
+
[
|
|
42
|
+
"acme.dev.json",
|
|
43
|
+
JSON.stringify({ domain: "acme.dev", claim: CLAIM, name: "Acme" }),
|
|
44
|
+
"unknown keys",
|
|
45
|
+
],
|
|
46
|
+
["www.acme.dev.json", file("www.acme.dev"), "bare lowercase hostname"],
|
|
47
|
+
["Acme.ba.json", file("Acme.ba"), "bare lowercase hostname"],
|
|
48
|
+
["other.ba.json", file("acme.dev"), "must be named acme.dev.json"],
|
|
49
|
+
["acme.dev.json", file("acme.dev", "rk_short"), "claim"],
|
|
50
|
+
])("rejects %s %s", (name, text, message) => {
|
|
51
|
+
expect(() => parseEntry(name, text)).toThrow(message);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("README and schema are not entries", () => {
|
|
55
|
+
expect(isEntryPath("community/ranking/acme.dev.json")).toBe(true);
|
|
56
|
+
expect(isEntryPath("community/ranking/schema.json")).toBe(false);
|
|
57
|
+
expect(isEntryPath("community/ranking/README.md")).toBe(false);
|
|
58
|
+
expect(isEntryPath("community/ranking/nested/acme.dev.json")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("checkRankingEntries", () => {
|
|
63
|
+
const files: Record<string, string> = {
|
|
64
|
+
"community/ranking/acme.dev.json": file("acme.dev"),
|
|
65
|
+
"community/ranking/acme.com.json": file("acme.com", CLAIM),
|
|
66
|
+
};
|
|
67
|
+
const run = (
|
|
68
|
+
paths: string[],
|
|
69
|
+
verify: (entry: Entry) => Promise<Verification>,
|
|
70
|
+
all = ["acme.dev.json"],
|
|
71
|
+
) =>
|
|
72
|
+
checkRankingEntries({
|
|
73
|
+
changes: paths.map((path) => ({ status: "A", path })),
|
|
74
|
+
readEntry: (path) => files[path] ?? "",
|
|
75
|
+
allEntries: all.map((name) => [name, files[`community/ranking/${name}`] ?? ""]),
|
|
76
|
+
verify,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a verified entry passes and the summary shows the listing", async () => {
|
|
80
|
+
const asked: Entry[] = [];
|
|
81
|
+
const result = await run(["community/ranking/acme.dev.json"], async (entry) => {
|
|
82
|
+
asked.push(entry);
|
|
83
|
+
return VERIFIED;
|
|
84
|
+
});
|
|
85
|
+
expect(result.problems).toEqual([]);
|
|
86
|
+
expect(asked).toEqual([{ domain: "acme.dev", claim: CLAIM }]);
|
|
87
|
+
const summary = result.summary.join("\n");
|
|
88
|
+
expect(summary).toContain("### acme.dev: verified");
|
|
89
|
+
expect(summary).toContain(
|
|
90
|
+
"| Acme | engineering | qa-testing | QA \\| release automation | — |",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an unverified entry fails with the proxy's reasons", async () => {
|
|
95
|
+
const result = await run(["community/ranking/acme.dev.json"], async () => ({
|
|
96
|
+
...VERIFIED,
|
|
97
|
+
verified: false,
|
|
98
|
+
reasons: ["telemetry_inactive_7d"],
|
|
99
|
+
}));
|
|
100
|
+
expect(result.problems).toHaveLength(1);
|
|
101
|
+
expect(result.problems[0]).toContain("telemetry_inactive_7d");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a proxy error fails without throwing", async () => {
|
|
105
|
+
const result = await run(["community/ranking/acme.dev.json"], async () => {
|
|
106
|
+
throw new Error("the proxy answered 503");
|
|
107
|
+
});
|
|
108
|
+
expect(result.problems[0]).toContain("503");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a claim shared with another entry fails before verify", async () => {
|
|
112
|
+
let called = false;
|
|
113
|
+
const result = await run(["community/ranking/acme.com.json"], async () => {
|
|
114
|
+
called = true;
|
|
115
|
+
return VERIFIED;
|
|
116
|
+
}, ["acme.dev.json", "acme.com.json"]);
|
|
117
|
+
expect(called).toBe(false);
|
|
118
|
+
expect(result.problems[0]).toContain("acme.dev.json");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a removal needs no proof, and README edits are ignored", async () => {
|
|
122
|
+
const result = await checkRankingEntries({
|
|
123
|
+
changes: [
|
|
124
|
+
{ status: "D", path: "community/ranking/acme.dev.json" },
|
|
125
|
+
{ status: "M", path: "community/ranking/README.md" },
|
|
126
|
+
],
|
|
127
|
+
readEntry: () => "",
|
|
128
|
+
allEntries: [],
|
|
129
|
+
verify: async () => {
|
|
130
|
+
throw new Error("must not be called");
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
expect(result.problems).toEqual([]);
|
|
134
|
+
expect(result.summary).toEqual(["- Removed `acme.dev.json`."]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("the claim differs per entry in a valid two-entry PR", async () => {
|
|
138
|
+
files["community/ranking/acme.com.json"] = file("acme.com", OTHER_CLAIM);
|
|
139
|
+
const result = await run(
|
|
140
|
+
["community/ranking/acme.com.json", "community/ranking/acme.dev.json"],
|
|
141
|
+
async () => VERIFIED,
|
|
142
|
+
["acme.dev.json", "acme.com.json"],
|
|
143
|
+
);
|
|
144
|
+
expect(result.problems).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -15,6 +15,7 @@ test("facade exposes repository bindings without internal exports", () => {
|
|
|
15
15
|
"rowToAgentTaskSummary",
|
|
16
16
|
"configureTaskReadDependencies",
|
|
17
17
|
"NEVER_STARTED_TASK_STATUSES",
|
|
18
|
+
"readableSessionCondition",
|
|
18
19
|
].includes(name)
|
|
19
20
|
) {
|
|
20
21
|
expect(name in db).toBe(false);
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(436);
|
|
107
|
+
expect(generatedKeys).toHaveLength(436);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { orgIdentityStatus } from "../utils/org-identity";
|
|
3
|
+
|
|
4
|
+
describe("orgIdentityStatus", () => {
|
|
5
|
+
test.each([
|
|
6
|
+
["empty", {}, ["SWARM_ORG_NAME", "SWARM_ORG_WEBSITE"]],
|
|
7
|
+
[
|
|
8
|
+
"whitespace",
|
|
9
|
+
{ SWARM_ORG_NAME: " ", SWARM_ORG_WEBSITE: " " },
|
|
10
|
+
["SWARM_ORG_NAME", "SWARM_ORG_WEBSITE"],
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
"the catalog default",
|
|
14
|
+
{ SWARM_ORG_NAME: "Swarm", SWARM_ORG_WEBSITE: "acme.com" },
|
|
15
|
+
["SWARM_ORG_NAME"],
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"the Slack fallback",
|
|
19
|
+
{ SWARM_ORG_NAME: " your swarm ", SWARM_ORG_WEBSITE: "acme.com" },
|
|
20
|
+
["SWARM_ORG_NAME"],
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
"an invalid website",
|
|
24
|
+
{ SWARM_ORG_NAME: "Acme", SWARM_ORG_WEBSITE: "localhost" },
|
|
25
|
+
["SWARM_ORG_WEBSITE"],
|
|
26
|
+
],
|
|
27
|
+
["both set", { SWARM_ORG_NAME: " Acme ", SWARM_ORG_WEBSITE: "https://www.Acme.com/" }, []],
|
|
28
|
+
] as const)("%s", (_label, src, missing) => {
|
|
29
|
+
const status = orgIdentityStatus(src);
|
|
30
|
+
expect(status.missing).toEqual([...missing]);
|
|
31
|
+
expect(status.ok).toBe(missing.length === 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns the trimmed name and the normalized website", () => {
|
|
35
|
+
expect(
|
|
36
|
+
orgIdentityStatus({ SWARM_ORG_NAME: " Acme ", SWARM_ORG_WEBSITE: "https://www.Acme.com/" }),
|
|
37
|
+
).toEqual({
|
|
38
|
+
name: "Acme",
|
|
39
|
+
website: "acme.com",
|
|
40
|
+
missing: [],
|
|
41
|
+
ok: true,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|